Setting the file. One moment. Problem Mockup Overwhelm · Hyperframes Animation · heygen-com/hyperframes · Skills DocsBlueprints Index
(opens in a new tab)
examples/problem-mockup-overwhelm.html
HTML·1,374 lines·43 KB
8 <!--
9 Blueprint: problem-mockup-overwhelm (HyperFrames preview)
10 HyperFrames-native version of the scene-02-mockup-morph-overwhelm concept.
11
12 Choreography (4 phases, 6 seconds total):
13 0.00 – 0.55s Three HyperFrames workflow mockups spring-in (left → right → center)
14 Check panel left · render queue right · preview timeline center
15 0.33 – 0.95s Nine scattered HyperFrames tool icons stagger in around the cluster
16 3.20 – 3.80s MORPH:
17 center mockup compositor-scale down to the avatar footprint
18 borderRadius repaint (42px → 50% = reads as circle)
19 content fades out at 0-40% of morph
20 non-center mockups + icons exit concurrently
21 avatar pop (scale 0 → 1) starts at morph trigger
22 avatar layer opacity fades in at 50% of morph
23 at 85-100% of morph, mockup-center fades to 0 → avatar visible underneath
24 3.53 – 4.40s Eight task bubbles stagger-enter in a radial pattern around the avatar
25 4.40 – 6.00s Idle: bubble micro-float + avatar orbit dots + avatar breath
26 paint-only tween. The "rect → circle" effect is completed by the final 0.51s-onward
27 opacity hand-off to the avatar element rendered underneath at z-index 20.
28 - Both mockup layer and avatar layer stay in DOM (no `{showX && <Y />}` conditional render);
29 opacity gates visibility — required because HyperFrames seek can move time backwards.
30 - All `Math.sin(frame * ...)` continuous motion replaced by:
31 mockup float / bubble float / orbit dots → shared onUpdate scene-ticker reading tl.time()
32 avatar breath → finite sine.inOut yoyo (multiplicative onto pop scale)
33 - The animated dark-frosted-glass blob background was replaced with a static
34 gradient + frosted overlay for renderer determinism and file size.
35 - Avatar video source replaced with a CSS gradient circle (no asset required).
36 To use a real video, swap the inner .avatar-disc for a <video muted playsinline>.
37 - Bubbles are generated via JS `forEach` at script load — positions pre-baked in
38 inline `left`/`top`; GSAP tweens only `scale`/`opacity`/`x`/`y`.
39
40 Spring → ease mapping (from skill SKILL.md):
41 spring(stiffness:70, damping:14) → back.out(1.4) (mockup entry)
42 spring(stiffness:180, damping:14) → back.out(1.6) (icon entry)
43 spring(stiffness:80, damping:18) → power3.out (morph driver)
44 spring(stiffness:120, damping:14) → back.out(1.4) (avatar pop)
45 spring(stiffness:180, damping:12) → back.out(1.4) (bubble pop)
46 -->
47
48 <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
49
50 <style>
51 :root {
52 --bg-base: #0a0a0f;
53 --text-primary: #ffffff;
54 --surface-dark: #000000;
55 --surface-mid: #16213e;
56 --surface-light: rgba(255, 255, 255, 0.98);
57 --line-soft: rgba(139, 92, 246, 0.5);
58 --bubble-bg: rgba(255, 255, 255, 0.95);
59 --bubble-border: rgba(139, 92, 246, 0.5);
60 --bubble-text: #1f2937;
61 --brand-cyan: #06b6d4;
62 --brand-teal: #14b8a6;
63 --brand-green: #00f2ea;
64 --brand-blue: #0ea5e9;
65 --brand-purple: #8b5cf6;
66 --brand-pink: #ec4899;
67 --brand-orange: #fb923c;
68 --brand-ink: #0f0f0f;
69 --accent-lime: #facc15;
70 --accent-mint: #ffffff;
71 --warn-amber: #ffc107;
72 --tt-cyan: #00f2ea;
73 }
74 * {
75 margin: 0;
76 padding: 0;
77 box-sizing: border-box;
78 }
79 html,
80 body {
81 margin: 0;
82 width: 1920px;
83 height: 1080px;
84 overflow: hidden;
85 background: var(--bg-base);
86 font-family:
87 "Inter",
88 system-ui,
89 -apple-system,
90 sans-serif;
91 color: var(--text-primary);
92 }
93
94 /* ============================================================
95 BACKGROUND (dark frosted glass, matching TSX reference)
96 ============================================================ */
97 .bg {
98 position: absolute;
99 inset: 0;
100 overflow: hidden;
101 background: radial-gradient(
102 ellipse 120% 100% at 50% 20%,
103 rgba(30, 30, 45, 1) 0%,
104 rgba(18, 18, 28, 1) 40%,
105 rgba(10, 10, 15, 1) 100%
106 );
107 }
108 .bg::before {
109 content: "";
110 position: absolute;
111 inset: 0;
112 background: rgba(15, 15, 25, 0.4);
113 backdrop-filter: blur(80px) saturate(180%);
114 -webkit-backdrop-filter: blur(80px) saturate(180%);
115 }
116 .bg::after {
117 content: "";
118 position: absolute;
119 inset: 0;
120 background:
121 linear-gradient(
122 180deg,
123 rgba(255, 255, 255, 0.08) 0%,
124 rgba(255, 255, 255, 0.02) 5%,
125 transparent 15%,
126 transparent 100%
127 ),
128 linear-gradient(0deg, rgba(139, 92, 246, 0.05) 0%, transparent 20%, transparent 100%);
129 }
130 .bg-frost {
131 position: absolute;
132 inset: 0;
133 opacity: 0.5;
134 filter: blur(120px);
135 background:
136 radial-gradient(
137 circle 550px at 30% 35%,
138 rgba(139, 92, 246, 0.7) 0%,
139 rgba(139, 92, 246, 0.3) 40%,
140 transparent 70%
141 ),
142 radial-gradient(
143 circle 500px at 70% 65%,
144 rgba(6, 182, 212, 0.6) 0%,
145 rgba(6, 182, 212, 0.25) 40%,
146 transparent 70%
147 ),
148 radial-gradient(
149 circle 400px at 50% 80%,
150 rgba(251, 146, 60, 0.5) 0%,
151 rgba(251, 146, 60, 0.2) 40%,
152 transparent 70%
153 ),
154 radial-gradient(
155 circle 350px at 20% 60%,
156 rgba(236, 72, 153, 0.4) 0%,
157 rgba(236, 72, 153, 0.15) 40%,
158 transparent 70%
159 );
160 }
161 .vignette {
162 position: absolute;
163 inset: 0;
164 background-image:
165 radial-gradient(
166 ellipse 80% 80% at 50% 50%,
167 transparent 0%,
168 transparent 50%,
169 rgba(0, 0, 0, 0.3) 100%
170 ),
171 url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"),
172 repeating-linear-gradient(
173 0deg,
174 transparent,
175 transparent 2px,
176 rgba(255, 255, 255, 0.00075) 2px,
177 rgba(255, 255, 255, 0.00075) 4px
178 );
179 pointer-events: none;
180 z-index: 400;
181 }
182
183 /* ============================================================
184 MOCKUP CLUSTER (Phase 1-2)
185 ============================================================ */
186 .mockup-cluster {
187 position: absolute;
188 inset: 0;
189 perspective: 1400px;
190 transform-style: preserve-3d;
191 }
192
193 /* Fitness-style phone shells */
194 .mockup {
195 position: absolute;
196 left: 50%;
197 top: 50%;
198 width: 320px;
199 height: 650px;
200 border-radius: 42px;
201 background: transparent;
202 transform-origin: center center;
203 transform-style: preserve-3d;
204 will-change: transform, opacity;
205 }
206 .mockup-left {
207 z-index: 10;
208 }
209 .mockup-right {
210 z-index: 12;
211 }
212 .mockup-center {
213 z-index: 25;
214 background: var(--surface-dark);
215 box-shadow:
216 0 24px 70px rgba(0, 0, 0, 0.4),
217 0 0 40px rgba(0, 242, 234, 0.15);
218 overflow: hidden;
219 }
220
221 .phone-shell {
222 position: absolute;
223 inset: 0;
224 border-radius: 42px;
225 background: #000000;
226 padding: 8px;
227 box-shadow:
228 0 18px 60px rgba(0, 0, 0, 0.3),
229 0 4px 16px rgba(0, 0, 0, 0.18),
230 inset 0 0 0 3px #333333;
231 overflow: hidden;
232 will-change: opacity;
233 }
234
235 .phone-notch {
236 position: absolute;
237 top: 8px;
238 left: 50%;
239 width: 100px;
240 height: 26px;
241 border-radius: 0 0 18px 18px;
242 background: #000000;
243 transform: translateX(-50%);
244 z-index: 5;
245 }
246
247 .phone-screen {
248 position: relative;
249 width: 100%;
250 height: 100%;
251 border-radius: 35px;
252 overflow: hidden;
253 display: flex;
254 flex-direction: column;
255 }
256
257 .mockup-left .phone-screen,
258 .mockup-right .phone-screen {
259 background: rgba(255, 255, 255, 0.98);
260 padding-top: 30px;
261 }
262
263 .mockup-center .phone-screen {
264 background: #000000;
265 }
266
267 .mockup-content {
268 flex: 1;
269 display: flex;
270 flex-direction: column;
271 min-height: 0;
272 will-change: opacity;
273 }
274
275 /* --- HyperFrames Check (left) --- */
276 .yt-header {
277 padding: 14px 18px;
278 background: #f9f9f9;
279 border-bottom: 1px solid #e5e5e5;
280 display: flex;
281 align-items: center;
282 gap: 10px;
283 font: 600 16px/1 Inter;
284 color: #0f0f0f;
285 }
286 .yt-header svg .hf-logo-left {
287 fill: url(#hf-mini-grad-left);
288 }
289 .yt-body {
290 padding: 18px;
291 color: #0f0f0f;
292 }
293 .yt-body h3 {
294 font: 700 17px/1.2 Inter;
295 margin-bottom: 12px;
296 }
297 .yt-thumb {
298 height: 130px;
299 background: linear-gradient(135deg, #1a1a2e, #16213e);
300 border-radius: 10px;
301 margin-bottom: 14px;
302 padding: 18px;
303 display: grid;
304 gap: 8px;
305 align-content: center;
306 }
307 .code-line {
308 height: 10px;
309 border-radius: 5px;
310 background: rgba(255, 255, 255, 0.24);
311 }
312 .code-line:nth-child(1) {
313 width: 74%;
314 background: #ff0000;
315 }
316 .code-line:nth-child(2) {
317 width: 58%;
318 }
319 .code-line:nth-child(3) {
320 width: 86%;
321 }
322 .code-line:nth-child(4) {
323 width: 46%;
324 background: rgba(255, 255, 255, 0.3);
325 }
326 .code-line:nth-child(5) {
327 width: 68%;
328 }
329 .code-line:nth-child(6) {
330 width: 52%;
331 background: rgba(255, 255, 255, 0.28);
332 }
333 .yt-progress-fill {
334 width: 72%;
335 height: 100%;
336 background: #ff0000;
337 border-radius: 3px;
338 }
339 .yt-progress-row {
340 display: flex;
341 justify-content: space-between;
342 font: 500 12px/1 Inter;
343 color: #606060;
344 margin-bottom: 6px;
345 }
346 .yt-progress-track {
347 height: 5px;
348 background: #e5e5e5;
349 border-radius: 3px;
350 overflow: hidden;
351 }
352 .yt-task-label {
353 font: 500 12px/1 Inter;
354 color: #606060;
355 margin-top: 16px;
356 margin-bottom: 8px;
357 }
358 .yt-task {
359 display: flex;
360 align-items: center;
361 gap: 8px;
362 margin-bottom: 6px;
363 font: 400 12px/1.2 Inter;
364 color: #606060;
365 }
366 .yt-task::before {
367 content: "";
368 width: 13px;
369 height: 13px;
370 border: 2px solid #909090;
371 border-radius: 50%;
372 background: transparent;
373 flex-shrink: 0;
374 }
375 .yt-footer {
376 margin-top: auto;
377 padding: 12px 18px;
378 background: #fff4e5;
379 border-top: 1px solid #ffe0b2;
380 font: 500 12px/1 Inter;
381 color: #e65100;
382 display: flex;
383 align-items: center;
384 gap: 8px;
385 }
386
387 /* --- HyperFrames Preview (center) — content fades during morph --- */
388 .tt-statusbar {
389 padding: 10px 14px;
390 display: flex;
391 justify-content: space-between;
392 font: 500 12px/1 Inter;
393 color: var(--accent-mint);
394 }
395 .tt-preview {
396 flex: 1;
397 position: relative;
398 background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
399 }
400 .tt-overlay {
401 position: absolute;
402 top: 14px;
403 left: 14px;
404 right: 14px;
405 background: rgba(0, 0, 0, 0.6);
406 border-radius: 10px;
407 padding: 10px 12px;
408 border: none;
409 backdrop-filter: blur(10px);
410 -webkit-backdrop-filter: blur(10px);
411 }
412 .tt-overlay-title {
413 font: 500 12px/1 Inter;
414 color: white;
415 margin-bottom: 4px;
416 }
417 .tt-overlay-sub {
418 font: 400 10px/1.2 Inter;
419 color: rgba(216, 254, 255, 0.72);
420 }
421 .tt-play {
422 position: absolute;
423 top: 50%;
424 left: 50%;
425 width: 60px;
426 height: 60px;
427 margin: -30px 0 0 -30px;
428 border-radius: 50%;
429 background: rgba(255, 255, 255, 0.3);
430 box-shadow: none;
431 display: flex;
432 align-items: center;
433 justify-content: center;
434 font-size: 22px;
435 color: white;
436 }
437 .tt-side {
438 position: absolute;
439 right: 12px;
440 bottom: 70px;
441 display: flex;
442 flex-direction: column;
443 gap: 14px;
444 font: 500 10px/1 Inter;
445 color: white;
446 text-align: center;
447 }
448 .tt-side div {
449 display: flex;
450 flex-direction: column;
451 gap: 3px;
452 }
453 .tt-side span {
454 font-size: 18px;
455 font-weight: 800;
456 color: #ffffff;
457 }
458 .tt-timeline {
459 padding: 12px;
460 background: #111111;
461 }
462 .tt-strip {
463 display: flex;
464 gap: 4px;
465 margin-bottom: 8px;
466 }
467 .tt-clip {
468 flex: 1;
469 height: 32px;
470 border-radius: 4px;
471 background: #333333;
472 opacity: 0.6;
473 }
474 .tt-clip.active {
475 background: var(--tt-cyan);
476 opacity: 1;
477 }
478 .tt-note {
479 font: 500 10px/1 Inter;
480 color: #888888;
481 text-align: center;
482 }
483 .tt-tabs {
484 padding: 10px 18px;
485 background: #000000;
486 display: flex;
487 justify-content: space-around;
488 font: 500 11px/1 Inter;
489 color: white;
490 }
491
492 /* --- HyperFrames Render (right) --- */
493 .ig-header {
494 padding: 12px 16px;
495 border-bottom: 1px solid #efefef;
496 display: flex;
497 align-items: center;
498 gap: 10px;
499 font: 600 15px/1 Inter;
500 color: #262626;
501 }
502 .ig-body {
503 padding: 14px;
504 flex: 1;
505 color: #262626;
506 }
507 .ig-body h3 {
508 font: 500 13px/1 Inter;
509 margin-bottom: 12px;
510 }
511 .ig-warn {
512 background: #fff3cd;
513 border: 1px solid #ffc107;
514 border-radius: 10px;
515 padding: 10px 12px;
516 margin-bottom: 14px;
517 }
518 .ig-warn-title {
519 font: 500 12px/1.1 Inter;
520 color: #856404;
521 }
522 .ig-warn-sub {
523 font: 400 11px/1.2 Inter;
524 color: #856404;
525 margin-top: 4px;
526 }
527 .ig-task-label {
528 font: 500 13px/1 Inter;
529 color: #262626;
530 margin-bottom: 8px;
531 }
532 .ig-task {
533 display: flex;
534 align-items: center;
535 gap: 8px;
536 margin-bottom: 6px;
537 font: 400 12px/1.2 Inter;
538 color: #666666;
539 }
540 .ig-task::before {
541 content: "";
542 width: 12px;
543 height: 12px;
544 border: 2px solid #c7c7c7;
545 border-radius: 50%;
546 background: transparent;
547 flex-shrink: 0;
548 }
549 .ig-time {
550 margin-top: 14px;
551 background: #ffebee;
552 border-radius: 8px;
553 padding: 10px 12px;
554 font: 500 12px/1 Inter;
555 color: #c62828;
556 }
557 .ig-footer {
558 margin-top: auto;
559 padding: 12px 14px;
560 border-top: 1px solid #efefef;
561 background: #fafafa;
562 }
563 .ig-share {
564 background: linear-gradient(90deg, #833ab4, #fd1d1d, #fcaf45);
565 border-radius: 10px;
566 padding: 11px 0;
567 text-align: center;
568 font: 600 13px/1 Inter;
569 color: #ffffff;
570 }
571
572 /* ============================================================
573 PLATFORM ICONS — scattered, positions pre-baked in CSS
574 ============================================================ */
575 .platform-icon {
576 position: absolute;
577 border-radius: 22%;
578 background: rgba(255, 255, 255, 0.98);
579 display: flex;
580 align-items: center;
581 justify-content: center;
582 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
583 border: none;
584 z-index: 30;
585 will-change: transform, opacity;
586 }
587 .platform-icon svg {
588 width: 62%;
589 height: 62%;
590 }
591 .platform-icon .icon-type {
592 font: 800 25px/1 Inter;
593 letter-spacing: 0;
594 color: #063238;
595 }
596 .platform-icon .icon-small {
597 font-size: 20px;
598 }
599
600 /* Nine icon positions — taken from the the source SCATTERED_ICONS array */
601 .pi-youtube {
602 left: calc(22% + 100px);
603 top: calc(20% - 179px);
604 width: 102px;
605 height: 102px;
606 }
607 .pi-google {
608 left: calc(60% + 16px);
609 top: calc(15% - 113px);
610 width: 96px;
611 height: 96px;
612 }
613 .pi-instagram {
614 left: calc(88% - 73px);
615 top: calc(25% - 103px);
616 width: 102px;
617 height: 102px;
618 }
619 .pi-tiktok {
620 left: 10%;
621 top: calc(48% - 101px);
622 width: 90px;
623 height: 90px;
624 }
625 .pi-tripadvisor {
626 left: calc(94% - 89px);
627 top: calc(55% - 45px);
628 width: 84px;
629 height: 84px;
630 }
631 .pi-facebook {
632 left: 15%;
633 top: 75%;
634 width: 90px;
635 height: 90px;
636 }
637 .pi-twitter {
638 left: 42%;
639 top: 85%;
640 width: 84px;
641 height: 84px;
642 }
643 .pi-linkedin {
644 left: 70%;
645 top: 85%;
646 width: 78px;
647 height: 78px;
648 }
649 .pi-yelp {
650 left: calc(92% - 165px);
651 top: calc(72% + 4px);
652 width: 90px;
653 height: 90px;
654 }
655
656 /* ============================================================
657 AVATAR + BUBBLES (Phase 3-4)
658 Initially opacity 0; opacity tween reveals at morph hand-off.
659 ============================================================ */
660 .avatar-with-bubbles {
661 position: absolute;
662 inset: 0;
663 opacity: 0;
664 will-change: opacity;
665 }
666 .avatar-stage {
667 position: absolute;
668 left: 50%;
669 top: 50%;
670 z-index: 20;
671 will-change: transform;
672 }
673 .avatar-ring {
674 width: 220px;
675 height: 220px;
676 border-radius: 50%;
677 padding: 5px;
678 background: linear-gradient(135deg, #14b8a6, #06b6d4, #0ea5e9);
679 box-shadow:
680 0 0 60px rgba(20, 184, 166, 0.5),
681 0 0 120px rgba(6, 182, 212, 0.3);
682 }
683 .avatar-disc {
684 width: 100%;
685 height: 100%;
686 border-radius: 50%;
687 border: 3px solid rgba(255, 255, 255, 0.25);
688 background:
689 radial-gradient(circle at 40% 35%, rgba(255, 255, 255, 0.2), transparent 50%),
690 conic-gradient(from 40deg, #14b8a6 0%, #06b6d4 35%, #0ea5e9 65%, #8b5cf6 100%);
691 overflow: hidden;
692 }
693 .orbit-dot {
694 position: absolute;
695 left: 50%;
696 top: 50%;
697 width: 10px;
698 height: 10px;
699 margin: -5px 0 0 -5px;
700 border-radius: 50%;
701 will-change: transform, opacity;
702 }
703 .orbit-dot:nth-child(1) {
704 background: var(--accent-lime);
705 }
706 .orbit-dot:nth-child(2) {
707 background: var(--brand-purple);
708 }
709 .orbit-dot:nth-child(3) {
710 background: var(--accent-lime);
711 }
712 .orbit-dot:nth-child(4) {
713 background: var(--brand-purple);
714 }
715
716 .task-bubble {
717 position: absolute;
718 z-index: 30;
719 background: var(--bubble-bg);
720 border: 3px solid var(--bubble-border);
721 border-radius: 24px;
722 padding: 22px 34px;
723 box-shadow: 0 12px 35px rgba(139, 92, 246, 0.3);
724 font: 500 26px/1.4 Inter;
725 color: var(--bubble-text);
726 white-space: nowrap;
727 will-change: transform, opacity;
728 }
729 </style>
730 </head>
731 <body>
732 <div
733 id="root"
734 data-composition-id="main"
735 data-start="0"
736 data-duration="6"
737 data-width="1920"
738 data-height="1080"
739 >
740 <div class="bg"></div>
741 <div class="bg-frost"></div>
742
743 <div
744 id="problem-mockup-overwhelm-scene"
745 class="scene clip"
746 data-start="0"
747 data-duration="6"
748 data-track-index="1"
749 >
750 <!-- ===== Phase 1-2: Mockup cluster + scattered icons ===== -->
751 <div class="mockup-cluster">
752 <!-- HyperFrames Check mockup (left) -->
753 <div class="mockup mockup-left">
754 <div class="phone-shell">
755 <div class="phone-notch"></div>
756 <div class="phone-screen">
757 <div class="mockup-content yt-content">
758 <div class="yt-header">
759 <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
760 <defs>
761 <linearGradient id="hf-mini-grad-left" x1="2" y1="4" x2="22" y2="20">
762 <stop offset="0%" stop-color="#ff0000" />
763 <stop offset="100%" stop-color="#cc0000" />
764 </linearGradient>
765 </defs>
766 <path
767 class="hf-logo-left"
768 d="M4 9.5 11.5 5c2.7-1.6 5.7.8 5.1 3.8L15 17.5c-.5 2.8-3.8 4-6 2.2L3.2 15c-2-1.6-1.5-4.2.8-5.5Z"
769 />
770 <path
771 class="hf-logo-left"
772 d="M13.2 6.5 20.8 11c2.4 1.4 2.4 4.6 0 6l-7.6 4.5c-2.6 1.5-5.8-.7-5.2-3.6l1.8-9c.5-2.8 3-3.8 3.4-2.4Z"
773 opacity="0.82"
774 />
775 </svg>
776 HyperFrames Check
777 </div>
778 <div class="yt-body">
779 <h3>Validate composition</h3>
780 <div class="yt-thumb">
781 <div class="code-line"></div>
782 <div class="code-line"></div>
783 <div class="code-line"></div>
784 <div class="code-line"></div>
785 <div class="code-line"></div>
786 <div class="code-line"></div>
787 </div>
788 <div class="yt-progress-row"><span>Lint + inspect</span><span>72%</span></div>
789 <div class="yt-progress-track"><div class="yt-progress-fill"></div></div>
790 <div class="yt-task-label">Render-safe rules:</div>
791 <div class="yt-task">data-start / duration set</div>
792 <div class="yt-task">Timeline registered</div>
793 <div class="yt-task">Assets resolve locally</div>
794 <div class="yt-task">Text fits every frame</div>
795 </div>
796 <div class="yt-footer">npm run check before render</div>
797 </div>
798 </div>
799 </div>
800 </div>
801
802 <!-- HyperFrames Preview mockup (center, morph target) -->
803 <div class="mockup mockup-center">
804 <div class="phone-shell">
805 <div class="phone-notch"></div>
806 <div class="phone-screen">
807 <div class="mockup-content tt-content">
808 <div class="tt-statusbar"><span>9:41</span><span>HF</span></div>
809 <div class="tt-preview">
810 <div class="tt-overlay">
811 <div class="tt-overlay-title">Scene 05 · seekable</div>
812 <div class="tt-overlay-sub">GSAP timeline + data clips</div>
813 </div>
814 <div class="tt-play">HF</div>
815 <div class="tt-side">
816 <div><span>12</span>Clips</div>
817 <div><span>4</span>Tracks</div>
818 <div><span>V</span>Vars</div>
819 <div><span>MP4</span>Render</div>
820 </div>
821 </div>
822 <div class="tt-timeline">
823 <div class="tt-strip">
824 <div class="tt-clip"></div>
825 <div class="tt-clip"></div>
826 <div class="tt-clip active"></div>
827 <div class="tt-clip"></div>
828 <div class="tt-clip"></div>
829 <div class="tt-clip"></div>
830 <div class="tt-clip"></div>
831 <div class="tt-clip"></div>
832 </div>
833 <div class="tt-note">0 errors · frame-safe render</div>
834 </div>
835 <div class="tt-tabs">
836 <span>Clips</span><span>Tracks</span><span>Vars</span><span>Render</span>
837 </div>
838 </div>
839 </div>
840 </div>
841 </div>
842
843 <!-- HyperFrames Render mockup (right) -->
844 <div class="mockup mockup-right">
845 <div class="phone-shell">
846 <div class="phone-notch"></div>
847 <div class="phone-screen">
848 <div class="mockup-content ig-content">
849 <div class="ig-header">
850 <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
851 <rect
852 x="3"
853 y="4"
854 width="18"
855 height="14"
856 rx="3"
857 fill="none"
858 stroke="#833ab4"
859 stroke-width="2"
860 />
861 <path d="M9 9.2 15 12 9 14.8Z" fill="#fd1d1d" />
862 <path d="M6 21h12" stroke="#fcaf45" stroke-width="2" stroke-linecap="round" />
863 </svg>
864 Render Queue
865 </div>
866 <div class="ig-body">
867 <h3>Export HyperFrames scene</h3>
868 <div class="ig-warn">
869 <div class="ig-warn-title">Ready for MP4 render</div>
870 <div class="ig-warn-sub">lint · validate · inspect passed</div>
871 </div>
872 <div class="ig-task-label">Pipeline:</div>
873 <div class="ig-task">Preview locally</div>
874 <div class="ig-task">Validate runtime</div>
875 <div class="ig-task">Inspect layout</div>
876 <div class="ig-task">Render MP4</div>
877 <div class="ig-task">Publish share link</div>
878 <div class="ig-time">Draft render: ~45 sec</div>
879 </div>
880 <div class="ig-footer">
881 <div class="ig-share">Publish link</div>
882 </div>
883 </div>
884 </div>
885 </div>
886 </div>
887
888 <!-- Nine scattered HyperFrames workflow icons -->
889 <div class="platform-icon pi-youtube">
890 <svg viewBox="0 0 24 24" aria-hidden="true">
891 <defs>
892 <linearGradient id="hf-orbit-grad" x1="2" y1="4" x2="22" y2="20">
893 <stop offset="0%" stop-color="#14b8a6" />
894 <stop offset="100%" stop-color="#06b6d4" />
895 </linearGradient>
896 </defs>
897 <path
898 d="M4 9.5 11.5 5c2.7-1.6 5.7.8 5.1 3.8L15 17.5c-.5 2.8-3.8 4-6 2.2L3.2 15c-2-1.6-1.5-4.2.8-5.5Z"
899 fill="url(#hf-orbit-grad)"
900 />
901 <path
902 d="M13.2 6.5 20.8 11c2.4 1.4 2.4 4.6 0 6l-7.6 4.5c-2.6 1.5-5.8-.7-5.2-3.6l1.8-9c.5-2.8 3-3.8 3.4-2.4Z"
903 fill="url(#hf-orbit-grad)"
904 opacity="0.82"
905 />
906 </svg>
907 </div>
908 <div class="platform-icon pi-google">
909 <span class="icon-type"></></span>
910 </div>
911 <div class="platform-icon pi-instagram">
912 <span class="icon-type icon-small">GS</span>
913 </div>
914 <div class="platform-icon pi-tiktok">
915 <span class="icon-type icon-small">CSS</span>
916 </div>
917 <div class="platform-icon pi-tripadvisor">
918 <svg viewBox="0 0 24 24" aria-hidden="true">
919 <path
920 d="M12 3 20 7.5v9L12 21 4 16.5v-9Z"
921 fill="none"
922 stroke="#063238"
923 stroke-width="1.6"
924 />
925 <path
926 d="M12 3v9l8-4.5M12 12 4 7.5M12 12v9"
927 stroke="#06b6d4"
928 stroke-width="1.6"
929 stroke-linecap="round"
930 />
931 </svg>
932 </div>
933 <div class="platform-icon pi-facebook">
934 <svg viewBox="0 0 24 24" aria-hidden="true">
935 <path
936 d="M4 14v-4M8 17V7M12 19V5M16 16V8M20 13v-2"
937 stroke="#063238"
938 stroke-width="2.4"
939 stroke-linecap="round"
940 />
941 <path
942 d="M8 17V7M16 16V8"
943 stroke="#06b6d4"
944 stroke-width="2.4"
945 stroke-linecap="round"
946 />
947 </svg>
948 </div>
949 <div class="platform-icon pi-twitter">
950 <span class="icon-type icon-small">CAP</span>
951 </div>
952 <div class="platform-icon pi-linkedin">
953 <span class="icon-type icon-small">MP4</span>
954 </div>
955 <div class="platform-icon pi-yelp">
956 <span class="icon-type icon-small">PUB</span>
957 </div>
958 </div>
959 <!-- /.mockup-cluster -->
960
961 <!-- ===== Phase 3-4: Avatar + task bubbles ===== -->
962 <div class="avatar-with-bubbles" id="avatar-bubbles">
963 <div class="avatar-stage" id="avatar-stage">
964 <div class="avatar-ring">
965 <div class="avatar-disc"></div>
966 </div>
967 <div class="orbit-dot" id="orbit-0"></div>
968 <div class="orbit-dot" id="orbit-1"></div>
969 <div class="orbit-dot" id="orbit-2"></div>
970 <div class="orbit-dot" id="orbit-3"></div>
971 </div>
972 <!-- task bubbles appended by JS -->
973 </div>
974 </div>
975 <!-- /.scene -->
976
977 <div class="vignette"></div>
978 </div>
979
980 <script>
981 /* ================================================================
982 CONSTANTS
983 ================================================================ */
984 const W = 1920,
985 H = 1080;
986 const MOCKUP_CENTER_W = 320; // .mockup-center intrinsic width (matches CSS)
987 const MOCKUP_CENTER_H = 650;
988 const AVATAR_DIAMETER = 220;
989 const MORPH_END_SCALE_X = AVATAR_DIAMETER / MOCKUP_CENTER_W; // ≈ 0.6875
990 const MORPH_END_SCALE_Y = AVATAR_DIAMETER / MOCKUP_CENTER_H; // ≈ 0.3385
991
992 const TIMING = {
993 // Phase 1 — center stack → side-phone fan-out
994 mockupCenterAt: 0.08,
995 mockupLeftAt: 0.2,
996 mockupRightAt: 0.28,
997 mockupEntryDur: 0.7,
998 mockupFanDur: 1.08,
999
1000 // Phase 2 — icon entries
1001 iconsAt: 0.33,
1002 iconEntryDur: 0.45,
1003 iconStagger: 0.07,
1004
1005 // Phase 3 — morph
1006 morphAt: 3.2,
1007 morphDur: 0.6,
1008
1009 // Avatar (pop concurrent with morph; layer opacity in at 50% of morph)
1010 avatarPopAt: 3.2,
1011 avatarPopDur: 0.55,
1012 avatarLayerInAt: 3.5,
1013 avatarLayerInDur: 0.3,
1014
1015 // Phase 4 — bubbles
1016 bubblesAt: 3.53,
1017 bubbleDur: 0.45,
1018 bubbleStagger: 0.07,
1019 };
1020
1021 const BUBBLE_TASKS = [
1022 { label: "Missing data-duration", angle: 270, dx: 14, dy: -8 }, // top
1023 { label: "Timeline not registered", angle: 315, dx: 122, dy: 63 }, // top-right
1024 { label: "Same-track overlaps", angle: 0, dx: 86, dy: 63 }, // right
1025 { label: "Text overflow at 5s", angle: 45, dx: 79, dy: -9 }, // bottom-right
1026 { label: "Asset path mismatch", angle: 90 }, // bottom
1027 { label: "Non-deterministic motion", angle: 135, dx: -124, dy: -32 }, // bottom-left
1028 { label: "Sub-comp wiring drift", angle: 180, dx: -79, dy: 1 }, // left
1029 { label: "Render blocked by console", angle: 225, dx: -128, dy: 45 }, // top-left
1030 ];
1031 const BUBBLE_RADIUS = 420; // px from screen center
1032 const BUBBLE_CENTER_X = W / 2;
1033 const BUBBLE_CENTER_Y = H / 2;
1034
1035 /* ================================================================
1036 INITIAL STATE (GSAP owns the transform from here on)
1037 ================================================================ */
1038 // All phones start stacked at center, then the side phones fan out like the Fitness showcase.
1039 gsap.set(".mockup-left", {
1040 xPercent: -50,
1041 yPercent: -50,
1042 x: 0,
1043 y: 0,
1044 rotation: 0,
1045 rotationY: 0,
1046 scale: 0.86,
1047 opacity: 0,
1048 });
1049 gsap.set(".mockup-right", {
1050 xPercent: -50,
1051 yPercent: -50,
1052 x: 0,
1053 y: 0,
1054 rotation: 0,
1055 rotationY: 0,
1056 scale: 0.86,
1057 opacity: 0,
1058 });
1059 gsap.set(".mockup-center", {
1060 xPercent: -50,
1061 yPercent: -50,
1062 x: 0,
1063 y: 0,
1064 rotation: 0,
1065 rotationY: 0,
1066 scale: 0.84,
1067 opacity: 0,
1068 });
1069
1070 gsap.set(".platform-icon", { scale: 0, opacity: 0 });
1071
1072 // Avatar — sits centered. xPercent/yPercent centering, pop starts at scale 0.
1073 gsap.set(".avatar-stage", { xPercent: -50, yPercent: -50, scale: 0 });
1074
1075 /* Build task bubbles deterministically at script load.
1076 Positions baked into inline style; GSAP only tweens scale/opacity. */
1077 const stage = document.getElementById("avatar-bubbles");
1078 BUBBLE_TASKS.forEach((task, i) => {
1079 const rad = (task.angle * Math.PI) / 180;
1080 const x = BUBBLE_CENTER_X + Math.cos(rad) * BUBBLE_RADIUS + (task.dx || 0);
1081 const y = BUBBLE_CENTER_Y + Math.sin(rad) * BUBBLE_RADIUS + (task.dy || 0);
1082 const el = document.createElement("div");
1083 el.className = "task-bubble";
1084 el.id = "bubble-" + i;
1085 el.style.left = x + "px";
1086 el.style.top = y + "px";
1087 el.textContent = task.label;
1088 stage.appendChild(el);
1089 // GSAP-owned centering + initial hidden state.
1090 gsap.set(el, { xPercent: -50, yPercent: -50, scale: 0, opacity: 0 });
1091 });
1092
1093 /* ================================================================
1094 TIMELINE
1095 ================================================================ */
1096 window.__timelines = window.__timelines || {};
1097 const tl = gsap.timeline({ paused: true });
1098 window.__timelines["main"] = tl;
1099
1100 /* ---------------------------------------------------------------
1101 PHASE 1: Mockup entries (center stack → fan-out)
1102 spring(70, 14) → back.out(1.4)
1103 --------------------------------------------------------------- */
1104 tl.to(
1105 ".mockup-center",
1106 { opacity: 1, scale: 1.0, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
1107 TIMING.mockupCenterAt,
1108 );
1109 tl.to(
1110 ".mockup-left",
1111 { opacity: 1, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
1112 TIMING.mockupLeftAt,
1113 );
1114 tl.to(
1115 ".mockup-left",
1116 {
1117 x: -370,
1118 rotation: -10,
1119 rotationY: 6,
1120 scale: 0.92,
1121 duration: TIMING.mockupFanDur,
1122 ease: "expo.out",
1123 },
1124 TIMING.mockupLeftAt,
1125 );
1126 tl.to(
1127 ".mockup-right",
1128 { opacity: 1, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
1129 TIMING.mockupRightAt,
1130 );
1131 tl.to(
1132 ".mockup-right",
1133 {
1134 x: 370,
1135 rotation: 10,
1136 rotationY: -6,
1137 scale: 0.92,
1138 duration: TIMING.mockupFanDur,
1139 ease: "expo.out",
1140 },
1141 TIMING.mockupRightAt,
1142 );
1143
1144 /* ---------------------------------------------------------------
1145 PHASE 2: Platform icons (staggered)
1146 spring(180, 14) → back.out(1.6)
1147 --------------------------------------------------------------- */
1148 tl.to(
1149 ".platform-icon",
1150 {
1151 scale: 1,
1152 opacity: 1,
1153 duration: TIMING.iconEntryDur,
1154 ease: "back.out(1.6)",
1155 stagger: { each: TIMING.iconStagger, from: "start" },
1156 },
1157 TIMING.iconsAt,
1158 );
1159
1160 /* ---------------------------------------------------------------
1161 PHASE 3: MORPH (the core glue)
1162 spring(80, 18) → power3.out
1163 --------------------------------------------------------------- */
1164 // 1. Center mockup compositor-scales toward the avatar footprint.
1165 tl.to(
1166 ".mockup-center",
1167 {
1168 scaleX: MORPH_END_SCALE_X,
1169 scaleY: MORPH_END_SCALE_Y,
1170 duration: TIMING.morphDur,
1171 ease: "power3.out",
1172 },
1173 TIMING.morphAt,
1174 );
1175
1176 // 2. borderRadius repaint — reads as a circle by morph end.
1177 tl.to(
1178 ".mockup-center",
1179 {
1180 borderRadius: "50%",
1181 duration: TIMING.morphDur,
1182 ease: "power3.out",
1183 },
1184 TIMING.morphAt,
1185 );
1186
1187 // 3. Surface shifts: black card → cyan-gradient glow.
1188 tl.to(
1189 ".mockup-center",
1190 {
1191 background: "linear-gradient(135deg, #14B8A6, #06B6D4, #0EA5E9)",
1192 boxShadow: "0 0 50px rgba(20, 184, 166, 0.50), 0 0 100px rgba(6, 182, 212, 0.25)",
1193 duration: TIMING.morphDur,
1194 ease: "power3.out",
1195 },
1196 TIMING.morphAt,
1197 );
1198
1199 // 4. Content fades during first 40% of morph (hides aspect-ratio mismatch).
1200 tl.to(
1201 ".mockup-center .mockup-content",
1202 {
1203 opacity: 0,
1204 duration: TIMING.morphDur * 0.4,
1205 ease: "power2.out",
1206 },
1207 TIMING.morphAt,
1208 );
1209
1210 // 4b. Fade the physical phone shell early so the morph reads as a clean surface hand-off.
1211 tl.to(
1212 ".mockup-center .phone-shell",
1213 {
1214 opacity: 0,
1215 duration: TIMING.morphDur * 0.45,
1216 ease: "power2.out",
1217 },
1218 TIMING.morphAt,
1219 );
1220
1221 // 5. Hand-off: at 85-100% of morph, container fades to 0 → avatar reveals underneath.
1222 tl.to(
1223 ".mockup-center",
1224 {
1225 opacity: 0,
1226 duration: TIMING.morphDur * 0.15,
1227 ease: "none",
1228 },
1229 TIMING.morphAt + TIMING.morphDur * 0.85,
1230 );
1231
1232 /* Non-center mockups exit concurrently with morph */
1233 tl.to(
1234 [".mockup-left", ".mockup-right"],
1235 {
1236 opacity: 0,
1237 scale: 0.85,
1238 duration: TIMING.morphDur * 0.55,
1239 ease: "power2.out",
1240 },
1241 TIMING.morphAt,
1242 );
1243
1244 /* Platform icons exit concurrently with morph (edge-out stagger) */
1245 tl.to(
1246 ".platform-icon",
1247 {
1248 opacity: 0,
1249 scale: 0.85,
1250 duration: TIMING.morphDur * 0.5,
1251 ease: "power2.out",
1252 stagger: { each: 0.025, from: "edges" },
1253 },
1254 TIMING.morphAt,
1255 );
1256
1257 /* Avatar pop — spring(120, 14) → back.out(1.4), concurrent with morph trigger */
1258 tl.fromTo(
1259 ".avatar-stage",
1260 { scale: 0 },
1261 { scale: 1, duration: TIMING.avatarPopDur, ease: "back.out(1.4)" },
1262 TIMING.avatarPopAt,
1263 );
1264
1265 /* Avatar layer opacity in at 50% of morph */
1266 tl.to(
1267 "#avatar-bubbles",
1268 {
1269 opacity: 1,
1270 duration: TIMING.avatarLayerInDur,
1271 ease: "power2.out",
1272 },
1273 TIMING.avatarLayerInAt,
1274 );
1275
1276 /* ---------------------------------------------------------------
1277 PHASE 4: Task bubbles (staggered radial entry)
1278 spring(180, 12) → back.out(1.4)
1279 --------------------------------------------------------------- */
1280 tl.to(
1281 ".task-bubble",
1282 {
1283 scale: 1,
1284 opacity: 0.95,
1285 duration: TIMING.bubbleDur,
1286 ease: "back.out(1.4)",
1287 stagger: { each: TIMING.bubbleStagger, from: "start" },
1288 },
1289 TIMING.bubblesAt,
1290 );
1291
1292 /* ---------------------------------------------------------------
1293 CONTINUOUS MOTION (shared scene-ticker onUpdate)
1294 Reads tl.time() each frame; cheaper than N independent onUpdates.
1295 Drives:
1296 - mockup floating (Phase 1-2 only — gated to t < morphAt)
1297 - platform-icon floating (same)
1298 - orbit dots around avatar
1299 - avatar breath (multiplicative on pop scale)
1300 - bubble micro-float (after bubblesAt)
1301 --------------------------------------------------------------- */
1302 const orbitDots = [
1303 document.getElementById("orbit-0"),
1304 document.getElementById("orbit-1"),
1305 document.getElementById("orbit-2"),
1306 document.getElementById("orbit-3"),
1307 ];
1308 const ORBIT_RADIUS = 130;
1309 const ORBIT_SPEED = 2; // degrees per timeline-time unit (i.e. per second × 30)
1310
1311 const avatarStage = document.getElementById("avatar-stage");
1312 const bubbleEls = Array.from(document.querySelectorAll(".task-bubble"));
1313 const mockupLeft = document.querySelector(".mockup-left");
1314 const mockupRight = document.querySelector(".mockup-right");
1315 const platformIcons = Array.from(document.querySelectorAll(".platform-icon"));
1316
1317 tl.to(
1318 { tick: 0 },
1319 {
1320 tick: 1,
1321 duration: 6.0,
1322 ease: "none",
1323 onUpdate: function () {
1324 const t = tl.time();
1325
1326 /* Mockup + icon floating (only while they're visible — pre-morph) */
1327 if (t < TIMING.morphAt) {
1328 const mlY = Math.sin(t * 0.36) * 3;
1329 const mrY = Math.sin(t * 0.45 + 1) * 3;
1330 gsap.set(mockupLeft, { y: mlY });
1331 gsap.set(mockupRight, { y: mrY });
1332
1333 platformIcons.forEach((el, i) => {
1334 const fx = Math.sin((t * 30 + i * 30) * 0.025) * 5;
1335 const fy = Math.cos((t * 30 + i * 20) * 0.03) * 4;
1336 gsap.set(el, { x: fx, y: fy });
1337 });
1338 }
1339
1340 /* Avatar breath (multiplicative on pop scale, only after pop completes) */
1341 const popEnd = TIMING.avatarPopAt + TIMING.avatarPopDur;
1342 if (t >= popEnd) {
1343 const idleT = t - popEnd;
1344 const breath = 1 + Math.sin(idleT * 0.72) * 0.03;
1345 // popScale at this point is 1.0 (back.out settled). Multiplicative → still 1.0 × breath.
1346 gsap.set(avatarStage, { scale: 1 * breath });
1347 }
1348
1349 /* Orbit dots — always cycling once avatar layer is visible */
1350 if (t >= TIMING.avatarLayerInAt) {
1351 orbitDots.forEach((dot, i) => {
1352 const angle = (t * 30 * ORBIT_SPEED + i * 90) * (Math.PI / 180);
1353 const x = Math.cos(angle) * ORBIT_RADIUS;
1354 const y = Math.sin(angle) * ORBIT_RADIUS;
1355 const op = 0.7 + Math.sin(t * 30 * 0.1 + i) * 0.3;
1356 gsap.set(dot, { x: x, y: y, opacity: op });
1357 });
1358 }
1359
1360 /* Bubble micro-float (after they enter) */
1361 if (t >= TIMING.bubblesAt + TIMING.bubbleDur) {
1362 bubbleEls.forEach((el, i) => {
1363 const fx = Math.sin((t * 30 + i * 24) * 0.015) * 5;
1364 const fy = Math.cos((t * 30 + i * 30) * 0.018) * 4;
1365 gsap.set(el, { x: fx, y: fy });
1366 });
1367 }
1368 },
1369 },
1370 0,
1371 );
1372 </script>
1373 </body>
1374</html>