eletrotupi / tcc/ api/src/static/js/app.js7c779e23b4945ae724e7a21d58f8f30d34ab10b8
6.9 KB | 269 lines Raw
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
  const img = container.querySelector('img');
115
  if (img) container.replaceChild(svg, img);
116
  else container.appendChild(svg);
117

118
  // Motion path
119
  const totalLen = logoPathEl.getTotalLength();
120
  if (!totalLen) return;
121

122
  const DURATION = 12000;
123
  let rafStart = null;
124

125
  function traceDot(ts) {
126
    if (rafStart === null) rafStart = ts;
127
    const t = ((ts - rafStart) % DURATION) / DURATION;
128
    const pt = logoPathEl.getPointAtLength(t * totalLen);
129

130
    dot.setAttribute('cx', pt.x);
131
    dot.setAttribute('cy', pt.y);
132
    trail.setAttribute('cx', pt.x);
133
    trail.setAttribute('cy', pt.y);
134
    requestAnimationFrame(traceDot);
135
  }
136
  requestAnimationFrame(traceDot);
137

138
  // morphTo experiment
139
  container.addEventListener('pointerenter', () => {
140
    animate(logoPathEl, { d: morphTo(starEl), duration: 900, ease: 'inOutCubic' });
141
  });
142

143
  container.addEventListener('pointerleave', () => {
144
    animate(logoPathEl, { d: morphTo(origEl), duration: 900, ease: 'inOutCubic' });
145
  });
146
}
147

148
document.addEventListener('DOMContentLoaded', function () {
149
  const colors = [];
150

151
  new Glide('.glide', {
152
    type: 'carousel',
153
    autoplay: 2000,
154
    startAt: 0,
155
    perView: 2.5,
156
    gap: 32,
157
    breakpoints: {
158
      1024: {
159
        perView: 2
160
      },
161
      768: {
162
        perView: 1
163
      }
164
    }
165
  }).mount();
166

167
  // Initialize Atropos 3D Effect
168
  const dash = Atropos({
169
    el: '.my-atropos',
170
    activeOffset: 40,
171
    shadow: true,
172
    shadowScale: 1.05,
173
    onEnter() {
174
      console.log('Atropos Enter');
175
    },
176
    onLeave() {
177
      console.log('Atropos Leave');
178
    }
179
  });
180

181

182
  // Anime js text bullshit
183
  splitText('.animated-text', {
184
    lines: true,
185
  })
186
    .addEffect(({ lines }) => animate(lines, {
187
      y: ['20%', '-20%'],
188
      loop: true,
189
      alternate: true,
190
      delay: stagger(15000),
191
      ease: 'inOutQuad',
192
    }))
193
    .addEffect(split => {
194
      split.words.forEach(($el, i) => {
195
        const color = colors[i];
196
        if (color) utils.set($el, { color });
197
        $el.addEventListener('pointerenter', () => {
198
          animate($el, {
199
            color: utils.randomPick(['#FF4B4B', '#FFCC2A', '#B7FF54', '#57F695']),
200
            duration: 250,
201
          })
202
        });
203
      });
204
      return () => {
205
        /* Called between each split */
206
        split.words.forEach((w, i) => colors[i] = utils.get(w, 'color'));
207
      }
208
    });
209

210
  setupLogoAnimations();
211

212
  document.querySelectorAll('.glide__slide .morph-icon').forEach(svg => {
213
    const path = svg.querySelector(':scope > path');
214
    const resting = svg.querySelector('defs .morph-resting');
215
    const alt = svg.querySelector('defs .morph-alt');
216
    const shape = svg.querySelector('defs .morph-shape');
217
    const card = svg.closest('.group');
218

219
    let hovered = false;
220
    let spikeCycle = null;
221

222
    const rotAnim = animate(svg, {
223
      rotate: 360,
224
      loop: true,
225
      duration: 7000,
226
      ease: 'linear',
227
    });
228

229
    function runSpikeCycle() {
230
      if (hovered) return;
231
      const targets = [resting, alt];
232
      let i = 0;
233

234
      function step() {
235
        if (hovered) return;
236
        i = (i + 1) % targets.length;
237
        spikeCycle = animate(path, {
238
          d: morphTo(targets[i]),
239
          duration: 1800,
240
          ease: 'inOutSine',
241
          onComplete: () => setTimeout(step, 600),
242
        });
243
      }
244
      step();
245
    }
246

247
    runSpikeCycle();
248

249
    card.addEventListener('pointerenter', () => {
250
      hovered = true;
251
      spikeCycle?.pause();
252
      rotAnim.pause();
253
      animate(path, { d: morphTo(shape), duration: 500, ease: 'inOutCubic' });
254
    });
255

256
    card.addEventListener('pointerleave', () => {
257
      hovered = false;
258
      animate(path, {
259
        d: morphTo(resting),
260
        duration: 500,
261
        ease: 'inOutCubic',
262
        onComplete: () => {
263
          rotAnim.play();
264
          runSpikeCycle();
265
        },
266
      });
267
    });
268
  });
269
});