Setting the file. One moment. Hook Counter Burst · Hyperframes Animation · heygen-com/hyperframes · Skills DocsBlueprints Index
(opens in a new tab)
examples/hook-counter-burst.html
HTML·729 lines·22 KB
8 <!--
9 HyperFrames composition.tsx.
10
11 Choreography (4 phases, 3.5 seconds total):
12 0.00 – 0.17s Background visible with dark overlay; nothing else
13 0.17 – 0.57s Four enriched icons enter staggered, clustered at startOffset 0.4
14 (clock 0.17s · scissors 0.30s · video 0.43s · play 0.57s)
15 0.47 – 1.47s Counter "0 → 90" with growing font size (0.20W → 0.42W);
16 icons expand outward from 40% to 100% position
17 0.50 – 2.33s Camera focus phase: scale 0.92 → 1.0
18 2.33 – 3.50s Camera push phase: scale 1.0 → 1.08
19 1.27s Percent symbol pops in with own spring
20
21 Continuous motion (running from t=0, gated by icon visibility):
22 Clock minute hand: linear rotation 420° over 3.5s
23 Scissors: ±15° sine oscillation, period ~1.7s
24 Cutting line: stroke-dashoffset drifts -200 over 3.5s
25 Video record dot: opacity + scale phase-offset sine pulses
26 Play triangle: scale pulse ±8%, period ~2.6s
27 ease + duration (no shared driver needed, just identical timing)
28 - Counter text + number scale are emitted as seek-safe timeline keyframes
29 - Background: local Pexels MP4 plus dark overlay
30 - Per-icon entry uses GSAP fromTo on a nested .icon-entry wrapper
31 so it never overwrites the .icon-pos expansion tween
32 -->
33
34 <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
35
36 <style>
37 :root {
38 --bg-dark: #060812;
39 --text-primary: #ffffff;
40 --accent-yellow: #edcb50;
41 --record-red: #ef4444;
42 }
43 * {
44 margin: 0;
45 padding: 0;
46 box-sizing: border-box;
47 }
48 html,
49 body {
50 margin: 0;
51 width: 1920px;
52 height: 1080px;
53 overflow: hidden;
54 background: var(--bg-dark);
55 font-family:
56 "Inter",
57 system-ui,
58 -apple-system,
59 sans-serif;
60 color: var(--text-primary);
61 }
62
63 /* ============================================================
64 BACKGROUND (static gradient)
65 ============================================================ */
66 .bg {
67 position: absolute;
68 inset: 0;
69 transform-origin: center center;
70 will-change: transform;
71 background:
72 radial-gradient(ellipse at 30% 30%, rgba(80, 100, 200, 0.25), transparent 60%),
73 radial-gradient(ellipse at 70% 70%, rgba(180, 80, 150, 0.2), transparent 60%),
74 linear-gradient(135deg, #0a0d1f 0%, #1a0d20 100%);
75 }
76 .bg-overlay {
77 position: absolute;
78 inset: 0;
79 background: rgba(0, 0, 0, 0.66);
80 }
81
82 /* ============================================================
83 CAMERA WRAPPER (GSAP-managed scale phases)
84 ============================================================ */
85 .camera {
86 position: absolute;
87 inset: 0;
88 transform-origin: center center;
89 will-change: transform;
90 }
91
92 /* ============================================================
93 ICONS — two nested wrappers per icon.
94 .icon-pos is positioned at TARGET; GSAP x/y shifts toward center.
95 .icon-entry tweens scale/opacity/rotation.
96 ============================================================ */
97 .icons-stage {
98 position: absolute;
99 inset: 0;
100 }
101 .icon-pos {
102 position: absolute;
103 width: 180px;
104 height: 180px;
105 will-change: transform;
106 }
107 .icon-entry {
108 width: 100%;
109 height: 100%;
110 display: flex;
111 align-items: center;
112 justify-content: center;
113 will-change: transform, opacity;
114 /* initial scale(0) + opacity(0) set by GSAP fromTo */
115 }
116 .icon-svg {
117 width: 100%;
118 height: 100%;
119 display: block;
120 }
121
122 /* SVG inner elements — class hooks for GSAP tweens */
123 .clock-hand-min {
124 transform-box: view-box;
125 }
126 .scissor-upper,
127 .scissor-lower {
128 transform-origin: 12px 12px;
129 }
130 .play-tri {
131 transform-origin: 12px 12px;
132 }
133 .record-dot {
134 transform-origin: 19px 8px;
135 }
136 .play-ring {
137 transform-origin: 12px 12px;
138 transform: rotate(-90deg);
139 }
140
141 /* ============================================================
142 COUNTER (absolute center, 3D entry)
143 ============================================================ */
144 .counter-stage {
145 position: absolute;
146 inset: 0;
147 display: flex;
148 align-items: center;
149 justify-content: center;
150 perspective: 2000px;
151 perspective-origin: center center;
152 }
153 .counter-3d {
154 display: flex;
155 align-items: baseline;
156 justify-content: center;
157 transform-style: preserve-3d;
158 will-change: transform, opacity;
159 opacity: 0; /* GSAP entry fades in */
160 }
161 .counter-number {
162 font-size: 806px; /* W × 0.42 — final size, GSAP scales from 0.20W */
163 font-weight: 900;
164 letter-spacing: -0.04em;
165 line-height: 0.9;
166 font-variant-numeric: tabular-nums; /* prevents 1→2 digit jitter */
167 color: var(--text-primary);
168 display: inline-block;
169 transform-origin: center bottom;
170 will-change: transform;
171 }
172 .counter-percent {
173 font-size: 269px; /* W × 0.14 */
174 font-weight: 700;
175 letter-spacing: -0.02em;
176 color: var(--accent-yellow);
177 margin-left: 16px;
178 display: inline-block;
179 will-change: transform, opacity;
180 opacity: 0; /* GSAP entry tweens in later */
181 }
182
183 /* ============================================================
184 VIGNETTE
185 ============================================================ */
186 .vignette {
187 position: absolute;
188 inset: 0;
189 background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
190 pointer-events: none;
191 z-index: 400;
192 }
193 </style>
194 </head>
195 <body>
196 <div
197 id="root"
198 data-composition-id="main"
199 data-start="0"
200 data-duration="3.5"
201 data-width="1920"
202 data-height="1080"
203 >
204 <div class="bg"></div>
205 <div class="bg-overlay"></div>
206
207 <div
208 class="camera clip"
209 data-start="0"
210 data-duration="3.5"
211 data-track-index="1"
212 id="camera-stage"
213 >
214 <div class="icons-stage">
215 <!-- Clock -->
216 <div class="icon-pos clock-pos" id="clock-pos">
217 <div class="icon-entry clock-entry">
218 <svg
219 class="icon-svg"
220 viewBox="0 0 24 24"
221 xmlns="http://www.w3.org/2000/svg"
222 aria-hidden="true"
223 >
224 <circle
225 class="clock-ring"
226 cx="12"
227 cy="12"
228 r="9"
229 fill="none"
230 stroke="white"
231 stroke-width="2"
232 stroke-dasharray="56.5"
233 stroke-dashoffset="56.5"
234 />
235 <line
236 x1="12"
237 y1="12"
238 x2="12"
239 y2="8"
240 stroke="white"
241 stroke-width="2"
242 stroke-linecap="round"
243 />
244 <line
245 class="clock-hand-min"
246 x1="12"
247 y1="12"
248 x2="12"
249 y2="6"
250 stroke="#edcb50"
251 stroke-width="2"
252 stroke-linecap="round"
253 />
254 <circle cx="12" cy="12" r="1.5" fill="#edcb50" />
255 </svg>
256 </div>
257 </div>
258
259 <!-- Scissors -->
260 <div class="icon-pos scissors-pos" id="scissors-pos">
261 <div class="icon-entry scissors-entry">
262 <svg
263 class="icon-svg"
264 viewBox="0 0 24 24"
265 xmlns="http://www.w3.org/2000/svg"
266 aria-hidden="true"
267 >
268 <g class="scissor-upper">
269 <path
270 d="M12 10.5L4.5 3L6.5 3L14 10.5"
271 stroke="white"
272 stroke-width="2"
273 stroke-linecap="round"
274 fill="none"
275 />
276 <circle cx="6" cy="6" r="2.5" fill="none" stroke="white" stroke-width="1.5" />
277 </g>
278 <g class="scissor-lower">
279 <path
280 d="M12 13.5L4.5 21L6.5 21L14 13.5"
281 stroke="white"
282 stroke-width="2"
283 stroke-linecap="round"
284 fill="none"
285 />
286 <circle cx="6" cy="18" r="2.5" fill="none" stroke="white" stroke-width="1.5" />
287 </g>
288 <line
289 class="cutting-line"
290 x1="14"
291 y1="12"
292 x2="20"
293 y2="12"
294 stroke="#edcb50"
295 stroke-width="2"
296 stroke-linecap="round"
297 stroke-dasharray="4 2"
298 />
299 </svg>
300 </div>
301 </div>
302
303 <!-- Video -->
304 <div class="icon-pos video-pos" id="video-pos">
305 <div class="icon-entry video-entry">
306 <svg
307 class="icon-svg"
308 viewBox="0 0 24 24"
309 xmlns="http://www.w3.org/2000/svg"
310 aria-hidden="true"
311 >
312 <rect
313 class="video-frame"
314 x="3"
315 y="5"
316 width="14"
317 height="14"
318 rx="2"
319 stroke="white"
320 stroke-width="2"
321 fill="none"
322 stroke-dasharray="56"
323 stroke-dashoffset="56"
324 />
325 <path d="M8 9L13 12L8 15V9Z" fill="white" />
326 <circle class="record-dot" cx="19" cy="8" r="2.5" fill="#ef4444" />
327 </svg>
328 </div>
329 </div>
330
331 <!-- Play -->
332 <div class="icon-pos play-pos" id="play-pos">
333 <div class="icon-entry play-entry">
334 <svg
335 class="icon-svg"
336 viewBox="0 0 24 24"
337 xmlns="http://www.w3.org/2000/svg"
338 aria-hidden="true"
339 >
340 <circle
341 class="play-ring"
342 cx="12"
343 cy="12"
344 r="9"
345 fill="none"
346 stroke="white"
347 stroke-width="2"
348 stroke-dasharray="56.5"
349 stroke-dashoffset="56.5"
350 />
351 <circle cx="12" cy="12" r="7" fill="rgba(255,255,255,0.05)" />
352 <path class="play-tri" d="M10 8L16 12L10 16V8Z" fill="#edcb50" />
353 </svg>
354 </div>
355 </div>
356 </div>
357 <!-- /.icons-stage -->
358
359 <div class="counter-stage">
360 <div class="counter-3d" id="counter-3d">
361 <span class="counter-number" id="counter-number">0</span
362 ><span class="counter-percent" id="counter-percent">%</span>
363 </div>
364 </div>
365 </div>
366 <!-- /.camera -->
367
368 <div class="vignette"></div>
369 </div>
370
371 <script>
372 /* ================================================================
373 CONSTANTS
374 ================================================================ */
375 const W = 1920,
376 H = 1080;
377 const ICON_SIZE = 180;
378 const CENTER_X = W / 2 - ICON_SIZE / 2; // 870
379 const CENTER_Y = H / 2 - ICON_SIZE / 2; // 450
380
381 const TIMING = {
382 // Phase 2 — icon entries (staggered)
383 clockEntryAt: 0.17,
384 scissorsEntryAt: 0.3,
385 videoEntryAt: 0.43,
386 playEntryAt: 0.57,
387 entryDur: 0.55,
388
389 // Phase 3 — count + expansion (shared start, dur, ease)
390 countAt: 0.47,
391 countDur: 1.0,
392 startOffset: 0.4,
393
394 // Counter 3D entry (slightly earlier than count starts)
395 counterEntryAt: 0.27,
396 counterEntryDur: 0.7,
397
398 // Percent symbol entry
399 percentEntryAt: 1.27,
400 percentEntryDur: 0.55,
401
402 // Phase 4 — camera
403 cameraFocusAt: 0.5,
404 cameraFocusDur: 1.83,
405 cameraPushAt: 2.33,
406 cameraPushDur: 1.17,
407 };
408
409 const ICONS = [
410 {
411 name: "clock",
412 sel: "#clock-pos",
413 targetX: W * 0.06,
414 targetY: H * 0.18,
415 delay: TIMING.clockEntryAt,
416 entryRotation: -180,
417 entryEase: "back.out(1.5)",
418 },
419 {
420 name: "scissors",
421 sel: "#scissors-pos",
422 targetX: W * 0.85,
423 targetY: H * 0.2,
424 delay: TIMING.scissorsEntryAt,
425 entryRotation: 0,
426 entryEase: "back.out(1.5)",
427 },
428 {
429 name: "video",
430 sel: "#video-pos",
431 targetX: W * 0.04,
432 targetY: H * 0.68,
433 delay: TIMING.videoEntryAt,
434 entryRotation: 0,
435 entryEase: "back.out(1.4)",
436 },
437 {
438 name: "play",
439 sel: "#play-pos",
440 targetX: W * 0.88,
441 targetY: H * 0.65,
442 delay: TIMING.playEntryAt,
443 entryRotation: 90,
444 entryEase: "back.out(1.5)",
445 },
446 ];
447
448 /* ================================================================
449 SET ICON TARGET POSITIONS + INITIAL EXPANSION OFFSETS
450 ================================================================ */
451 ICONS.forEach(({ sel, targetX, targetY }) => {
452 const el = document.querySelector(sel);
453 el.style.left = targetX + "px";
454 el.style.top = targetY + "px";
455
456 // Initial offset for START_OFFSET = 0.4:
457 // position = target + (center - target) * (1 - 0.4) = target + 0.6 * (center - target)
458 const offsetX = (CENTER_X - targetX) * (1 - TIMING.startOffset);
459 const offsetY = (CENTER_Y - targetY) * (1 - TIMING.startOffset);
460 gsap.set(el, { x: offsetX, y: offsetY });
461 });
462
463 /* Set initial entry state on each .icon-entry */
464 ICONS.forEach(({ name, entryRotation }) => {
465 gsap.set(`.${name}-entry`, {
466 scale: 0,
467 opacity: 0,
468 rotation: entryRotation,
469 });
470 });
471
472 /* Set initial background and camera scale + subtle pan baseline */
473 gsap.set(".bg", { scale: 1.05 });
474 gsap.set(".camera", { scale: 0.92, x: 0, y: 2 });
475 gsap.set("#counter-number", { scale: 0.2 / 0.42 });
476
477 /* ================================================================
478 TIMELINE
479 ================================================================ */
480 window.__timelines = window.__timelines || {};
481 const tl = gsap.timeline({ paused: true });
482 window.__timelines["main"] = tl;
483
484 tl.to(
485 ".bg",
486 {
487 scale: 1.07,
488 x: 6,
489 y: -4,
490 duration: 3.5,
491 ease: "sine.inOut",
492 },
493 0,
494 );
495
496 /* ----------------------------------------------------------------
497 PHASE 2: Icon entries (staggered)
498 ---------------------------------------------------------------- */
499 ICONS.forEach(({ name, delay, entryEase }) => {
500 // Scale + opacity + rotation entry
501 tl.to(
502 `.${name}-entry`,
503 {
504 scale: 1,
505 opacity: 0.85,
506 rotation: 0,
507 duration: TIMING.entryDur,
508 ease: entryEase,
509 },
510 delay,
511 );
512 });
513
514 // Stroke-draw the outline circles/rect on clock, video, and play during their entries
515 tl.to(
516 ".clock-ring",
517 {
518 attr: { "stroke-dashoffset": 0 },
519 duration: TIMING.entryDur,
520 ease: "power2.out",
521 },
522 TIMING.clockEntryAt,
523 );
524
525 tl.to(
526 ".video-frame",
527 {
528 attr: { "stroke-dashoffset": 0 },
529 duration: TIMING.entryDur,
530 ease: "power2.out",
531 },
532 TIMING.videoEntryAt,
533 );
534
535 tl.to(
536 ".play-ring",
537 {
538 attr: { "stroke-dashoffset": 0 },
539 duration: TIMING.entryDur,
540 ease: "power2.out",
541 },
542 TIMING.playEntryAt,
543 );
544
545 /* ----------------------------------------------------------------
546 PHASE 3: Counter (with 3D entry) + expansion
547 ---------------------------------------------------------------- */
548 // Counter 3D entry — translateZ + rotateY + scale + opacity, in parallel.
549 tl.fromTo(
550 "#counter-3d",
551 { rotationY: 10, scale: 0.7, z: -300, opacity: 0 },
552 {
553 rotationY: 0,
554 scale: 1.0,
555 z: 0,
556 opacity: 1,
557 duration: TIMING.counterEntryDur,
558 ease: "power3.out",
559 },
560 TIMING.counterEntryAt,
561 );
562
563 // Counter number + apparent size. Use discrete frame keyframes so
564 // HyperFrames can seek directly to any sampled time.
565 const counterEl = document.querySelector("#counter-number");
566 const COUNT_STEPS = 30;
567 const COUNT_START_SCALE = 0.2 / 0.42;
568
569 for (let i = 0; i <= COUNT_STEPS; i += 1) {
570 const p = i / COUNT_STEPS;
571 const eased = 1 - Math.pow(1 - p, 2.5);
572 const number = Math.round(eased * 90);
573 const scale = COUNT_START_SCALE + eased * (1 - COUNT_START_SCALE);
574
575 tl.set(
576 counterEl,
577 {
578 textContent: String(number),
579 scale,
580 },
581 TIMING.countAt + p * TIMING.countDur,
582 );
583 }
584
585 // Percent symbol enters later with its own spring.
586 tl.fromTo(
587 "#counter-percent",
588 { x: 60, rotation: 20, scale: 0.4, opacity: 0 },
589 {
590 x: 0,
591 rotation: 0,
592 scale: 1.0,
593 opacity: 1,
594 duration: TIMING.percentEntryDur,
595 ease: "back.out(1.4)",
596 },
597 TIMING.percentEntryAt,
598 );
599
600 // Icon expansion — per-icon, same start/dur/ease as counter for lock-step sync.
601 ICONS.forEach(({ sel }) => {
602 tl.to(
603 sel,
604 {
605 x: 0,
606 y: 0,
607 duration: TIMING.countDur,
608 ease: "power2.out",
609 },
610 TIMING.countAt,
611 );
612 });
613
614 /* ----------------------------------------------------------------
615 PHASE 4: Multi-phase camera
616 ---------------------------------------------------------------- */
617 tl.to(
618 ".camera",
619 {
620 scale: 1.0,
621 duration: TIMING.cameraFocusDur,
622 ease: "power2.out",
623 },
624 TIMING.cameraFocusAt,
625 );
626
627 tl.to(
628 ".camera",
629 {
630 scale: 1.08,
631 duration: TIMING.cameraPushDur,
632 ease: "power2.out",
633 },
634 TIMING.cameraPushAt,
635 );
636
637 // Subtle deterministic pan from the the source camera micro-movement.
638 tl.to(
639 ".camera",
640 {
641 x: 3,
642 y: 1.6,
643 duration: 3.5,
644 ease: "sine.inOut",
645 },
646 0,
647 );
648
649 /* ----------------------------------------------------------------
650 CONTINUOUS INTERNAL SVG MOTION
651 All four icons' inner animations are derived from tl.time() in
652 a single shared onUpdate. Cheaper than 4-5 separate onUpdates,
653 easier to inspect.
654 ---------------------------------------------------------------- */
655 const SCISSOR_SPEED = 0.12 * 30; // = 3.6 rad/sec (sin freq)
656 const SCISSOR_AMP = 15;
657 const REC_OPACITY_SPEED = 0.15 * 30; // = 4.5 rad/sec
658 const REC_SCALE_SPEED = 0.1 * 30; // = 3.0 rad/sec
659 const REC_OPACITY_AMP = 0.3;
660 const REC_OPACITY_BASE = 0.7;
661 const REC_SCALE_AMP = 0.15;
662 const PLAY_PULSE_SPEED = 0.08 * 30; // = 2.4 rad/sec
663 const PLAY_PULSE_AMP = 0.08;
664 const CUTTING_FLOW_SPEED = 0.5 * 30; // = 15 units/sec (for stroke-dashoffset)
665
666 const scissorUpper = document.querySelector(".scissor-upper");
667 const scissorLower = document.querySelector(".scissor-lower");
668 const recordDot = document.querySelector(".record-dot");
669 const playTri = document.querySelector(".play-tri");
670 const cuttingLine = document.querySelector(".cutting-line");
671
672 tl.to(
673 { tick: 0 },
674 {
675 tick: 1,
676 duration: 3.5,
677 ease: "none",
678 onUpdate: function () {
679 const t = tl.time();
680
681 // Scissors — symmetric oscillation, anchored to scissors delay
682 const sciT = t - TIMING.scissorsEntryAt;
683 const sciAngle = sciT > 0 ? Math.sin(sciT * SCISSOR_SPEED) * SCISSOR_AMP : 0;
684 gsap.set(scissorUpper, { rotation: sciAngle });
685 gsap.set(scissorLower, { rotation: -sciAngle });
686
687 // Cutting line — linear dash flow (negative = leftward drift)
688 const cutT = t - TIMING.scissorsEntryAt;
689 if (cutT > 0) {
690 gsap.set(cuttingLine, {
691 attr: { "stroke-dashoffset": -cutT * CUTTING_FLOW_SPEED },
692 });
693 }
694
695 // Record dot — phase-offset opacity and scale
696 const recT = t - TIMING.videoEntryAt;
697 if (recT > 0) {
698 const recOpacity =
699 Math.sin(recT * REC_OPACITY_SPEED) * REC_OPACITY_AMP + REC_OPACITY_BASE;
700 const recScale = 1 + Math.sin(recT * REC_SCALE_SPEED) * REC_SCALE_AMP;
701 gsap.set(recordDot, { opacity: recOpacity, scale: recScale });
702 }
703
704 // Play triangle — gentle scale pulse
705 const playT = t - TIMING.playEntryAt;
706 if (playT > 0) {
707 const playScale = 1 + Math.sin(playT * PLAY_PULSE_SPEED) * PLAY_PULSE_AMP;
708 gsap.set(playTri, { scale: playScale });
709 }
710 },
711 },
712 0,
713 );
714
715 /* Clock minute hand — linear rotation, anchored to clock entry */
716 tl.fromTo(
717 ".clock-hand-min",
718 { rotation: 0, svgOrigin: "12 12" },
719 {
720 rotation: 420, // 120 deg/sec × 3.5s, scaled for visible motion
721 svgOrigin: "12 12",
722 duration: 3.5 - TIMING.clockEntryAt,
723 ease: "none",
724 },
725 TIMING.clockEntryAt,
726 );
727 </script>
728 </body>
729</html>