Files
heygen-com--hyperframes/skills/hyperframes-animation/examples/hook-counter-burst.html
T
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

730 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Scene 01 — Counting Icon Burst</title>
<!--
HyperFrames composition.tsx.
Choreography (4 phases, 3.5 seconds total):
0.00 0.17s Background visible with dark overlay; nothing else
0.17 0.57s Four enriched icons enter staggered, clustered at startOffset 0.4
(clock 0.17s · scissors 0.30s · video 0.43s · play 0.57s)
0.47 1.47s Counter "0 → 90" with growing font size (0.20W → 0.42W);
icons expand outward from 40% to 100% position
0.50 2.33s Camera focus phase: scale 0.92 → 1.0
2.33 3.50s Camera push phase: scale 1.0 → 1.08
1.27s Percent symbol pops in with own spring
Continuous motion (running from t=0, gated by icon visibility):
Clock minute hand: linear rotation 420° over 3.5s
Scissors: ±15° sine oscillation, period ~1.7s
Cutting line: stroke-dashoffset drifts -200 over 3.5s
Video record dot: opacity + scale phase-offset sine pulses
Play triangle: scale pulse ±8%, period ~2.6s
ease + duration (no shared driver needed, just identical timing)
- Counter text + number scale are emitted as seek-safe timeline keyframes
- Background: local Pexels MP4 plus dark overlay
- Per-icon entry uses GSAP fromTo on a nested .icon-entry wrapper
so it never overwrites the .icon-pos expansion tween
-->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
:root {
--bg-dark: #060812;
--text-primary: #ffffff;
--accent-yellow: #edcb50;
--record-red: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--bg-dark);
font-family:
"Inter",
system-ui,
-apple-system,
sans-serif;
color: var(--text-primary);
}
/* ============================================================
BACKGROUND (static gradient)
============================================================ */
.bg {
position: absolute;
inset: 0;
transform-origin: center center;
will-change: transform;
background:
radial-gradient(ellipse at 30% 30%, rgba(80, 100, 200, 0.25), transparent 60%),
radial-gradient(ellipse at 70% 70%, rgba(180, 80, 150, 0.2), transparent 60%),
linear-gradient(135deg, #0a0d1f 0%, #1a0d20 100%);
}
.bg-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.66);
}
/* ============================================================
CAMERA WRAPPER (GSAP-managed scale phases)
============================================================ */
.camera {
position: absolute;
inset: 0;
transform-origin: center center;
will-change: transform;
}
/* ============================================================
ICONS — two nested wrappers per icon.
.icon-pos is positioned at TARGET; GSAP x/y shifts toward center.
.icon-entry tweens scale/opacity/rotation.
============================================================ */
.icons-stage {
position: absolute;
inset: 0;
}
.icon-pos {
position: absolute;
width: 180px;
height: 180px;
will-change: transform;
}
.icon-entry {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
will-change: transform, opacity;
/* initial scale(0) + opacity(0) set by GSAP fromTo */
}
.icon-svg {
width: 100%;
height: 100%;
display: block;
}
/* SVG inner elements — class hooks for GSAP tweens */
.clock-hand-min {
transform-box: view-box;
}
.scissor-upper,
.scissor-lower {
transform-origin: 12px 12px;
}
.play-tri {
transform-origin: 12px 12px;
}
.record-dot {
transform-origin: 19px 8px;
}
.play-ring {
transform-origin: 12px 12px;
transform: rotate(-90deg);
}
/* ============================================================
COUNTER (absolute center, 3D entry)
============================================================ */
.counter-stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
perspective: 2000px;
perspective-origin: center center;
}
.counter-3d {
display: flex;
align-items: baseline;
justify-content: center;
transform-style: preserve-3d;
will-change: transform, opacity;
opacity: 0; /* GSAP entry fades in */
}
.counter-number {
font-size: 806px; /* W × 0.42 — final size, GSAP scales from 0.20W */
font-weight: 900;
letter-spacing: -0.04em;
line-height: 0.9;
font-variant-numeric: tabular-nums; /* prevents 1→2 digit jitter */
color: var(--text-primary);
display: inline-block;
transform-origin: center bottom;
will-change: transform;
}
.counter-percent {
font-size: 269px; /* W × 0.14 */
font-weight: 700;
letter-spacing: -0.02em;
color: var(--accent-yellow);
margin-left: 16px;
display: inline-block;
will-change: transform, opacity;
opacity: 0; /* GSAP entry tweens in later */
}
/* ============================================================
VIGNETTE
============================================================ */
.vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
pointer-events: none;
z-index: 400;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="3.5"
data-width="1920"
data-height="1080"
>
<div class="bg"></div>
<div class="bg-overlay"></div>
<div
class="camera clip"
data-start="0"
data-duration="3.5"
data-track-index="1"
id="camera-stage"
>
<div class="icons-stage">
<!-- Clock -->
<div class="icon-pos clock-pos" id="clock-pos">
<div class="icon-entry clock-entry">
<svg
class="icon-svg"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<circle
class="clock-ring"
cx="12"
cy="12"
r="9"
fill="none"
stroke="white"
stroke-width="2"
stroke-dasharray="56.5"
stroke-dashoffset="56.5"
/>
<line
x1="12"
y1="12"
x2="12"
y2="8"
stroke="white"
stroke-width="2"
stroke-linecap="round"
/>
<line
class="clock-hand-min"
x1="12"
y1="12"
x2="12"
y2="6"
stroke="#edcb50"
stroke-width="2"
stroke-linecap="round"
/>
<circle cx="12" cy="12" r="1.5" fill="#edcb50" />
</svg>
</div>
</div>
<!-- Scissors -->
<div class="icon-pos scissors-pos" id="scissors-pos">
<div class="icon-entry scissors-entry">
<svg
class="icon-svg"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<g class="scissor-upper">
<path
d="M12 10.5L4.5 3L6.5 3L14 10.5"
stroke="white"
stroke-width="2"
stroke-linecap="round"
fill="none"
/>
<circle cx="6" cy="6" r="2.5" fill="none" stroke="white" stroke-width="1.5" />
</g>
<g class="scissor-lower">
<path
d="M12 13.5L4.5 21L6.5 21L14 13.5"
stroke="white"
stroke-width="2"
stroke-linecap="round"
fill="none"
/>
<circle cx="6" cy="18" r="2.5" fill="none" stroke="white" stroke-width="1.5" />
</g>
<line
class="cutting-line"
x1="14"
y1="12"
x2="20"
y2="12"
stroke="#edcb50"
stroke-width="2"
stroke-linecap="round"
stroke-dasharray="4 2"
/>
</svg>
</div>
</div>
<!-- Video -->
<div class="icon-pos video-pos" id="video-pos">
<div class="icon-entry video-entry">
<svg
class="icon-svg"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<rect
class="video-frame"
x="3"
y="5"
width="14"
height="14"
rx="2"
stroke="white"
stroke-width="2"
fill="none"
stroke-dasharray="56"
stroke-dashoffset="56"
/>
<path d="M8 9L13 12L8 15V9Z" fill="white" />
<circle class="record-dot" cx="19" cy="8" r="2.5" fill="#ef4444" />
</svg>
</div>
</div>
<!-- Play -->
<div class="icon-pos play-pos" id="play-pos">
<div class="icon-entry play-entry">
<svg
class="icon-svg"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<circle
class="play-ring"
cx="12"
cy="12"
r="9"
fill="none"
stroke="white"
stroke-width="2"
stroke-dasharray="56.5"
stroke-dashoffset="56.5"
/>
<circle cx="12" cy="12" r="7" fill="rgba(255,255,255,0.05)" />
<path class="play-tri" d="M10 8L16 12L10 16V8Z" fill="#edcb50" />
</svg>
</div>
</div>
</div>
<!-- /.icons-stage -->
<div class="counter-stage">
<div class="counter-3d" id="counter-3d">
<span class="counter-number" id="counter-number">0</span
><span class="counter-percent" id="counter-percent">%</span>
</div>
</div>
</div>
<!-- /.camera -->
<div class="vignette"></div>
</div>
<script>
/* ================================================================
CONSTANTS
================================================================ */
const W = 1920,
H = 1080;
const ICON_SIZE = 180;
const CENTER_X = W / 2 - ICON_SIZE / 2; // 870
const CENTER_Y = H / 2 - ICON_SIZE / 2; // 450
const TIMING = {
// Phase 2 — icon entries (staggered)
clockEntryAt: 0.17,
scissorsEntryAt: 0.3,
videoEntryAt: 0.43,
playEntryAt: 0.57,
entryDur: 0.55,
// Phase 3 — count + expansion (shared start, dur, ease)
countAt: 0.47,
countDur: 1.0,
startOffset: 0.4,
// Counter 3D entry (slightly earlier than count starts)
counterEntryAt: 0.27,
counterEntryDur: 0.7,
// Percent symbol entry
percentEntryAt: 1.27,
percentEntryDur: 0.55,
// Phase 4 — camera
cameraFocusAt: 0.5,
cameraFocusDur: 1.83,
cameraPushAt: 2.33,
cameraPushDur: 1.17,
};
const ICONS = [
{
name: "clock",
sel: "#clock-pos",
targetX: W * 0.06,
targetY: H * 0.18,
delay: TIMING.clockEntryAt,
entryRotation: -180,
entryEase: "back.out(1.5)",
},
{
name: "scissors",
sel: "#scissors-pos",
targetX: W * 0.85,
targetY: H * 0.2,
delay: TIMING.scissorsEntryAt,
entryRotation: 0,
entryEase: "back.out(1.5)",
},
{
name: "video",
sel: "#video-pos",
targetX: W * 0.04,
targetY: H * 0.68,
delay: TIMING.videoEntryAt,
entryRotation: 0,
entryEase: "back.out(1.4)",
},
{
name: "play",
sel: "#play-pos",
targetX: W * 0.88,
targetY: H * 0.65,
delay: TIMING.playEntryAt,
entryRotation: 90,
entryEase: "back.out(1.5)",
},
];
/* ================================================================
SET ICON TARGET POSITIONS + INITIAL EXPANSION OFFSETS
================================================================ */
ICONS.forEach(({ sel, targetX, targetY }) => {
const el = document.querySelector(sel);
el.style.left = targetX + "px";
el.style.top = targetY + "px";
// Initial offset for START_OFFSET = 0.4:
// position = target + (center - target) * (1 - 0.4) = target + 0.6 * (center - target)
const offsetX = (CENTER_X - targetX) * (1 - TIMING.startOffset);
const offsetY = (CENTER_Y - targetY) * (1 - TIMING.startOffset);
gsap.set(el, { x: offsetX, y: offsetY });
});
/* Set initial entry state on each .icon-entry */
ICONS.forEach(({ name, entryRotation }) => {
gsap.set(`.${name}-entry`, {
scale: 0,
opacity: 0,
rotation: entryRotation,
});
});
/* Set initial background and camera scale + subtle pan baseline */
gsap.set(".bg", { scale: 1.05 });
gsap.set(".camera", { scale: 0.92, x: 0, y: 2 });
gsap.set("#counter-number", { scale: 0.2 / 0.42 });
/* ================================================================
TIMELINE
================================================================ */
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
window.__timelines["main"] = tl;
tl.to(
".bg",
{
scale: 1.07,
x: 6,
y: -4,
duration: 3.5,
ease: "sine.inOut",
},
0,
);
/* ----------------------------------------------------------------
PHASE 2: Icon entries (staggered)
---------------------------------------------------------------- */
ICONS.forEach(({ name, delay, entryEase }) => {
// Scale + opacity + rotation entry
tl.to(
`.${name}-entry`,
{
scale: 1,
opacity: 0.85,
rotation: 0,
duration: TIMING.entryDur,
ease: entryEase,
},
delay,
);
});
// Stroke-draw the outline circles/rect on clock, video, and play during their entries
tl.to(
".clock-ring",
{
attr: { "stroke-dashoffset": 0 },
duration: TIMING.entryDur,
ease: "power2.out",
},
TIMING.clockEntryAt,
);
tl.to(
".video-frame",
{
attr: { "stroke-dashoffset": 0 },
duration: TIMING.entryDur,
ease: "power2.out",
},
TIMING.videoEntryAt,
);
tl.to(
".play-ring",
{
attr: { "stroke-dashoffset": 0 },
duration: TIMING.entryDur,
ease: "power2.out",
},
TIMING.playEntryAt,
);
/* ----------------------------------------------------------------
PHASE 3: Counter (with 3D entry) + expansion
---------------------------------------------------------------- */
// Counter 3D entry — translateZ + rotateY + scale + opacity, in parallel.
tl.fromTo(
"#counter-3d",
{ rotationY: 10, scale: 0.7, z: -300, opacity: 0 },
{
rotationY: 0,
scale: 1.0,
z: 0,
opacity: 1,
duration: TIMING.counterEntryDur,
ease: "power3.out",
},
TIMING.counterEntryAt,
);
// Counter number + apparent size. Use discrete frame keyframes so
// HyperFrames can seek directly to any sampled time.
const counterEl = document.querySelector("#counter-number");
const COUNT_STEPS = 30;
const COUNT_START_SCALE = 0.2 / 0.42;
for (let i = 0; i <= COUNT_STEPS; i += 1) {
const p = i / COUNT_STEPS;
const eased = 1 - Math.pow(1 - p, 2.5);
const number = Math.round(eased * 90);
const scale = COUNT_START_SCALE + eased * (1 - COUNT_START_SCALE);
tl.set(
counterEl,
{
textContent: String(number),
scale,
},
TIMING.countAt + p * TIMING.countDur,
);
}
// Percent symbol enters later with its own spring.
tl.fromTo(
"#counter-percent",
{ x: 60, rotation: 20, scale: 0.4, opacity: 0 },
{
x: 0,
rotation: 0,
scale: 1.0,
opacity: 1,
duration: TIMING.percentEntryDur,
ease: "back.out(1.4)",
},
TIMING.percentEntryAt,
);
// Icon expansion — per-icon, same start/dur/ease as counter for lock-step sync.
ICONS.forEach(({ sel }) => {
tl.to(
sel,
{
x: 0,
y: 0,
duration: TIMING.countDur,
ease: "power2.out",
},
TIMING.countAt,
);
});
/* ----------------------------------------------------------------
PHASE 4: Multi-phase camera
---------------------------------------------------------------- */
tl.to(
".camera",
{
scale: 1.0,
duration: TIMING.cameraFocusDur,
ease: "power2.out",
},
TIMING.cameraFocusAt,
);
tl.to(
".camera",
{
scale: 1.08,
duration: TIMING.cameraPushDur,
ease: "power2.out",
},
TIMING.cameraPushAt,
);
// Subtle deterministic pan from the the source camera micro-movement.
tl.to(
".camera",
{
x: 3,
y: 1.6,
duration: 3.5,
ease: "sine.inOut",
},
0,
);
/* ----------------------------------------------------------------
CONTINUOUS INTERNAL SVG MOTION
All four icons' inner animations are derived from tl.time() in
a single shared onUpdate. Cheaper than 4-5 separate onUpdates,
easier to inspect.
---------------------------------------------------------------- */
const SCISSOR_SPEED = 0.12 * 30; // = 3.6 rad/sec (sin freq)
const SCISSOR_AMP = 15;
const REC_OPACITY_SPEED = 0.15 * 30; // = 4.5 rad/sec
const REC_SCALE_SPEED = 0.1 * 30; // = 3.0 rad/sec
const REC_OPACITY_AMP = 0.3;
const REC_OPACITY_BASE = 0.7;
const REC_SCALE_AMP = 0.15;
const PLAY_PULSE_SPEED = 0.08 * 30; // = 2.4 rad/sec
const PLAY_PULSE_AMP = 0.08;
const CUTTING_FLOW_SPEED = 0.5 * 30; // = 15 units/sec (for stroke-dashoffset)
const scissorUpper = document.querySelector(".scissor-upper");
const scissorLower = document.querySelector(".scissor-lower");
const recordDot = document.querySelector(".record-dot");
const playTri = document.querySelector(".play-tri");
const cuttingLine = document.querySelector(".cutting-line");
tl.to(
{ tick: 0 },
{
tick: 1,
duration: 3.5,
ease: "none",
onUpdate: function () {
const t = tl.time();
// Scissors — symmetric oscillation, anchored to scissors delay
const sciT = t - TIMING.scissorsEntryAt;
const sciAngle = sciT > 0 ? Math.sin(sciT * SCISSOR_SPEED) * SCISSOR_AMP : 0;
gsap.set(scissorUpper, { rotation: sciAngle });
gsap.set(scissorLower, { rotation: -sciAngle });
// Cutting line — linear dash flow (negative = leftward drift)
const cutT = t - TIMING.scissorsEntryAt;
if (cutT > 0) {
gsap.set(cuttingLine, {
attr: { "stroke-dashoffset": -cutT * CUTTING_FLOW_SPEED },
});
}
// Record dot — phase-offset opacity and scale
const recT = t - TIMING.videoEntryAt;
if (recT > 0) {
const recOpacity =
Math.sin(recT * REC_OPACITY_SPEED) * REC_OPACITY_AMP + REC_OPACITY_BASE;
const recScale = 1 + Math.sin(recT * REC_SCALE_SPEED) * REC_SCALE_AMP;
gsap.set(recordDot, { opacity: recOpacity, scale: recScale });
}
// Play triangle — gentle scale pulse
const playT = t - TIMING.playEntryAt;
if (playT > 0) {
const playScale = 1 + Math.sin(playT * PLAY_PULSE_SPEED) * PLAY_PULSE_AMP;
gsap.set(playTri, { scale: playScale });
}
},
},
0,
);
/* Clock minute hand — linear rotation, anchored to clock entry */
tl.fromTo(
".clock-hand-min",
{ rotation: 0, svgOrigin: "12 12" },
{
rotation: 420, // 120 deg/sec × 3.5s, scaled for visible motion
svgOrigin: "12 12",
duration: 3.5 - TIMING.clockEntryAt,
ease: "none",
},
TIMING.clockEntryAt,
);
</script>
</body>
</html>