Setting the file. One moment. Cta Orbit Collapse · Hyperframes Animation · heygen-com/hyperframes · Skills DocsBlueprints Index
(opens in a new tab)
examples/cta-orbit-collapse.html
HTML·1,298 lines·49 KB
8 <!--
9 Blueprint: cta-orbit-collapse (HyperFrames preview)
10 Six HyperFrames technology categories orbit a central "Drop an HTML
11 scene · Render" CTA. A cursor flies in, clicks, the icons collapse,
12 and a demo card springs out from the collapse point — all in 6.5s.
13
14 Palette: mirrors scene-05-orbit-collapse-action.tsx
15 Background: near-black frosted glass (#0A0A0F base + violet/cyan/pink
16 glow blobs + rgba(15,15,25,0.35) frosted tint).
17 Brand: Tailwind violet ramp
18 #c4b5fd (300) → #a78bfa (400) → #8b5cf6 (500) → #7c3aed (600)
19 matching COLORS.brand.purple / purpleGlow in the source.
20 CTA card: rgba(20,20,30,0.8) — scene-05 inner CTA dark glass.
21 CTA button: white fill, #0A0A0F text — identical to scene-05.
22
23 Six orbiting tech icons (driven by ICONS array, evenly spaced 2π/6):
24 HTML angle 0, delay 0.00s — angle brackets + center slash
25 CSS angle π/3, delay 0.10s — rounded frame + 3 color swatches
26 SVG angle 2π/3, delay 0.20s — bezier curve + 2 anchor points
27 GSAP angle π, delay 0.30s — sine wave timeline + playhead
28 3D angle 4π/3, delay 0.40s — isometric cube + inner facets
29 LOTTIE angle 5π/3, delay 0.50s — 2 diamond keyframes on timeline
30
31 Choreography (5 phases, 6.5 seconds total):
32 0.00 – 1.05s Six icons enter staggered with 3D flip + stroke-draw
33 (each 0.55s back.out(1.4) spring; outline draws via
34 strokeDashoffset; inner accents fade in with 0.10s
35 stagger 0.30s after each icon's entry begins)
36 0.00 – 2.95s Continuous orbit (0.25 rad/sec) with per-icon idle
37 wobble (±6 px y + ±2.5° rotation, damped by collapse)
38 1.50 – 2.20s Cursor fades in off-screen-right and slides to the
39 CTA's white button (back.out(1.3) overshoot landing)
40 2.20 – 2.46s Click — cursor (0.85) + CTA button (0.95) compress;
41 white boxShadow glow pulse; ripple 0.3 → 5.0 over 0.70s
42 2.20 – 3.05s Icons collapse toward center (back.out(1.6) ease);
43 radiusFactor 1 → 0, scale 1 → 0.5, opacity envelope
44 2.95 – 3.75s Demo card springs out from collapse point (scale 0 → 1)
45 3.95 – 6.50s Demo floats with finite-yoyo breathing (±8 px, ±1° tilt)
46
47 Continuous SVG enrichment (per icon, gated by entryDelay):
48 HTML brackets: horizontal sway ±0.6 units, period ~1.8s
49 CSS swatches: three dots scale ±0.18 with 1.4 rad phase offsets
50 SVG bezier: curve group rotation ±5°, period ~2.9s
51 GSAP timeline: playhead marker traverses 0→18 + wave scaleY ±0.12
52 3D cube: linear rotation 30°/s (continuous tumble)
53 LOTTIE keyframes: two diamonds scale ±0.12 with 1.6 rad phase offset
54
55 Continuous ambient accents (single scene-ticker writes all):
56 Per-icon tile glow pulse (base 22px ±12px, phased by icon index)
57 CTA breathing aura (base 38px ±26px, frozen at click)
58 Demo glow pulse (base 72px ±28px, runs after demo emerges)
59 Background radial wash (opacity 0.02 ↔ 0.42, period ~11.4s)
60 -->
61
62 <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
63
64 <style>
65 :root {
66 /* Palette mirrors scene-05-orbit-collapse-action.tsx — Tailwind violet
67 ramp on a near-black frosted-glass base, matching COLORS.brand.purple
68 and CTA_COLORS in */
69 --bg-dark: #0a0a0f;
70 --text-primary: #ffffff;
71 --text-muted: rgba(255, 255, 255, 0.4);
72 --brand-purple: #8b5cf6; /* violet-500 — COLORS.brand.purple */
73 --brand-purple-light: #a78bfa; /* violet-400 */
74 --brand-purple-soft: #c4b5fd; /* violet-300 */
75 --brand-purple-deep: #7c3aed; /* violet-600 */
76 --brand-purple-glow: rgba(139, 92, 246, 0.6);
77 --glass-bg: rgba(255, 255, 255, 0.08); /* matches CTA_COLORS.inputBg */
78 --glass-border: rgba(139, 92, 246, 0.4); /* matches scene-05 icon border */
79 --cta-dark-bg: rgba(20, 20, 30, 0.8); /* scene-05 CTA card background */
80 --record-red: #ef4444;
81 }
82 * {
83 margin: 0;
84 padding: 0;
85 box-sizing: border-box;
86 }
87 html,
88 body {
89 margin: 0;
90 width: 1920px;
91 height: 1080px;
92 overflow: hidden;
93 background: var(--bg-dark);
94 font-family:
95 "Inter",
96 system-ui,
97 -apple-system,
98 sans-serif;
99 color: var(--text-primary);
100 }
101
102 /* ============================================================
103 BACKGROUND — frosted glass over near-black base.
104 Mirrors scene-05 (FrostedGlassBackground): a deep dark radial
105 base + violet/cyan/pink glow blobs + a frosted overlay tint.
106 The blobs are static here (no per-frame jitter) to keep the
107 seek-safe contract; the orbit/CTA on top carries all motion.
108 ============================================================ */
109 .stage {
110 position: absolute;
111 inset: 0;
112 perspective: 1400px;
113 }
114 .bg {
115 position: absolute;
116 inset: 0;
117 background:
118 /* mirrored violet blobs — left + right symmetry so the wash
119 reads as a single coherent purple atmosphere rather than
120 a directional glow. */
121 radial-gradient(circle at 25% 40%, rgba(139, 92, 246, 0.55), transparent 55%),
122 radial-gradient(circle at 75% 40%, rgba(139, 92, 246, 0.55), transparent 55%),
123 /* deeper violet pool at the bottom centerline */
124 radial-gradient(ellipse 80% 50% at 50% 90%, rgba(124, 58, 237, 0.35), transparent 65%),
125 /* deep dark base */
126 radial-gradient(
127 ellipse 120% 100% at 50% 20%,
128 rgba(30, 30, 45, 1) 0%,
129 rgba(18, 18, 28, 1) 40%,
130 rgba(10, 10, 15, 1) 100%
131 );
132 }
133 .bg::after {
134 /* frosted overlay tint — emulates the rgba(15,15,25,0.4) layer
135 in scene-02's DarkFrostedGlassBackground. */
136 content: "";
137 position: absolute;
138 inset: 0;
139 background: rgba(15, 15, 25, 0.35);
140 pointer-events: none;
141 }
142 .bg-overlay {
143 position: absolute;
144 inset: 0;
145 background: rgba(10, 10, 15, 0.3);
146 }
147
148 /* ============================================================
149 ORBIT ICONS — three nested wrappers per icon.
150 .icon-pos ← orbit x/y from master onUpdate
151 .icon-collapse ← collapse scale/opacity from master onUpdate
152 .icon-entry ← 3D-flip entry from per-icon fromTo tween
153 ============================================================ */
154 .orbit-stage {
155 position: absolute;
156 inset: 0;
157 }
158 .icon-pos {
159 position: absolute;
160 left: 50%;
161 top: 50%;
162 width: 140px;
163 height: 170px;
164 margin: -85px 0 0 -70px; /* recenter the 140×170 box */
165 perspective: 800px;
166 will-change: transform;
167 }
168 .icon-collapse {
169 width: 100%;
170 height: 100%;
171 will-change: transform, opacity;
172 }
173 .icon-entry {
174 width: 100%;
175 height: 100%;
176 display: flex;
177 flex-direction: column;
178 align-items: center;
179 justify-content: center;
180 gap: 8px;
181 transform-style: preserve-3d;
182 will-change: transform, opacity;
183 }
184 .icon-tile {
185 width: 110px;
186 height: 110px;
187 border-radius: 26px;
188 background: var(--glass-bg);
189 backdrop-filter: blur(10px);
190 border: 1px solid var(--glass-border);
191 display: flex;
192 align-items: center;
193 justify-content: center;
194 box-shadow:
195 0 15px 40px rgba(0, 0, 0, 0.35),
196 0 0 32px var(--brand-purple-glow),
197 inset 0 1px 0 rgba(255, 255, 255, 0.1);
198 }
199 .icon-svg {
200 width: 60px;
201 height: 60px;
202 display: block;
203 }
204 .icon-label {
205 font-size: 14px;
206 font-weight: 500;
207 color: rgba(255, 255, 255, 0.7);
208 text-transform: uppercase;
209 letter-spacing: 0.1em;
210 text-shadow: 0 0 14px rgba(139, 92, 246, 0.5);
211 }
212
213 /* SVG inner-element transform origins (in viewBox units).
214 Each origin is the geometric center of the element it animates
215 (the path's bbox center, or for circles, the cx/cy point). */
216 .html-brackets-g {
217 transform-origin: 12px 12px;
218 }
219 .css-dot-a {
220 transform-origin: 8px 9px;
221 }
222 .css-dot-b {
223 transform-origin: 16px 9px;
224 }
225 .css-dot-c {
226 transform-origin: 12px 16px;
227 }
228 .svg-curve-g {
229 transform-origin: 12px 12px;
230 }
231 .gsap-wave-g {
232 transform-origin: 12px 12px;
233 }
234 .gsap-marker {
235 transform-origin: 3px 12px;
236 }
237 .three-cube-g {
238 transform-origin: 12px 12px;
239 }
240 .lottie-kf-a {
241 transform-origin: 8px 12px;
242 }
243 .lottie-kf-b {
244 transform-origin: 16px 12px;
245 }
246
247 /* ============================================================
248 CENTER CTA — Drop a video link · Get free clips
249 ============================================================ */
250 .cta {
251 position: absolute;
252 left: 50%;
253 top: 50%;
254 /* xPercent/yPercent set by GSAP for centering — so GSAP rotateY
255 doesn't overwrite a CSS translate(-50%,-50%). */
256 z-index: 5;
257 display: flex;
258 align-items: center;
259 background: var(--cta-dark-bg); /* rgba(20,20,30,0.8) — scene-05 */
260 backdrop-filter: blur(14px);
261 border-radius: 36px;
262 padding: 10px 10px 10px 28px;
263 gap: 16px;
264 border: 1px solid var(--glass-border);
265 box-shadow:
266 0 40px 80px rgba(0, 0, 0, 0.5),
267 0 0 60px var(--brand-purple-glow),
268 inset 0 1px 0 rgba(255, 255, 255, 0.1);
269 transform-style: preserve-3d;
270 will-change: transform, opacity;
271 }
272 .cta-link-icon {
273 width: 22px;
274 height: 22px;
275 color: var(--text-muted);
276 filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.5));
277 }
278 .cta-placeholder {
279 font-size: 22px;
280 color: var(--text-muted);
281 min-width: 220px;
282 font-weight: 500;
283 }
284 .cta-button {
285 padding: 18px 36px;
286 background: #ffffff;
287 border-radius: 28px;
288 color: #0a0a0f; /* scene-05 button text */
289 font-size: 22px;
290 font-weight: 600;
291 white-space: nowrap;
292 box-shadow:
293 0 0 30px rgba(255, 255, 255, 0.35),
294 inset 0 -2px 6px rgba(139, 92, 246, 0.15);
295 will-change: transform, box-shadow;
296 }
297
298 /* ============================================================
299 RIPPLE — centered on the CTA's white button.
300 Button center sits at (CENTER_X + 130, CENTER_Y + 15) — same target
301 as the cursor's CURSOR_TARGET. Margin offsets re-center the 120 px
302 ring on that point. Avoid `margin` shorthand here so the offsets
303 can't be overridden by a stray longhand later.
304 ============================================================ */
305 .ripple {
306 position: absolute;
307 left: 50%;
308 top: 50%;
309 width: 120px;
310 height: 120px;
311 margin-top: -45px; /* = -60 (recenter) + 15 (y offset to button) */
312 margin-left: 70px; /* = -60 (recenter) + 130 (x offset to button) */
313 margin-right: 0;
314 margin-bottom: 0;
315 border: 2px solid rgba(255, 255, 255, 0.7);
316 border-radius: 50%;
317 opacity: 0;
318 pointer-events: none;
319 z-index: 6;
320 will-change: transform, opacity;
321 }
322
323 /* ============================================================
324 CURSOR — top layer, opacity 0 until Phase 2
325 ============================================================ */
326 .cursor {
327 position: absolute;
328 left: 0;
329 top: 0;
330 z-index: 999;
331 pointer-events: none;
332 opacity: 0;
333 filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
334 will-change: transform, opacity;
335 }
336
337 /* ============================================================
338 DEMO — appears from the collapse point in Phase 4
339 ============================================================ */
340 .demo {
341 position: absolute;
342 left: 50%;
343 top: 50%;
344 width: 800px;
345 height: 450px;
346 margin: -225px 0 0 -400px;
347 z-index: 10;
348 border-radius: 18px;
349 overflow: hidden;
350 background:
351 radial-gradient(ellipse at 22% 28%, rgba(139, 92, 246, 0.45), transparent 62%),
352 radial-gradient(ellipse at 78% 72%, rgba(124, 58, 237, 0.45), transparent 62%),
353 linear-gradient(135deg, rgba(20, 20, 30, 0.92) 0%, rgba(10, 10, 15, 0.95) 100%);
354 border: 1px solid var(--glass-border);
355 box-shadow:
356 0 50px 100px rgba(0, 0, 0, 0.5),
357 0 0 60px var(--brand-purple-glow),
358 inset 0 1px 0 rgba(255, 255, 255, 0.1);
359 display: flex;
360 align-items: center;
361 justify-content: center;
362 /* Initial scale: 0 / opacity: 0 set by GSAP — no CSS transform here to avoid
363 GSAP overwriting `translateX(-50%)` style centering. We use left/top + margin
364 for static centering instead. */
365 transform-style: preserve-3d;
366 will-change: transform, opacity;
367 }
368 .demo-play {
369 width: 118px;
370 height: 118px;
371 border-radius: 50%;
372 background: radial-gradient(circle at 35% 30%, #ffffff 0%, #ede9fe 60%, #c4b5fd 100%);
373 display: flex;
374 align-items: center;
375 justify-content: center;
376 box-shadow:
377 0 0 80px rgba(139, 92, 246, 0.7),
378 0 0 180px rgba(139, 92, 246, 0.4),
379 0 18px 40px rgba(0, 0, 0, 0.5),
380 inset 0 -4px 14px rgba(124, 58, 237, 0.3),
381 inset 0 3px 8px rgba(255, 255, 255, 0.95);
382 position: relative;
383 }
384 .demo-play::after {
385 /* outer ring — adds depth without breaking the seek-safe layout */
386 content: "";
387 position: absolute;
388 inset: -10px;
389 border-radius: 50%;
390 border: 1.5px solid rgba(139, 92, 246, 0.65);
391 box-shadow: 0 0 40px rgba(139, 92, 246, 0.5);
392 pointer-events: none;
393 }
394 .demo-play svg {
395 width: 44px;
396 height: 44px;
397 color: #0a0a0f;
398 transform: translateX(3px); /* optical center for play triangle */
399 filter: drop-shadow(0 1px 2px rgba(124, 58, 237, 0.5));
400 }
401 .demo-label {
402 position: absolute;
403 bottom: 28px;
404 left: 32px;
405 font-size: 22px;
406 font-weight: 700;
407 letter-spacing: 0.14em;
408 text-transform: uppercase;
409 background: linear-gradient(90deg, #ffffff 0%, #c4b5fd 50%, #8b5cf6 100%);
410 -webkit-background-clip: text;
411 background-clip: text;
412 color: transparent;
413 filter: drop-shadow(0 0 16px rgba(139, 92, 246, 0.6));
414 }
415
416 /* ============================================================
417 VIGNETTE
418 ============================================================ */
419 .vignette {
420 /* scene-05 vignette: transparent 40% → rgba(0,0,0,0.4) at edges */
421 position: absolute;
422 inset: 0;
423 background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.4) 100%);
424 pointer-events: none;
425 z-index: 50;
426 }
427
428 /* ============================================================
429 TITLE — centered via xPercent in GSAP (not CSS translateX),
430 so the GSAP `y` tween doesn't overwrite the centering transform.
431 Single line at 96 px; the brand wordmark ("HyperFrames") uses
432 the HyperFrames teal gradient + heavy weight, the tagline
433 ("renders any scene") stays in soft white-teal for contrast.
434 No filter — crisp edges, no blurred halo.
435 ============================================================ */
436 .title {
437 position: absolute;
438 top: 64px;
439 left: 50%;
440 white-space: nowrap;
441 text-align: center;
442 font-size: 96px;
443 font-weight: 700;
444 letter-spacing: -0.022em;
445 line-height: 1;
446 color: #ffffff;
447 text-shadow:
448 0 0 24px rgba(255, 255, 255, 0.3),
449 0 0 60px rgba(139, 92, 246, 0.45); /* scene-05 brand.purpleGlow */
450 z-index: 20;
451 will-change: transform, opacity;
452 }
453 .title-brand {
454 font-weight: 800;
455 /* Violet ramp matching scene-05's COLORS.brand.purple family */
456 background: linear-gradient(135deg, #ede9fe 0%, #c4b5fd 35%, #a78bfa 65%, #8b5cf6 100%);
457 -webkit-background-clip: text;
458 background-clip: text;
459 color: transparent;
460 filter: drop-shadow(0 0 28px rgba(139, 92, 246, 0.7))
461 drop-shadow(0 0 60px rgba(124, 58, 237, 0.5));
462 }
463
464 /* ============================================================
465 ENHANCEMENTS — stroke-draw entry, stagger reveal,
466 ambient background breathing.
467 The .draw-N classes preset stroke-dasharray + stroke-dashoffset
468 to N so the path starts fully hidden; GSAP animates offset → 0
469 to reveal it in time with the icon's 3D-flip entry.
470 ============================================================ */
471 .draw-50 {
472 stroke-dasharray: 50;
473 stroke-dashoffset: 50;
474 }
475 .draw-100 {
476 stroke-dasharray: 100;
477 stroke-dashoffset: 100;
478 }
479 .draw-150 {
480 stroke-dasharray: 150;
481 stroke-dashoffset: 150;
482 }
483 .draw-200 {
484 stroke-dasharray: 200;
485 stroke-dashoffset: 200;
486 }
487
488 .reveal-late {
489 opacity: 0;
490 }
491
492 /* Ambient violet breathing — sits over the dark overlay, under
493 the orbit ring; opacity driven by the scene-ticker onUpdate. */
494 .bg-pulse {
495 position: absolute;
496 inset: 0;
497 background: radial-gradient(ellipse at 50% 50%, rgba(139, 92, 246, 0.35), transparent 62%);
498 opacity: 0;
499 pointer-events: none;
500 z-index: 1;
501 will-change: opacity;
502 }
503 </style>
504 </head>
505 <body>
506 <div
507 id="root"
508 data-composition-id="main"
509 data-start="0"
510 data-duration="6.5"
511 data-width="1920"
512 data-height="1080"
513 >
514 <div
515 id="orbit-scene"
516 class="stage clip"
517 data-start="0"
518 data-duration="6.5"
519 data-track-index="1"
520 >
521 <div class="bg"></div>
522
523 <!-- ============================================================
524 ORBIT ICONS — 6 genre icons distributed evenly around 2π
525 ============================================================ -->
526 <div class="orbit-stage">
527 <!-- 1. HTML — angle brackets + diagonal slash -->
528 <div class="icon-pos icon-html">
529 <div class="icon-collapse">
530 <div class="icon-entry">
531 <div class="icon-tile">
532 <svg class="icon-svg" viewBox="0 0 24 24" fill="none">
533 <g class="html-brackets-g">
534 <path
535 class="draw-50"
536 d="M8 7 L3 12 L8 17"
537 stroke="url(#g-html)"
538 stroke-width="2.2"
539 stroke-linecap="round"
540 stroke-linejoin="round"
541 />
542 <path
543 class="draw-50"
544 d="M16 7 L21 12 L16 17"
545 stroke="url(#g-html)"
546 stroke-width="2.2"
547 stroke-linecap="round"
548 stroke-linejoin="round"
549 />
550 </g>
551 <line
552 class="reveal-late html-slash"
553 x1="14"
554 y1="5"
555 x2="10"
556 y2="19"
557 stroke="url(#g-html)"
558 stroke-width="2"
559 stroke-linecap="round"
560 />
561 <defs>
562 <linearGradient id="g-html" x1="0%" y1="0%" x2="100%" y2="100%">
563 <stop offset="0%" stop-color="#a78bfa" />
564 <stop offset="100%" stop-color="#8b5cf6" />
565 </linearGradient>
566 </defs>
567 </svg>
568 </div>
569 <span class="icon-label">HTML</span>
570 </div>
571 </div>
572 </div>
573
574 <!-- 2. CSS — frame with three color swatches -->
575 <div class="icon-pos icon-css">
576 <div class="icon-collapse">
577 <div class="icon-entry">
578 <div class="icon-tile">
579 <svg class="icon-svg" viewBox="0 0 24 24" fill="none">
580 <rect
581 class="draw-100"
582 x="3"
583 y="3"
584 width="18"
585 height="18"
586 rx="3"
587 stroke="url(#g-css)"
588 stroke-width="2"
589 fill="rgba(139, 92, 246, 0.18)"
590 />
591 <circle class="reveal-late css-dot-a" cx="8" cy="9" r="1.9" fill="#c4b5fd" />
592 <circle class="reveal-late css-dot-b" cx="16" cy="9" r="1.9" fill="#a78bfa" />
593 <circle class="reveal-late css-dot-c" cx="12" cy="16" r="1.9" fill="#7c3aed" />
594 <defs>
595 <linearGradient id="g-css" x1="0%" y1="0%" x2="100%" y2="100%">
596 <stop offset="0%" stop-color="#c4b5fd" />
597 <stop offset="100%" stop-color="#8b5cf6" />
598 </linearGradient>
599 </defs>
600 </svg>
601 </div>
602 <span class="icon-label">CSS</span>
603 </div>
604 </div>
605 </div>
606
607 <!-- 3. SVG — bezier curve with anchor points -->
608 <div class="icon-pos icon-vector">
609 <div class="icon-collapse">
610 <div class="icon-entry">
611 <div class="icon-tile">
612 <svg class="icon-svg" viewBox="0 0 24 24" fill="none">
613 <g class="svg-curve-g">
614 <path
615 class="draw-100"
616 d="M4 19 C 8 5, 16 19, 20 5"
617 stroke="url(#g-vec)"
618 stroke-width="2.5"
619 stroke-linecap="round"
620 stroke-linejoin="round"
621 fill="none"
622 />
623 <circle
624 class="reveal-late svg-anchor-a"
625 cx="4"
626 cy="19"
627 r="2.2"
628 stroke="url(#g-vec)"
629 stroke-width="1.5"
630 fill="rgba(139, 92, 246, 0.30)"
631 />
632 <circle
633 class="reveal-late svg-anchor-b"
634 cx="20"
635 cy="5"
636 r="2.2"
637 stroke="url(#g-vec)"
638 stroke-width="1.5"
639 fill="rgba(139, 92, 246, 0.30)"
640 />
641 </g>
642 <defs>
643 <linearGradient id="g-vec" x1="0%" y1="0%" x2="100%" y2="100%">
644 <stop offset="0%" stop-color="#c4b5fd" />
645 <stop offset="100%" stop-color="#7c3aed" />
646 </linearGradient>
647 </defs>
648 </svg>
649 </div>
650 <span class="icon-label">SVG</span>
651 </div>
652 </div>
653 </div>
654
655 <!-- 4. GSAP — sine wave timeline with playhead marker -->
656 <div class="icon-pos icon-gsap">
657 <div class="icon-collapse">
658 <div class="icon-entry">
659 <div class="icon-tile">
660 <svg class="icon-svg" viewBox="0 0 24 24" fill="none">
661 <line
662 x1="3"
663 y1="20"
664 x2="21"
665 y2="20"
666 stroke="url(#g-gsap)"
667 stroke-width="1.2"
668 stroke-linecap="round"
669 opacity="0.4"
670 />
671 <path
672 class="draw-100 gsap-wave-g"
673 d="M3 12 C 7 5, 9 5, 12 12 C 15 19, 17 19, 21 12"
674 stroke="url(#g-gsap)"
675 stroke-width="2.2"
676 stroke-linecap="round"
677 stroke-linejoin="round"
678 fill="none"
679 />
680 <circle class="reveal-late gsap-marker" cx="3" cy="12" r="2.2" fill="#c4b5fd" />
681 <defs>
682 <linearGradient id="g-gsap" x1="0%" y1="0%" x2="100%" y2="100%">
683 <stop offset="0%" stop-color="#a78bfa" />
684 <stop offset="100%" stop-color="#8b5cf6" />
685 </linearGradient>
686 </defs>
687 </svg>
688 </div>
689 <span class="icon-label">GSAP</span>
690 </div>
691 </div>
692 </div>
693
694 <!-- 5. 3D — isometric cube with inner facet lines -->
695 <div class="icon-pos icon-three">
696 <div class="icon-collapse">
697 <div class="icon-entry">
698 <div class="icon-tile">
699 <svg class="icon-svg" viewBox="0 0 24 24" fill="none">
700 <g class="three-cube-g">
701 <path
702 class="draw-100"
703 d="M12 3 L20 7 L20 17 L12 21 L4 17 L4 7 Z"
704 stroke="url(#g-three)"
705 stroke-width="2"
706 stroke-linecap="round"
707 stroke-linejoin="round"
708 fill="rgba(139, 92, 246, 0.24)"
709 />
710 <path
711 class="reveal-late three-facet"
712 d="M4 7 L12 11 L20 7 M12 11 L12 21"
713 stroke="url(#g-three)"
714 stroke-width="1.5"
715 stroke-linecap="round"
716 stroke-linejoin="round"
717 />
718 </g>
719 <defs>
720 <linearGradient id="g-three" x1="0%" y1="0%" x2="100%" y2="100%">
721 <stop offset="0%" stop-color="#c4b5fd" />
722 <stop offset="100%" stop-color="#7c3aed" />
723 </linearGradient>
724 </defs>
725 </svg>
726 </div>
727 <span class="icon-label">3D</span>
728 </div>
729 </div>
730 </div>
731
732 <!-- 6. LOTTIE — two diamond keyframes on a timeline -->
733 <div class="icon-pos icon-lottie">
734 <div class="icon-collapse">
735 <div class="icon-entry">
736 <div class="icon-tile">
737 <svg class="icon-svg" viewBox="0 0 24 24" fill="none">
738 <line
739 x1="3"
740 y1="12"
741 x2="21"
742 y2="12"
743 stroke="url(#g-lottie)"
744 stroke-width="1.2"
745 stroke-linecap="round"
746 opacity="0.4"
747 />
748 <path
749 class="draw-50 lottie-kf-a"
750 d="M8 8 L12 12 L8 16 L4 12 Z"
751 stroke="url(#g-lottie)"
752 stroke-width="2"
753 stroke-linecap="round"
754 stroke-linejoin="round"
755 fill="rgba(139, 92, 246, 0.28)"
756 />
757 <path
758 class="draw-50 lottie-kf-b"
759 d="M16 8 L20 12 L16 16 L12 12 Z"
760 stroke="url(#g-lottie)"
761 stroke-width="2"
762 stroke-linecap="round"
763 stroke-linejoin="round"
764 fill="rgba(139, 92, 246, 0.28)"
765 />
766 <defs>
767 <linearGradient id="g-lottie" x1="0%" y1="0%" x2="100%" y2="100%">
768 <stop offset="0%" stop-color="#a78bfa" />
769 <stop offset="100%" stop-color="#8b5cf6" />
770 </linearGradient>
771 </defs>
772 </svg>
773 </div>
774 <span class="icon-label">LOTTIE</span>
775 </div>
776 </div>
777 </div>
778 </div>
779
780 <!-- ============================================================
781 CENTER CTA — Drop a video link · Get free clips
782 ============================================================ -->
783 <div class="cta">
784 <svg
785 class="cta-link-icon"
786 viewBox="0 0 24 24"
787 fill="none"
788 stroke="currentColor"
789 stroke-width="2"
790 >
791 <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
792 <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
793 </svg>
794 <span class="cta-placeholder">Drop an HTML scene</span>
795 <div class="cta-button">Render</div>
796 </div>
797
798 <!-- ============================================================
799 RIPPLE — centered on the CTA's white button
800 ============================================================ -->
801 <div class="ripple"></div>
802
803 <!-- ============================================================
804 CURSOR — starts off-screen-right with opacity: 0.
805 White fill + black stroke matches the scene-05 cursor SVG.
806 ============================================================ -->
807 <div class="cursor">
808 <svg width="28" height="28" viewBox="0 0 24 24" fill="none">
809 <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="#fff" stroke="#000" stroke-width="1.5" />
810 </svg>
811 </div>
812
813 <!-- ============================================================
814 DEMO — appears from the collapse point in Phase 4
815 ============================================================ -->
816 <div class="demo">
817 <div class="demo-play">
818 <svg viewBox="0 0 24 24" fill="currentColor">
819 <path d="M8 5v14l11-7L8 5z" />
820 </svg>
821 </div>
822 <span class="demo-label">HyperFrames · MP4 ready</span>
823 </div>
824
825 <!-- ============================================================
826 TITLE
827 ============================================================ -->
828 <div class="title"><span class="title-brand">HyperFrames</span> renders any scene</div>
829
830 <div class="vignette"></div>
831 </div>
832 </div>
833
834 <script>
835 window.__timelines = window.__timelines || {};
836 const tl = gsap.timeline({ paused: true });
837
838 /* ============================================================
839 CONSTANTS
840 ============================================================ */
841 const W = 1920,
842 H = 1080;
843 const CENTER_X = W / 2;
844 const CENTER_Y = H / 2;
845
846 const RADIUS_X = 480;
847 const RADIUS_Y = 280;
848 const ORBIT_SPEED = 0.25; // radians per second
849
850 // Six HyperFrames technologies arranged around the central CTA —
851 // each represents something you'd actually drop into a
852 // HyperFrames composition (markup, styling, vector graphics,
853 // animation engines, 3D, or animated assets).
854 //
855 // drawLen — the .draw-N class present on this icon's outline.
856 // The stroke-draw tween pulls strokeDashoffset from N to 0.
857 // When an icon has multiple .draw-N paths sharing the same N
858 // (HTML's two brackets, LOTTIE's two diamonds), one selector
859 // targets both and they draw in unison.
860 // hasReveal — whether this icon has .reveal-late inner accents
861 // to fade in late via stagger. LOTTIE has none (both diamonds
862 // are stroke-drawn, no extra accent).
863 const ICONS = [
864 {
865 sel: ".icon-html",
866 initialAngle: (0 * Math.PI) / 3,
867 entryDelay: 0.0,
868 drawLen: 50,
869 hasReveal: true,
870 },
871 {
872 sel: ".icon-css",
873 initialAngle: (1 * Math.PI) / 3,
874 entryDelay: 0.1,
875 drawLen: 100,
876 hasReveal: true,
877 },
878 {
879 sel: ".icon-vector",
880 initialAngle: (2 * Math.PI) / 3,
881 entryDelay: 0.2,
882 drawLen: 100,
883 hasReveal: true,
884 },
885 {
886 sel: ".icon-gsap",
887 initialAngle: (3 * Math.PI) / 3,
888 entryDelay: 0.3,
889 drawLen: 100,
890 hasReveal: true,
891 },
892 {
893 sel: ".icon-three",
894 initialAngle: (4 * Math.PI) / 3,
895 entryDelay: 0.4,
896 drawLen: 100,
897 hasReveal: true,
898 },
899 {
900 sel: ".icon-lottie",
901 initialAngle: (5 * Math.PI) / 3,
902 entryDelay: 0.5,
903 drawLen: 50,
904 hasReveal: false,
905 },
906 ];
907
908 const ENTRY_DUR = 0.55;
909 const TITLE_AT = 0.2;
910 const CURSOR_AT = 1.5;
911 const CURSOR_MOVE = 0.5;
912 const CLICK_AT = 2.2;
913 const COLLAPSE_DUR = 0.85;
914 const DEMO_AT = 2.95;
915 const DEMO_DUR = 0.8;
916 const IDLE_START = 3.95;
917 const TOTAL = 6.5;
918
919 const COLLAPSE_EASE = gsap.parseEase("back.out(1.6)");
920 const ORBIT_END = DEMO_AT;
921
922 const CURSOR_START_X = W + 100;
923 const CURSOR_START_Y = H * 0.85;
924 const CURSOR_TARGET_X = CENTER_X + 130; // over the white button
925 const CURSOR_TARGET_Y = CENTER_Y + 15;
926
927 /* ============================================================
928 INITIAL STATE — embedded as tl.set calls at t=0 so the lint
929 can reason about them as timeline operations rather than
930 as overlapping zero-duration tweens.
931 ============================================================ */
932 tl.set(".icon-entry", { rotateX: 90, rotateY: -45, z: -100, scale: 0, opacity: 0 }, 0);
933 tl.set(".cursor", { x: CURSOR_START_X, y: CURSOR_START_Y, opacity: 0 }, 0);
934 tl.set(".ripple", { scale: 0.3, opacity: 0 }, 0);
935 // CTA: GSAP-driven centering + 3D entry start state.
936 tl.set(".cta", { xPercent: -50, yPercent: -50, rotateY: -90, scale: 0.7, opacity: 0 }, 0);
937 tl.set(".demo", { scale: 0, rotateY: 90, opacity: 0 }, 0);
938
939 /* ============================================================
940 TITLE — slides down and fades in early.
941 xPercent: -50 replaces CSS `transform: translateX(-50%)` so GSAP's
942 tween of `y` doesn't overwrite the centering transform. fromTo is
943 linter-exempt from the CSS-transform-conflict rule and bakes in the
944 start state explicitly.
945 ============================================================ */
946 tl.fromTo(
947 ".title",
948 { xPercent: -50, y: -30, opacity: 0 },
949 { xPercent: -50, y: 0, opacity: 1, duration: 0.6, ease: "back.out(1.4)" },
950 TITLE_AT,
951 );
952
953 /* ============================================================
954 CTA — 3D flip entry. rotateY -90° → 0° spins the card in from
955 the right; opacity + scale ease it forward at the same time.
956 xPercent/yPercent set at t=0 keep it centered while GSAP owns
957 the transform.
958 ============================================================ */
959 tl.to(
960 ".cta",
961 { rotateY: 0, scale: 1, opacity: 1, duration: 0.75, ease: "back.out(1.4)" },
962 0.3,
963 );
964
965 // Subtle Y-axis tilt while the CTA is idle waiting for the click.
966 // Half-cycle 0.9s, repeats yoyo until just before CLICK_AT.
967 tl.to(".cta", { rotateY: 5, duration: 0.9, ease: "sine.inOut", yoyo: true, repeat: 1 }, 1.05);
968
969 /* ============================================================
970 PHASE 1: 3D flip entry per icon
971 ============================================================ */
972 ICONS.forEach(({ sel, entryDelay }) => {
973 tl.fromTo(
974 `${sel} .icon-entry`,
975 { rotateX: 90, rotateY: -45, z: -100, scale: 0, opacity: 0 },
976 {
977 rotateX: 0,
978 rotateY: 0,
979 z: 0,
980 scale: 1,
981 opacity: 1,
982 duration: ENTRY_DUR,
983 ease: "back.out(1.4)",
984 },
985 entryDelay,
986 );
987 });
988
989 /* ============================================================
990 PHASE 1b: SVG stroke-draw per icon
991 Each icon has one .draw-N class on its outline path. CSS
992 preset the dasharray/dashoffset to N (path hidden); this
993 tween pulls the offset to 0, drawing the path along its
994 length. power2.out front-loads the reveal so the stroke
995 is mostly visible by the time the 3D flip settles.
996 ============================================================ */
997 ICONS.forEach(({ sel, entryDelay, drawLen }) => {
998 tl.fromTo(
999 `${sel} .draw-${drawLen}`,
1000 { strokeDashoffset: drawLen },
1001 { strokeDashoffset: 0, duration: ENTRY_DUR + 0.1, ease: "power2.out" },
1002 entryDelay,
1003 );
1004 });
1005
1006 /* ============================================================
1007 PHASE 1c: Stagger reveal for inner accent elements
1008 (music notes, gaming d-pad, vlog play triangle, podcast
1009 base lines). Preset to opacity 0 via .reveal-late; this
1010 tween fades siblings in with a 0.10s stagger so the icon
1011 "completes itself" after its outline is drawn.
1012 Skipped for education/sports — they have no inner accents.
1013 ============================================================ */
1014 ICONS.forEach(({ sel, entryDelay, hasReveal }) => {
1015 if (!hasReveal) return;
1016 tl.fromTo(
1017 `${sel} .reveal-late`,
1018 { opacity: 0 },
1019 { opacity: 1, duration: 0.3, ease: "power2.out", stagger: 0.1 },
1020 entryDelay + 0.3,
1021 );
1022 });
1023
1024 /* ============================================================
1025 PHASE 1 + 3: Master orbit + collapse engine
1026 Single onUpdate spans from t=0 to ORBIT_END. Reads tl.time()
1027 and computes:
1028 - orbit angle per icon (relative to its entryDelay)
1029 - collapse driver (eased fraction of CLICK_AT → CLICK_AT+COLLAPSE_DUR)
1030 Writes x/y to .icon-pos and scale/opacity to .icon-collapse.
1031 ============================================================ */
1032 tl.to(
1033 { tick: 0 },
1034 {
1035 tick: 1,
1036 duration: ORBIT_END,
1037 ease: "none",
1038 onUpdate: () => {
1039 const t = tl.time();
1040 const collapseLinear = Math.max(0, Math.min(1, (t - CLICK_AT) / COLLAPSE_DUR));
1041 const collapseEased = COLLAPSE_EASE(collapseLinear);
1042 const radiusFactor = 1 - collapseEased;
1043 const collapseScale = 1 - collapseEased * 0.5;
1044
1045 // Two-segment opacity envelope: 1 at 0, 0.5 at 0.8, 0 at 1
1046 const o = collapseEased;
1047 const collapseOpacity = o < 0.8 ? 1 - o * 0.625 : (0.5 * (1 - o)) / 0.2;
1048
1049 ICONS.forEach(({ sel, initialAngle, entryDelay }, i) => {
1050 const localT = Math.max(0, t - entryDelay);
1051 const angle = initialAngle + localT * ORBIT_SPEED;
1052 const x = Math.cos(angle) * RADIUS_X * radiusFactor;
1053 const y = Math.sin(angle) * RADIUS_Y * radiusFactor;
1054
1055 // Per-icon idle wobble — phased per index so the ring
1056 // doesn't pulse in sync. Damped by radiusFactor so the
1057 // wobble relaxes as icons collapse to the center.
1058 const floatY = Math.sin(t * 1.4 + i * 1.1) * 6 * radiusFactor;
1059 const floatRot = Math.sin(t * 1.1 + i * 1.7) * 2.5 * radiusFactor;
1060
1061 gsap.set(`${sel}.icon-pos`, { x, y: y + floatY });
1062 gsap.set(`${sel} .icon-collapse`, {
1063 scale: collapseScale,
1064 opacity: collapseOpacity,
1065 rotation: floatRot,
1066 });
1067 });
1068 },
1069 },
1070 0,
1071 );
1072
1073 /* ============================================================
1074 PHASE 2: Cursor enters, slides to CTA, clicks
1075 ============================================================ */
1076 // Fade in
1077 tl.to(".cursor", { opacity: 1, duration: 0.1, ease: "none" }, CURSOR_AT);
1078
1079 // Slide to CTA button
1080 tl.to(
1081 ".cursor",
1082 { x: CURSOR_TARGET_X, y: CURSOR_TARGET_Y, duration: CURSOR_MOVE, ease: "back.out(1.3)" },
1083 CURSOR_AT,
1084 );
1085
1086 // Click depression — cursor + button compress, then recover
1087 tl.to(".cursor", { scale: 0.85, duration: 0.08, ease: "power2.out" }, CLICK_AT);
1088 tl.to(".cursor", { scale: 1, duration: 0.18, ease: "back.out(1.6)" }, CLICK_AT + 0.08);
1089 tl.to(".cta-button", { scale: 0.95, duration: 0.08, ease: "power2.out" }, CLICK_AT);
1090 tl.to(".cta-button", { scale: 1, duration: 0.18, ease: "back.out(1.6)" }, CLICK_AT + 0.08);
1091
1092 // Button glow pulse during click
1093 tl.fromTo(
1094 ".cta-button",
1095 { boxShadow: "0 0 0 rgba(255,255,255,0)" },
1096 { boxShadow: "0 0 40px rgba(255,255,255,0.7)", duration: 0.2, ease: "power2.out" },
1097 CLICK_AT,
1098 );
1099 tl.to(
1100 ".cta-button",
1101 { boxShadow: "0 0 0 rgba(255,255,255,0)", duration: 0.4, ease: "power2.in" },
1102 CLICK_AT + 0.2,
1103 );
1104
1105 // Ripple — attack-decay envelope via keyframes
1106 tl.to(
1107 ".ripple",
1108 {
1109 duration: 0.7,
1110 keyframes: {
1111 "0%": { scale: 0.3, opacity: 0 },
1112 "20%": { opacity: 0.7 },
1113 "100%": { scale: 5.0, opacity: 0 },
1114 easeEach: "power2.out",
1115 },
1116 },
1117 CLICK_AT,
1118 );
1119
1120 /* ============================================================
1121 PHASE 4: Demo springs out of the collapse point
1122 ============================================================ */
1123 tl.fromTo(
1124 ".demo",
1125 { scale: 0, rotateY: 90, opacity: 0 },
1126 { scale: 1, rotateY: 0, opacity: 1, duration: DEMO_DUR, ease: "back.out(1.6)" },
1127 DEMO_AT,
1128 );
1129
1130 // CTA fades out as the demo emerges — the click's "result" replaces the action target.
1131 tl.to(".cta", { opacity: 0, duration: 0.3, ease: "power2.in" }, DEMO_AT - 0.05);
1132
1133 // Cursor exits with the CTA
1134 tl.to(".cursor", { opacity: 0, duration: 0.3, ease: "power2.in" }, DEMO_AT);
1135
1136 /* ============================================================
1137 PHASE 5: Demo floats with finite-yoyo breathing
1138 ============================================================ */
1139 const HALF_CYCLE = 1.1;
1140 const remaining = TOTAL - IDLE_START;
1141 const halfCycles = Math.max(0, Math.floor(remaining / HALF_CYCLE) - 1);
1142
1143 tl.fromTo(
1144 ".demo",
1145 { y: 0, rotation: 0, rotateY: 0, rotateX: 0 },
1146 {
1147 y: -8,
1148 rotation: 1,
1149 rotateY: 6,
1150 rotateX: -2,
1151 duration: HALF_CYCLE,
1152 ease: "sine.inOut",
1153 yoyo: true,
1154 repeat: halfCycles,
1155 },
1156 IDLE_START,
1157 );
1158
1159 /* ============================================================
1160 CONTINUOUS INTERNAL SVG ENRICHMENT
1161 Shared scene-ticker — one onUpdate writes all per-SVG motions.
1162 Gated per-icon by its entryDelay so a hidden icon doesn't pay
1163 the cost (still cheap, but cleaner intent).
1164 ============================================================ */
1165 const htmlBrackets = document.querySelector(".html-brackets-g");
1166 const cssDotA = document.querySelector(".css-dot-a");
1167 const cssDotB = document.querySelector(".css-dot-b");
1168 const cssDotC = document.querySelector(".css-dot-c");
1169 const svgCurve = document.querySelector(".svg-curve-g");
1170 const gsapMarker = document.querySelector(".gsap-marker");
1171 const gsapWave = document.querySelector(".gsap-wave-g");
1172 const threeCube = document.querySelector(".three-cube-g");
1173 const lottieKfA = document.querySelector(".lottie-kf-a");
1174 const lottieKfB = document.querySelector(".lottie-kf-b");
1175
1176 tl.to(
1177 { tick2: 0 },
1178 {
1179 tick2: 1,
1180 duration: TOTAL,
1181 ease: "none",
1182 onUpdate: () => {
1183 const t = tl.time();
1184
1185 // HTML brackets — tiny horizontal sway, like a typing cursor
1186 const htmlT = t - 0.0;
1187 if (htmlT > 0) {
1188 gsap.set(htmlBrackets, { x: Math.sin(htmlT * 3.5) * 0.6 });
1189 }
1190
1191 // CSS swatches — three dots scale-pulse with phase offsets
1192 const cssT = t - 0.1;
1193 if (cssT > 0) {
1194 gsap.set(cssDotA, { scale: 0.85 + Math.sin(cssT * 5.5) * 0.18 });
1195 gsap.set(cssDotB, { scale: 0.85 + Math.sin(cssT * 5.5 + 1.4) * 0.18 });
1196 gsap.set(cssDotC, { scale: 0.85 + Math.sin(cssT * 5.5 + 2.8) * 0.18 });
1197 }
1198
1199 // SVG bezier — whole curve group gently rotates back and forth
1200 const vecT = t - 0.2;
1201 if (vecT > 0) {
1202 gsap.set(svgCurve, { rotation: Math.sin(vecT * 2.2) * 5 });
1203 }
1204
1205 // GSAP timeline — playhead marker slides along the wave +
1206 // the wave itself flexes vertically (amplitude pulse).
1207 const gsapT = t - 0.3;
1208 if (gsapT > 0) {
1209 // marker traverses 0 → 18 viewBox units left-to-right
1210 const markerX = (Math.sin(gsapT * 1.4 - Math.PI / 2) + 1) * 9;
1211 gsap.set(gsapMarker, { x: markerX });
1212 gsap.set(gsapWave, { scaleY: 1 + Math.sin(gsapT * 2.5) * 0.12 });
1213 }
1214
1215 // 3D cube — slow continuous tumble (replaces sports-ball spin)
1216 const threeT = t - 0.4;
1217 if (threeT > 0) {
1218 gsap.set(threeCube, { rotation: threeT * 30 });
1219 }
1220
1221 // LOTTIE keyframes — two diamonds scale-pulse out of phase
1222 const lottieT = t - 0.5;
1223 if (lottieT > 0) {
1224 gsap.set(lottieKfA, { scale: 1 + Math.sin(lottieT * 2.8) * 0.12 });
1225 gsap.set(lottieKfB, { scale: 1 + Math.sin(lottieT * 2.8 + 1.6) * 0.12 });
1226 }
1227
1228 // ====== ENHANCEMENT: per-icon tile glow pulse ======
1229 // The tile's violet halo breathes throughout the orbit;
1230 // each icon is phased by its index so the ring shimmers
1231 // rather than pulses in unison. Only while the icons are
1232 // still on screen (before they finish collapsing).
1233 // Colors mirror scene-05: COLORS.brand.purple (#8b5cf6) +
1234 // a softer rgba violet halo.
1235 if (t < DEMO_AT) {
1236 ICONS.forEach(({ sel }, i) => {
1237 const glow = 32 + Math.sin(t * 1.3 + i * 0.95) * 16;
1238 gsap.set(`${sel} .icon-tile`, {
1239 boxShadow:
1240 "0 15px 40px rgba(0, 0, 0, 0.35), " +
1241 "0 0 " +
1242 glow +
1243 "px rgba(139, 92, 246, 0.65), " +
1244 "0 0 " +
1245 glow * 2 +
1246 "px rgba(124, 58, 237, 0.28), " +
1247 "inset 0 1px 0 rgba(255, 255, 255, 0.1)",
1248 });
1249 });
1250 }
1251
1252 // ====== ENHANCEMENT: CTA idle breathing (pre-click) ======
1253 // Soft violet aura pulses around the CTA card to invite
1254 // the click. Frozen at click time; the CTA fades out at
1255 // DEMO_AT - 0.05 so the static end-state never shows.
1256 if (t < CLICK_AT) {
1257 const ctaGlow = 70 + Math.sin(t * 2.6) * 30;
1258 gsap.set(".cta", {
1259 boxShadow:
1260 "0 40px 80px rgba(0, 0, 0, 0.5), " +
1261 "0 0 " +
1262 ctaGlow +
1263 "px rgba(139, 92, 246, 0.7), " +
1264 "0 0 " +
1265 ctaGlow * 2 +
1266 "px rgba(124, 58, 237, 0.32), " +
1267 "inset 0 1px 0 rgba(255, 255, 255, 0.1)",
1268 });
1269 }
1270
1271 // ====== ENHANCEMENT: demo glow pulse (post-emerge) ======
1272 // Once the demo card has sprung out, its violet aura
1273 // breathes in time with its yoyo float, giving the
1274 // "result" element a living idle state.
1275 if (t > DEMO_AT) {
1276 const demoT = t - DEMO_AT;
1277 const demoGlow = 90 + Math.sin(demoT * 1.9) * 36;
1278 gsap.set(".demo", {
1279 boxShadow:
1280 "0 50px 100px rgba(0, 0, 0, 0.5), " +
1281 "0 0 " +
1282 demoGlow +
1283 "px rgba(139, 92, 246, 0.7), " +
1284 "0 0 " +
1285 demoGlow +
1286 "px rgba(124, 58, 237, 0.42), " +
1287 "inset 0 1px 0 rgba(255, 255, 255, 0.1)",
1288 });
1289 }
1290 },
1291 },
1292 0,
1293 );
1294
1295 window.__timelines["main"] = tl;
1296 </script>
1297 </body>
1298</html>