| 1 | const { animate, utils, stagger, splitText, morphTo } = anime;
|
| 2 |
|
| 3 | // Motion path: glowing dot traces the logo outline.
|
| 4 | // morphTo on hover: logo → 16-pt star, tries to mimic the logo
|
| 5 |
|
| 6 | // XXX: morphTo note: animejs tries to find the shorter path to match the
|
| 7 | // logo's in ~300 commands, so the star gets hundreds of "stacked" points. The
|
| 8 | // morph works but looks like a chaotic crystallisation rather than a clean
|
| 9 | // shape-to-shape swap. Not gonna waste more time on this bullshit
|
| 10 | async function setupLogoAnimations() {
|
| 11 | const container = document.getElementById('logo-container');
|
| 12 | if (!container) return;
|
| 13 |
|
| 14 | let pathD;
|
| 15 | try {
|
| 16 | const text = await fetch('/images/logo.svg').then(r => r.text());
|
| 17 | pathD = new DOMParser()
|
| 18 | .parseFromString(text, 'image/svg+xml')
|
| 19 | .querySelector('path')
|
| 20 | .getAttribute('d');
|
| 21 | } catch {
|
| 22 | return;
|
| 23 | }
|
| 24 |
|
| 25 | // Blah
|
| 26 | const NS = 'http://www.w3.org/2000/svg';
|
| 27 | const mk = (tag, attrs = {}) => {
|
| 28 | const el = document.createElementNS(NS, tag);
|
| 29 | Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
|
| 30 | return el;
|
| 31 | };
|
| 32 |
|
| 33 | const svg = mk('svg', { viewBox: '0 0 600 600' });
|
| 34 | svg.style.cssText = 'width:100%;height:100%;display:block;overflow:visible';
|
| 35 |
|
| 36 | const defs = mk('defs');
|
| 37 |
|
| 38 | // Radial gradient: bright upper-left highlight fading to a deeper green at
|
| 39 | // edges
|
| 40 | const grad = mk('radialGradient', {
|
| 41 | id: 'logo-gradient',
|
| 42 | cx: '42%',
|
| 43 | cy: '36%',
|
| 44 | r: '65%',
|
| 45 | gradientUnits: 'objectBoundingBox'
|
| 46 | });
|
| 47 |
|
| 48 | grad.appendChild(mk('stop', { offset: '0%', 'stop-color': '#2dff80' }));
|
| 49 | grad.appendChild(mk('stop', { offset: '52%', 'stop-color': '#13ec5b' }));
|
| 50 | grad.appendChild(mk('stop', { offset: '100%', 'stop-color': '#09a042' }));
|
| 51 | defs.appendChild(grad);
|
| 52 |
|
| 53 | // Glow filter for the dot
|
| 54 | const dotFilter = mk('filter', {
|
| 55 | id: 'dot-glow',
|
| 56 | x: '-300%',
|
| 57 | y: '-300%',
|
| 58 | width: '700%',
|
| 59 | height: '700%'
|
| 60 | });
|
| 61 |
|
| 62 | dotFilter.appendChild(mk('feGaussianBlur', {
|
| 63 | in: 'SourceGraphic',
|
| 64 | stdDeviation: '6',
|
| 65 | result: 'blur'
|
| 66 | }));
|
| 67 |
|
| 68 | const merge = mk('feMerge');
|
| 69 | merge.appendChild(mk('feMergeNode', { in: 'blur' }));
|
| 70 | merge.appendChild(mk('feMergeNode', { in: 'SourceGraphic' }));
|
| 71 | dotFilter.appendChild(merge);
|
| 72 | defs.appendChild(dotFilter);
|
| 73 |
|
| 74 | svg.appendChild(defs);
|
| 75 |
|
| 76 | // Logo path: gradient fill
|
| 77 | const logoPathEl = mk('path', {
|
| 78 | fill: 'url(#logo-gradient)',
|
| 79 | filter: 'url(#logo-texture)',
|
| 80 | d: pathD
|
| 81 | });
|
| 82 |
|
| 83 | svg.appendChild(logoPathEl);
|
| 84 |
|
| 85 | // morphTo targets (same 0-600 coordinate space as the logo)
|
| 86 | const starD = 'M300,30 L322,192 L466,109 L371,258 L562,296 L371,340 L466,489 L322,412 L300,570 L278,412 L134,489 L229,340 L38,296 L229,258 L134,109 L278,192 Z';
|
| 87 | const starEl = mk('path', { d: starD, visibility: 'hidden' });
|
| 88 | const origEl = mk('path', { d: pathD, visibility: 'hidden' });
|
| 89 | svg.appendChild(starEl);
|
| 90 | svg.appendChild(origEl);
|
| 91 |
|
| 92 | // Motion dot
|
| 93 | const trail = mk('circle', {
|
| 94 | r: '24',
|
| 95 | fill: '#13ec5b',
|
| 96 | opacity: '0.15',
|
| 97 | filter: 'url(#dot-glow)',
|
| 98 | cx: '316',
|
| 99 | cy: '578'
|
| 100 | });
|
| 101 |
|
| 102 | const dot = mk('circle', {
|
| 103 | r: '8',
|
| 104 | fill: 'black',
|
| 105 | opacity: '0.85',
|
| 106 | filter: 'url(#dot-glow)',
|
| 107 | cx: '316',
|
| 108 | cy: '578'
|
| 109 | });
|
| 110 |
|
| 111 | svg.appendChild(trail);
|
| 112 | svg.appendChild(dot);
|
| 113 |
|
| 114 | container.appendChild(svg);
|
| 115 |
|
| 116 | // Motion path
|
| 117 | const totalLen = logoPathEl.getTotalLength();
|
| 118 | if (!totalLen) return;
|
| 119 |
|
| 120 | const DURATION = 12000;
|
| 121 | let rafStart = null;
|
| 122 |
|
| 123 | function traceDot(ts) {
|
| 124 | if (rafStart === null) rafStart = ts;
|
| 125 | const t = ((ts - rafStart) % DURATION) / DURATION;
|
| 126 | const pt = logoPathEl.getPointAtLength(t * totalLen);
|
| 127 |
|
| 128 | dot.setAttribute('cx', pt.x);
|
| 129 | dot.setAttribute('cy', pt.y);
|
| 130 | trail.setAttribute('cx', pt.x);
|
| 131 | trail.setAttribute('cy', pt.y);
|
| 132 | requestAnimationFrame(traceDot);
|
| 133 | }
|
| 134 | requestAnimationFrame(traceDot);
|
| 135 |
|
| 136 | // morphTo experiment
|
| 137 | container.addEventListener('pointerenter', () => {
|
| 138 | animate(logoPathEl, { d: morphTo(starEl), duration: 900, ease: 'inOutCubic' });
|
| 139 | });
|
| 140 |
|
| 141 | container.addEventListener('pointerleave', () => {
|
| 142 | animate(logoPathEl, { d: morphTo(origEl), duration: 900, ease: 'inOutCubic' });
|
| 143 | });
|
| 144 | }
|
| 145 |
|
| 146 | document.addEventListener('DOMContentLoaded', function () {
|
| 147 | const colors = [];
|
| 148 |
|
| 149 | new Glide('.glide', {
|
| 150 | type: 'carousel',
|
| 151 | autoplay: 2000,
|
| 152 | startAt: 0,
|
| 153 | perView: 2.5,
|
| 154 | gap: 32,
|
| 155 | breakpoints: {
|
| 156 | 1024: {
|
| 157 | perView: 2
|
| 158 | },
|
| 159 | 768: {
|
| 160 | perView: 1
|
| 161 | }
|
| 162 | }
|
| 163 | }).mount();
|
| 164 |
|
| 165 | // Initialize Atropos 3D Effect
|
| 166 | const dash = Atropos({
|
| 167 | el: '.my-atropos',
|
| 168 | activeOffset: 40,
|
| 169 | shadow: true,
|
| 170 | shadowScale: 1.05,
|
| 171 | onEnter() {
|
| 172 | console.log('Atropos Enter');
|
| 173 | },
|
| 174 | onLeave() {
|
| 175 | console.log('Atropos Leave');
|
| 176 | }
|
| 177 | });
|
| 178 |
|
| 179 |
|
| 180 | // Anime js text bullshit
|
| 181 | splitText('.animated-text', {
|
| 182 | lines: true,
|
| 183 | })
|
| 184 | .addEffect(({ lines }) => animate(lines, {
|
| 185 | y: ['20%', '-20%'],
|
| 186 | loop: true,
|
| 187 | alternate: true,
|
| 188 | delay: stagger(15000),
|
| 189 | ease: 'inOutQuad',
|
| 190 | }))
|
| 191 | .addEffect(split => {
|
| 192 | split.words.forEach(($el, i) => {
|
| 193 | const color = colors[i];
|
| 194 | if (color) utils.set($el, { color });
|
| 195 | $el.addEventListener('pointerenter', () => {
|
| 196 | animate($el, {
|
| 197 | color: utils.randomPick(['#FF4B4B', '#FFCC2A', '#B7FF54', '#57F695']),
|
| 198 | duration: 250,
|
| 199 | })
|
| 200 | });
|
| 201 | });
|
| 202 | return () => {
|
| 203 | /* Called between each split */
|
| 204 | split.words.forEach((w, i) => colors[i] = utils.get(w, 'color'));
|
| 205 | }
|
| 206 | });
|
| 207 |
|
| 208 | setupLogoAnimations();
|
| 209 |
|
| 210 | document.querySelectorAll('.glide__slide .morph-icon').forEach(svg => {
|
| 211 | const path = svg.querySelector(':scope > path');
|
| 212 | const resting = svg.querySelector('defs .morph-resting');
|
| 213 | const shape = svg.querySelector('defs .morph-shape');
|
| 214 | const card = svg.closest('.group');
|
| 215 |
|
| 216 | card.addEventListener('pointerenter', () => {
|
| 217 | animate(path, { d: morphTo(shape), duration: 500, ease: 'inOutCubic' });
|
| 218 | });
|
| 219 | card.addEventListener('pointerleave', () => {
|
| 220 | animate(path, { d: morphTo(resting), duration: 500, ease: 'inOutCubic' });
|
| 221 | });
|
| 222 | });
|
| 223 | });
|