
Hyperframes Animation
- 223k installs
- 39.5k repo stars
- Updated August 5, 2026
- heygen-com/hyperframes
hyperframes-animation is a HyperFrames skill teaching seekable animation patterns for deterministic video composition across runtime adapters.
About
Teach agents and developers seekable animation patterns for video composition using multiple runtime adapters. The skill covers atomic animation rules, runtime-specific implementations, and frame-accurate authoring for deterministic video rendering.
- Atomic motion rules and runtime adapters for GSAP, Lottie, Three.js, Anime.js, CSS, and WAAPI
- Seek-safe keyframe authoring for frame-accurate animation in video composition
- Scene blueprints, transitions, and sub-composition support
Hyperframes Animation by the numbers
- 223,306 all-time installs (skills.sh)
- +36,763 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #43 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
hyperframes-animation capabilities & compatibility
- Capabilities
- animation authoring · runtime adaptation · keyframe control
- Use cases
- video generation
npx skills add https://github.com/heygen-com/hyperframes --skill hyperframes-animationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 223k |
|---|---|
| repo stars | ★ 39.5k |
| Security audit | 1 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | heygen-com/hyperframes ↗ |
How do you build seek-safe animations for HyperFrames HTML video?
Enable creation of frame-accurate animations for video composition across multiple animation runtimes.
Who is it for?
Building animated video compositions with frame accuracy and multi-runtime support.
Skip if: Static documents, non-HyperFrames CSS-only sites, or narrated multi-minute brand reels better handled by general-video.
When should I use this skill?
Creating animations for HyperFrames video compositions
What you get
Atomic motion rules, scene blueprints, transitions, and runtime-specific animation code for HyperFrames compositions.
- Scene blueprints
- Runtime-specific animation snippets
- Composable atomic motion rules
By the numbers
- Covers seven runtime adapters including GSAP, Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, and TypeGPU
Files
HyperFrames Animation
All motion knowledge in one skill: rules (atomic recipes), blueprints (multi-phase scene templates), transitions (scene-to-scene), techniques (broader motion-design patterns), and adapters (per-runtime APIs).
For the composition contract (data attributes, sub-compositions, determinism) see hyperframes-core.
Default: compose atomic rules
Pick 2-4 rules from rules-index.md, glue them together with a single paused GSAP timeline, done. This is faster and produces less code than starting from a blueprint.
Load a blueprint when
- The scene matches an existing pre-designed multi-phase template (brand-reveal, social-proof, demo-page-scroll-spotlight, etc.) and reusing its phase pipeline saves real authoring time
- You want runnable ground-truth code for a complex 4-5 phase choreography
Blueprints live in blueprints-index.md. Each entry points to blueprints/<id>.md (recipe) and examples/<id>.html (runnable sample). Do not read it speculatively; load it when you've already decided you need scene-level orchestration.
Routing
| Want to… | Read |
|---|---|
| Pick an atomic motion pattern by trigger / tag | rules-index.md |
| Read one rule's full HTML / CSS / GSAP recipe | rules/<name>.md |
| Pick a multi-phase scene template | blueprints-index.md |
| Read one blueprint's full recipe | blueprints/<id>.md + examples/<id>.html |
| Author a scene transition (CSS-driven, between two clips) | transitions/overview.md, transitions/catalog.md |
| Look up a broader motion-design technique | techniques.md |
| Analyze an existing composition's animation map | scripts/animation-map.mjs |
| GSAP API — timeline / tweens / position parameters | adapters/gsap.md |
| GSAP — drop-in effect recipes | rules/gsap-effects.md |
| GSAP — transforms / perf | adapters/gsap-transforms-and-perf.md |
| GSAP — eases / stagger | adapters/gsap-easing-and-stagger.md |
| GSAP — timeline / labels | adapters/gsap-timeline-and-labels.md |
Lottie / dotLottie (After Effects exports, window.__hfLottie) | adapters/lottie.md |
Three.js / WebGL (3D scenes, AnimationMixer, hf-seek) | adapters/three.md |
Anime.js (window.__hfAnime) | adapters/animejs.md |
CSS keyframes (animation-delay / play-state / fill-mode) | adapters/css-animations.md |
Web Animations API (element.animate(), currentTime seek) | adapters/waapi.md |
TypeGPU / WebGPU (navigator.gpu, WGSL, compute pipelines) | adapters/typegpu.md |
HTML-as-texture + WebGL/GLSL post-fx (capture live DOM via drawElementImage) | adapters/html-in-canvas-patterns.md |
Named text-animation effects (24 IDs via external animate-text skill) | adapters/animate-text.md |
Picking a runtime
- GSAP is the default for 95% of motion work — covers timeline orchestration, transforms, easing, stagger. All atomic rules in this skill are GSAP-based.
- Lottie when an asset has its own pre-baked timeline (typically After Effects exports).
- Three.js for 3D scenes, camera motion, shader-driven visuals.
- Anime.js for lightweight tweening when GSAP is overkill.
- CSS for simple repeated motifs, decoration, shimmer — no JavaScript animation cost.
- WAAPI for native browser keyframes without a GSAP dependency.
- TypeGPU / WebGPU for GPU-rendered canvases (particles, liquid glass, custom shaders).
Multiple runtimes can coexist in one composition. Each registers its instances on the runtime-specific global so HyperFrames can seek all of them in one pass.
Critical Constraints
Prerequisite: `hyperframes-core` → Non-Negotiable Rules (single paused timeline, data-duration governs length, no Math.random / Date.now / performance.now, no repeat: -1, no gsap.set on later-scene clips, no display / visibility animation, no timeline construction inside async / setTimeout / Promise). Don't restate those here.
Animation-craft additions on top of core's contract:
- Pre-calculated layout constants — never derive positions from
getBoundingClientRect()at tween time. Tween-time DOM measurements desync because the renderer samples in parallel; compute coordinates once at composition setup and reuse. - Spatial motion uses GSAP transform aliases only (
x,y,scale,rotation). Core's allowlist also permitsopacity/color/backgroundColor/borderRadiusfor non-spatial property tweens — but neverwidth/height/top/leftfor layout changes.
Scripts
node skills/hyperframes-animation/scripts/animation-map.mjs <composition-dir> \
--out <composition-dir>/.hyperframes/anim-mapReads every GSAP timeline registered on window.__timelines, enumerates tweens, samples bboxes, computes flags, outputs animation-map.json. Use it to audit choreography (dead zones, stagger consistency, lifecycle warnings) after authoring.
See Also
hyperframes-core— composition structure, data attributes, sub-compositions, deterministic render contracthyperframes-creative— palettes, typography, narration, beat planning (non-animation creative direction)hyperframes-cli—npx hyperframes lint / validate / inspect / preview / render
Text Effects — Reference
For deterministic text-animation specs (e.g., typewriter at exact 240ms / 46ms stagger / steps(1, end) easing), this skill defers to the separate `animate-text` skill maintained by Pixel Point at github.com/pixel-point/animate-text. It provides a catalog of 24 named text effects with portable contracts and per-library implementation recipes (GSAP, Anime.js, WAAPI).
We do NOT ship the catalog inside this repo. Pixel Point's animate-text is the source of truth; vendoring its files here would violate the upstream's licensing (no explicit license declared upstream as of this writing). Loading the skill separately keeps the legal picture clean while giving you the same catalog.
How to use it
When a beat needs a deterministic text animation, load the upstream skill alongside this one:
# In your project root, install the upstream skill into .agents/skills/
npx skills add pixel-point/animate-textOr in a skill-aware agent runtime, the skill is invoked by name:
/animate-textOnce installed, the specs live at:
.agents/skills/animate-text/assets/effects/<id>.json # per-library implementation recipe
.agents/skills/animate-text/assets/specs/<id>.json # portable motion contractSub-agents reading those files get exact GSAP timings, easing strings, DOM split rules, and stagger algorithms — no creative invention needed.
When you don't need the upstream skill
If a beat's text animation is simple enough to describe in prose ("headline fades up word-by-word, 80ms stagger"), implement it inline using the GSAP knowledge already in these skills (hyperframes-creative → references/motion-principles.md and references/beat-direction.md; hyperframes-animation → techniques.md, entry #4 "Per-Word Kinetic Typography"). The upstream catalog is most valuable when:
- You want a specific NAMED effect across multiple beats (so they feel like one design system, not one-offs)
- You're choosing between several similar effects (typewriter vs per-character-rise vs bottom-up-letters) and want to see all 24 in one place
- You need layout-aware effects (
kinetic-center-build,short-slide-right,short-slide-down) where parameters alone aren't enough — those ship with custom layout algorithms
Effect names — vocabulary (do NOT use this as the implementation source)
For convenience while writing storyboards: the upstream skill provides 24 effects. Their IDs are listed here so you can name them in STORYBOARD.md even before loading the upstream skill. The implementation specs are in the upstream skill, not here.
- Per-character (7): soft-blur-in, per-character-rise, typewriter, bottom-up-letters, top-down-letters, stagger-from-center, stagger-from-edges
- Per-word (8): per-word-crossfade, spring-scale-in, shared-axis-y, blur-out-up, kinetic-center-build, short-slide-right, short-slide-down, depth-parallax-words
- Per-line (2): mask-reveal-up, line-by-line-slide
- Whole element (7): micro-scale-fade, shimmer-sweep, fade-through, shared-axis-z, scale-down-fade, focus-blur-resolve, shared-axis-x
For descriptions, durations, easing curves, and the per-library recipes: load /animate-text and read its own catalog page.
In the storyboard
Every text element in every beat can name an effect by ID, e.g.:
**Text Animations:**
- Main headline: `kinetic-center-build`
- Eyebrow label: `soft-blur-in`
- Body copy 3 lines: `mask-reveal-up`Sub-agents implementing the beat will load /animate-text if it's not already loaded, then read the spec for each named effect from the upstream skill's files.
If the upstream skill isn't available (offline build, network restrictions, agent runtime that doesn't support skill loading), sub-agents fall back to implementing the effect from the description alone — using GSAP knowledge plus the effect ID as a description of intent (e.g., "typewriter" = per-character stepped reveal with no interpolation).
Anime.js for HyperFrames
HyperFrames can seek Anime.js instances through its animejs runtime adapter. The composition owns the animation objects; HyperFrames owns the clock.
Contract
- Create animations or timelines synchronously during composition initialization.
- Set
autoplay: falseso Anime.js does not advance on its own clock. - Register every returned animation or timeline on
window.__hfAnime. - Use finite durations and loop counts.
- Avoid callbacks that mutate DOM based on wall-clock time, network state, or unseeded randomness.
The adapter seeks every registered instance with instance.seek(timeMs), where timeMs is HyperFrames time in milliseconds.
Basic Pattern
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
<script>
const anim = anime({
targets: ".mark",
translateX: 280,
rotate: "1turn",
opacity: [0, 1],
duration: 1200,
easing: "easeOutExpo",
autoplay: false,
});
window.__hfAnime = window.__hfAnime || [];
window.__hfAnime.push(anim);
</script>Timeline Pattern
<script>
const tl = anime.timeline({
autoplay: false,
easing: "easeOutCubic",
});
tl.add({
targets: ".title",
translateY: [40, 0],
opacity: [0, 1],
duration: 650,
}).add(
{
targets: ".accent",
scaleX: [0, 1],
duration: 450,
},
250,
);
window.__hfAnime = window.__hfAnime || [];
window.__hfAnime.push(tl);
</script>Module Builds
If you use an ES module build, the adapter does not care how the instance was created. It only needs the returned object to expose seek(), pause(), and preferably play():
<script type="module">
import { animate } from "https://cdn.jsdelivr.net/npm/animejs/+esm";
const anim = animate(".chip", {
x: "18rem",
duration: 900,
autoplay: false,
});
window.__hfAnime = window.__hfAnime || [];
window.__hfAnime.push(anim);
</script>Good Uses
- Small SVG and DOM flourishes where Anime.js syntax is compact.
- Imported Anime.js examples that can be made seek-driven.
- Multiple independent micro-animations pushed into the same registry.
Use GSAP for complex scene sequencing unless the user specifically asks for Anime.js. GSAP is still the primary HyperFrames authoring path.
Avoid
- Leaving
autoplayat the Anime.js default. - Depending on
anime.runningauto-discovery instead of explicitwindow.__hfAnime.push(...). - Infinite loops. Compute a finite repeat count from the composition duration.
- Building animations in timers, promises, event handlers, or after async asset loads.
Validation
After editing a composition that uses Anime.js:
npx hyperframes lint
npx hyperframes validateCredits And References
- HyperFrames adapter source:
packages/core/src/runtime/adapters/animejs.ts. - Anime.js documentation for
autoplay,pause(), andseek(): https://animejs.com/documentation/
CSS Animations for HyperFrames
HyperFrames can seek CSS keyframe animations through its css runtime adapter. Use this for simple repeated motifs, background motion, shimmer, glow, masks, and non-sequenced decoration.
For scene choreography, GSAP is usually clearer. CSS animations work best when the motion belongs to one element and has a fixed duration.
Contract
- Put the animated element in the DOM before runtime initialization finishes.
- Give timed elements a
data-startvalue so local animation time matches the clip. - Use finite
animation-durationandanimation-iteration-countbecause the negative-delay fallback cannot represent unbounded duration in environments without WAAPI-backed CSS animations. - Prefer
animation-fill-mode: bothso seeked states hold before and after active motion. - Avoid wall-clock JavaScript, hover-triggered state, and class toggles that depend on user events.
The adapter discovers elements with computed animation-name, seeks their browser Animation handles when available, and falls back to pausing with negative animation-delay.
Basic Pattern
<div
id="pulse-ring"
class="clip pulse-ring"
data-start="0"
data-duration="4"
data-track-index="2"
></div>
<style>
.pulse-ring {
width: 280px;
height: 280px;
border: 4px solid rgba(255, 255, 255, 0.7);
border-radius: 50%;
animation-name: pulse-ring;
animation-duration: 1200ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
animation-iteration-count: 3;
animation-fill-mode: both;
}
@keyframes pulse-ring {
from {
opacity: 0;
transform: scale(0.82);
}
35% {
opacity: 1;
}
to {
opacity: 0;
transform: scale(1.18);
}
}
</style>Stagger Pattern
Use CSS custom properties to avoid duplicating keyframes:
<div class="clip dots" data-start="1" data-duration="3" data-track-index="3">
<span style="--i: 0"></span>
<span style="--i: 1"></span>
<span style="--i: 2"></span>
</div>
<style>
.dots span {
display: inline-block;
width: 18px;
height: 18px;
margin-right: 10px;
border-radius: 50%;
background: currentColor;
animation: dot-pop 900ms ease-out both;
animation-delay: calc(var(--i) * 120ms);
}
@keyframes dot-pop {
from {
opacity: 0;
transform: translateY(18px) scale(0.75);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style>Good Uses
- Decorative loops with a known repeat count.
- Mask, glow, shimmer, grain, and subtle parallax layers.
- Simple one-element entrances where a full JS timeline would be excessive.
Avoid
- Infinite CSS animations unless you have verified the browser exposes seekable WAAPI-backed CSS animation handles. Prefer a finite iteration count covering the visible duration.
- Animating layout properties like
top,left,width, orheightwhen transforms work. - Relying on hover, focus, scroll, or media queries to trigger render-critical motion.
- Changing animation classes after startup unless another deterministic timeline controls that change.
Validation
After editing CSS animation compositions:
npx hyperframes lint
npx hyperframes validateCredits And References
- HyperFrames adapter source:
packages/core/src/runtime/adapters/css.ts. - MDN CSS animation documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation
- MDN
animation-fill-mode: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-fill-mode
Easing, Stagger, and Function-Based Values
Easing
Built-in eases: power1, power2, power3, power4, back, bounce, circ, elastic, expo, sine, none.
Each has .in, .out, .inOut variants.
| Ease | Use for |
|---|---|
power1.out, power2.out | Standard UI motion. Default for most entrances. |
power3.out, power4.out | Punchier deceleration. Title cards, hero reveals. |
sine.inOut | Long, slow, calm motion. Crossfades, ambient drift. |
back.out(1.7) | Slight overshoot. Playful entrances. The arg controls overshoot amount. |
elastic.out(1, 0.3) | Springy bounce. First arg = amplitude, second = period. |
expo.inOut | Snappy, dramatic. Quick transitions between hero scenes. |
none (linear) | Camera moves with timed counterpoint, mechanical motion. |
Pick .out for entrances, .in for exits, .inOut for symmetric moves and continuous motion.
Easing Vocabulary (character & mood)
Easings are tone of voice: a video that only whispers is boring; one that varies between whisper, normal, and punch is engaging. Every composition should use at least 3 different easings — power2.out for everything produces flat, monotonous motion.
The full palette by character (each family has .in, .out, .inOut variants):
| Family | Character | Typical use |
|---|---|---|
power1–power4 | Gentle (1) to aggressive (4) acceleration curves | General purpose. power2 is the workhorse, power4 for dramatic snaps |
back(N) | Overshoot then settle. N controls how far past the target (1=subtle, 4=wild) | Logo reveals, badge pops, card entrances. back.out(2.5) for playful, back.out(1.2) for elegant |
elastic(amp, freq) | Spring bounce. amp=magnitude, freq=oscillation speed | Panel scatter, energetic drops, fun reveals |
bounce | Ball-drop bouncing | Physical interactions, icons landing, score counters |
expo | Extreme acceleration curve (much steeper than power4) | Premium/luxury reveals, dramatic entrances |
sine | Smooth, organic, no hard edges | Ambient float, breathing, Ken Burns, anything that loops. .inOut for yoyo motion |
circ | Circular acceleration (starts very fast, ends very gentle or vice versa) | Camera moves, scene transitions, orbital motion |
steps(N) | Discrete N-step jumps, no interpolation | Typing effects, cursor blink, counter ticks, retro/digital aesthetics |
Mood mapping: Match easing character to the beat's emotional content. Smooth/organic easings (sine, power1) feel contemplative and drifting. Aggressive deceleration (power4.out, expo.out) feels snappy and confident. Spring overshoot (back.out) feels bouncy and physical. The storyboard's mood description should guide which character fits — not a formula.
Defaults
const tl = gsap.timeline({
paused: true,
defaults: { duration: 0.6, ease: "power2.out" },
});Or globally:
gsap.defaults({ duration: 0.6, ease: "power2.out" });Setting defaults at timeline scope is preferred — it documents the motion language of that composition in one place.
Stagger
gsap.fromTo(".item", { y: 24, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, stagger: 0.08 });Object form:
gsap.fromTo(
".item",
{ y: 24, opacity: 0 },
{
y: 0,
opacity: 1,
stagger: {
each: 0.08, // delay between each
from: "center", // "start" | "end" | "center" | "edges" | "random" | index
amount: 0.6, // total stagger time (overrides each if both set)
grid: "auto", // for 2D stagger
axis: "x" | "y",
},
},
);Prefer stagger over N separate tweens with manual delays — it stays correct when the target count or order changes. Use fromTo() rather than from() so the start state is explicit (see gsap-timeline-and-labels.md → sub-composition entrances).
Function-Based Values
Any var can be a function (index, target, targets) => value:
gsap.to(".item", {
x: (i, target, targets) => i * 50,
rotation: (i) => (i % 2 === 0 ? 5 : -5),
stagger: 0.1,
});Use this for per-element values that depend on index, attributes, or measured size. Cheaper and more idiomatic than building tweens in a loop.
gsap.matchMedia (preview only)
matchMedia runs setup only when a media query matches and auto-reverts when it stops matching. It is useful for preview in the browser at different viewport sizes, and for prefers-reduced-motion. It is not a substitute for rendering at the composition's actual data-width/data-height — HyperFrames renders at a fixed viewport.
let mm = gsap.matchMedia();
mm.add(
{
isDesktop: "(min-width: 800px)",
reduceMotion: "(prefers-reduced-motion: reduce)",
},
(context) => {
const { isDesktop, reduceMotion } = context.conditions;
gsap.to(".box", {
rotation: isDesktop ? 360 : 180,
duration: reduceMotion ? 0 : 2,
});
},
);Timelines and Labels
HyperFrames is a seek-driven runtime. Build one paused timeline per composition, attach it to window.__timelines["<composition-id>"], and let HyperFrames seek it. Never call .play() for render-critical motion.
Creating a Timeline
const tl = gsap.timeline({
paused: true,
defaults: { duration: 0.5, ease: "power2.out" },
});
tl.to(".a", { x: 100 }).to(".b", { y: 50 }).to(".c", { opacity: 0 });Timeline options:
- paused: true — required in HyperFrames. The framework drives the playhead.
- repeat, yoyo — apply to the whole timeline.
repeat: -1is forbidden; use finite counts. - defaults — vars merged into every child tween. Use this instead of repeating
easeanddurationon every line.
Position Parameter
The third argument to .to()/.from()/.fromTo() controls placement on the timeline:
| Form | Meaning |
|---|---|
0, 1.5 | Absolute time in seconds |
"+=0.5" | 0.5s after the end of the timeline |
"-=0.2" | 0.2s before the end of the timeline |
"intro" | At the intro label |
"intro+=0.3" | 0.3s after the intro label |
"<" | Same start as the previous tween |
">" | Right after the previous tween ends |
"<0.2" | 0.2s after the previous tween starts |
">-0.1" | 0.1s before the previous tween ends |
tl.to(".a", { x: 100 }, 0);
tl.to(".b", { y: 50 }, "<"); // same start as .a
tl.to(".c", { opacity: 0 }, "<0.2"); // 0.2s after .b startsPrefer the position parameter over delay: — it composes naturally and survives refactors that re-order tweens.
Labels
tl.addLabel("intro", 0);
tl.to(".a", { x: 100 }, "intro");
tl.addLabel("outro", "+=0.5");
tl.to(".a", { opacity: 0 }, "outro");Labels make a long timeline readable and let multiple tweens converge on the same beat without re-typing absolute times.
Nesting Timelines
const master = gsap.timeline({ paused: true });
const child = gsap.timeline();
child.to(".a", { x: 100 }).to(".b", { y: 50 });
master.add(child, 0);In HyperFrames, do not nest sub-composition timelines into the host. Sub-compositions loaded via data-composition-src are seeked independently by HyperFrames from their own data-start. Nesting is only for grouping pieces of the _same_ composition's timeline.
Inside Sub-Compositions: prefer fromTo over from
For entrance tweens inside a sub-composition, prefer gsap.fromTo() over gsap.from():
// Sub-composition entrance — survives re-seek cleanly
tl.fromTo(".title", { y: 60, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6 }, 0.2);Why: HyperFrames re-seeks the sub-composition every time its host clip becomes visible. gsap.from() snapshots the starting state at registration time (page load); when the playhead jumps back past data-start, that snapshot can desync from the actual CSS state and the element renders in the wrong position. gsap.fromTo() declares both endpoints explicitly, so the seek-back always produces the same start state.
In top-level (standalone) compositions either form works — there's no re-seek-through-mount cycle.
Playback Control (debug / preview only)
tl.play();
tl.pause();
tl.reverse();
tl.restart();
tl.time(2);
tl.progress(0.5);
tl.kill();These are useful when previewing in the browser. In rendered output HyperFrames calls seek() internally — your timeline must produce identical state for the same time value every time it is seeked.
Transforms and Performance
Transform Aliases
Prefer GSAP's transform aliases over raw transform strings:
| GSAP property | Equivalent |
|---|---|
x, y, z | translateX/Y/Z (px) |
xPercent, yPercent | translateX/Y in % |
scale, scaleX, scaleY | scale |
rotation | rotate (deg) |
rotationX, rotationY | 3D rotate |
skewX, skewY | skew |
transformOrigin | transform-origin |
Aliases let GSAP track and interpolate each axis independently, which prevents accidental overwrites between separate tweens on the same element.
autoAlpha
Prefer autoAlpha over opacity for show/hide:
gsap.to(".panel", { autoAlpha: 0, duration: 0.4 });autoAlpha: 0 sets both opacity: 0 and visibility: hidden, which removes the element from hit-testing and accessibility tree at zero alpha — closer to "gone" than plain opacity: 0.
clearProps
Removes inline styles set by GSAP when the tween completes:
gsap.to(".item", { x: 100, rotation: 45, clearProps: "all" });
gsap.to(".item", { x: 100, rotation: 45, clearProps: "rotation,x" });Useful at the end of an animation segment to hand the element back to CSS.
CSS Variables
gsap.to(".chart", { "--hue": 180, duration: 1 });Animate any custom property. Works for color, length, number — anything CSS will interpolate.
Relative and Directional Values
- Relative:
"+=20","-=10","*=2". - Directional rotation:
"360_cw","-170_short","90_ccw"— controls which way the angle takes when going between two values.
SVG Specifics
svgOriginsets transform origin in the SVG's global coordinate space (not the element's local box). Do not combinesvgOriginwithtransformOriginon the same element — pick one.- Animate SVG transform attributes via the same alias names (
x,y,rotation) — GSAP handles the SVG-specific quirks.
Performance Rules
Prefer transforms and opacity
Animating x, y, scale, rotation, opacity stays on the GPU compositor. Avoid width, height, top, left, margin, padding when transforms achieve the same effect.
will-change (sparingly)
.title {
will-change: transform;
}Only on elements that _actually_ animate. Applied everywhere it becomes useless and burns memory.
gsap.quickTo for frequent updates (preview-only)
For high-frequency updates driven by events — pointer move, scroll, audio scrub — quickTo reuses the same tween instead of creating a new one each frame:
const xTo = gsap.quickTo("#cursor", "x", { duration: 0.4, ease: "power3" });
const yTo = gsap.quickTo("#cursor", "y", { duration: 0.4, ease: "power3" });
container.addEventListener("mousemove", (e) => {
xTo(e.pageX);
yTo(e.pageY);
});Render mode has no input events. The renderer seeks frame-by-frame;mousemove,scroll, etc. never fire.quickTo's main use case applies in live preview in the browser only. For audio-reactive motion in renders, pre-extract audio data and drive the timeline declaratively (see../rules/gsap-effects.md).
Stagger beats N tweens
One tween with stagger beats N tweens with manual delays for both readability and runtime cost.
Cleanup
In live preview, pause or kill() off-screen animations. Render mode is unaffected (the renderer drives time directly).
HyperFrames GSAP
GSAP usage scoped to HyperFrames' seek-driven render model. This skill is the GSAP reference _as constrained by HyperFrames_ — for the framework's broader composition contract see hyperframes-core.
HyperFrames Contract
HyperFrames controls GSAP through its gsap runtime adapter. Create a paused timeline synchronously, register it on window.__timelines with the exact data-composition-id, and let HyperFrames seek it.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from(".title", { y: 48, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.to(".accent", { scaleX: 1, duration: 0.5, ease: "power2.out" }, 0.25);
window.__timelines["main"] = tl; // key must equal data-composition-id on the composition root
</script>- The registry key must match the composition root's
data-composition-id. - Bracket and dot syntax both register:
window.__timelines["main"] = tlandwindow.__timelines.main = tlare equivalent (the linter recognizes both). Bracket form is required when the id isn't a valid identifier (e.g. contains-). - Do not call
tl.play()for render-critical motion. - Do not build timelines inside async code, timers, or event handlers.
- Keep loops finite. HyperFrames renders finite video durations.
- Render duration comes from `data-duration` on the composition root, not from GSAP timeline length. Do not pad the timeline with empty tweens like
tl.set({}, {}, 283)to "extend" it. (Some external docs show this trick; in HyperFrames it conflicts with the seek-driven duration model — setdata-durationinstead.)
Core Tween Methods
- gsap.to(targets, vars) — animate from current state to
vars. Most common. - gsap.from(targets, vars) — animate from
varsto current state (entrances). - gsap.fromTo(targets, fromVars, toVars) — explicit start and end.
- gsap.set(targets, vars) — apply immediately (duration 0).
Always use camelCase property names (e.g. backgroundColor, rotationX).
Common vars (cheatsheet)
- duration — seconds (default 0.5).
- delay — seconds before start.
- ease —
"power1.out"(default),"power3.inOut","back.out(1.7)","elastic.out(1, 0.3)","none". See./gsap-easing-and-stagger.md. - stagger — number or object. See
./gsap-easing-and-stagger.md. - repeat — finite number; never
-1in HyperFrames. Compute repeats from the visible duration. - yoyo — alternates direction with repeat.
- overwrite —
false(default),true, or"auto". - immediateRender — default
truefor from()/fromTo(). Setfalseon later tweens targeting the same property+element. - onComplete, onStart, onUpdate — callbacks.
For transforms, autoAlpha, clearProps, and SVG specifics see ./gsap-transforms-and-perf.md.
Animated Property Allowlist
HyperFrames is stricter than vanilla GSAP. Animate only:
- Compositor-cheap:
opacity,x,y,scale,scaleX,scaleY,rotation,rotationX,rotationY,skewX,skewY,transformOrigin - Visual fills:
color,backgroundColor,borderColor,borderRadius - CSS variables:
"--hue": 180etc. - Media `volume` (on
<audio>/<video>): animate for fades/ducking, e.g.tl.to("#bgm", { volume: 0, duration: 1 }, "outro"). The runtime probes these keyframes from the timeline and drives them in both preview and render (they match). This sets the _author_ volume;data-volumeis the static baseline when no tween touches the element. - DOM text `innerText` (for numeric counters): tween it directly, e.g.
tl.to(el, { innerText: 100, snap: { innerText: 1 } })—snapkeeps it integer; the GSAP inspector recognizes it as a counter. Equivalent to theonUpdate-proxy form in../rules/counting-dynamic-scale.md; prefer that proxy form when you must also drive font-size, locale formatting (toLocaleString), or a suffix in the same tween.
Avoid (use the transform alias instead):
width/height/top/left/right/bottom/margin*/padding*— trigger layout reflows. UsescaleX/Y(withtransformOrigin) orx/y.
Forbidden (breaks the renderer or the clip lifecycle):
display,visibility— never tween these directly. UseautoAlpha(sets opacity + visibility together at endpoints, doesn't tween the discrete property).- Anything driven by
Math.random(),Date.now(),performance.now(), or event handlers — animation state must be deterministic from time alone.
Note:docs/guides/gsap-animation.mdxlistswidth/height/visibilityin its "Supported Properties" — that list is too permissive for HyperFrames composition rules. This allowlist is the canonical one. Seehyperframes-core/references/determinism-rules.mdfor the full deterministic-render contract.
References
./gsap-timeline-and-labels.md— timeline creation, position parameter (+=,<,>), labels, nesting, sub-compfromTopreference, playback control../gsap-easing-and-stagger.md— easing families, stagger objects, function-based values,gsap.matchMedia(),gsap.defaults()../gsap-transforms-and-perf.md— transform aliases, autoAlpha,quickTo,will-change, performance rules.../rules/gsap-effects.md— drop-in recipes: typewriter (with cursor / backspace / word rotation) + audio visualizer (usesskills/hyperframes-creative/scripts/extract-audio-data.py).
Best Practices
- Use camelCase property names; prefer transform aliases and autoAlpha.
- Prefer timelines over chained tweens with delays; use the position parameter.
- Add labels with
addLabel()for readable sequencing. - Pass defaults into the timeline constructor.
- Store the tween/timeline return value when controlling playback.
Do Not
- Animate layout properties (
width/height/top/left) when transforms suffice. - Use both
svgOriginandtransformOriginon the same SVG element. - Chain animations with
delaywhen a timeline can sequence them. - Create tweens before the DOM exists.
- Use infinite
repeat: -1in HyperFrames compositions — use finite repeat counts computed from the visible duration.
Credits And References
- HyperFrames adapter source:
packages/core/src/runtime/adapters/gsap.ts. - GSAP documentation: https://gsap.com/docs/v3/
- GSAP timeline pause and seek behavior: https://gsap.com/docs/v3/GSAP/Timeline/pause%28%29/
HTML-in-Canvas Patterns
HyperFrames' most powerful visual capability. Capture ANY live HTML/CSS as a GPU texture, then render it through WebGL shaders, Three.js 3D scenes, or post-processing effects — at 60fps, pixel-perfect, with every CSS feature supported.
Read this file when a beat deserves cinematic treatment beyond flat GSAP animations. Use for 1-3 hero beats per video, not every beat. The rest can use standard GSAP — the contrast between flat beats and HTML-in-Canvas beats IS part of the visual storytelling.
---
Core Boilerplate (same in every HTML-in-Canvas composition)
Every HTML-in-Canvas effect shares this structure. Learn this once, adapt it for any effect.
<!-- 1. Source HTML — your content goes inside a layoutsubtree canvas -->
<canvas
id="hic-source"
layoutsubtree
width="1920"
height="1080"
style="position:absolute;inset:0;opacity:0;"
>
<div id="hic-content" style="width:1920px;height:1080px;">
<!-- YOUR HTML CONTENT HERE — text, images, cards, dashboards, anything -->
</div>
</canvas>
<!-- 2. Render target — the visible canvas that shows the effect -->
<canvas id="hic-output" width="1920" height="1080" style="position:absolute;inset:0;"></canvas>// 3. Feature detection — always check, always provide fallback
function isHiCSupported() {
var tc = document.createElement("canvas");
if (!("layoutSubtree" in tc)) return false;
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
}
var apiOk = isHiCSupported();
// 4. Capture function — call this every frame in onUpdate
var capCanvas = document.getElementById("hic-source");
var capCtx = capCanvas.getContext("2d");
function captureContent() {
if (apiOk) {
capCtx.drawElementImage(document.getElementById("hic-content"), 0, 0, 1920, 1080);
}
}
// 5. Drive from GSAP timeline — capture + render every frame
tl.to(
proxy,
{
/* your animation properties */
duration: BEAT_DURATION,
ease: "sine.inOut",
onUpdate: function () {
captureContent();
// render your effect here (Three.js or WebGL2)
},
},
0,
);Fallback: When drawElementImage is not available (preview without Chrome flag), draw a solid-color placeholder or use Canvas 2D text. The HyperFrames renderer auto-enables the flag — the effect WILL work in the final video. See the liquid-glass block for a complete fallback example.
---
Effect Catalog
1. 3D Rotation with Bloom (Three.js)
What it looks like: Content floats in 3D space, slowly rotating with cinematic glow around bright edges. Like a product screenshot displayed in a dark theater.
When to use: Hero product showcase, feature reveal, CTA with premium feel.
Key Three.js components: PlaneGeometry + CanvasTexture + EffectComposer + UnrealBloomPass
// After the boilerplate above, add:
var scene3d = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 1920 / 1080, 0.1, 100);
camera.position.set(0, 0, 4);
var renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("hic-output"),
antialias: true,
alpha: true,
});
renderer.setSize(1920, 1080);
var texture = new THREE.CanvasTexture(capCanvas);
var mesh = new THREE.Mesh(
new THREE.PlaneGeometry(3.6, 2.2),
new THREE.MeshBasicMaterial({ map: texture }),
);
scene3d.add(mesh);
// Post-processing: bloom for cinematic glow.
// EffectComposer / RenderPass / UnrealBloomPass are ES-module named imports
// (see the import block below) — they're NOT properties of THREE in modern
// versions. Three.js r150+ removed the UMD `examples/js/` globals.
var composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene3d, camera));
composer.addPass(new UnrealBloomPass(new THREE.Vector2(1920, 1080), 0.3, 0.4, 0.85));
var proxy = { rotY: -0.12, zoom: 4.2 };
tl.to(
proxy,
{
rotY: 0.12,
zoom: 3.6,
duration: BEAT_DURATION,
ease: "sine.inOut",
onUpdate: function () {
captureContent();
texture.needsUpdate = true;
mesh.rotation.y = proxy.rotY;
camera.position.z = proxy.zoom;
composer.render();
},
},
0,
);Load Three.js and post-processing via ESM (use a `type="module"` script):
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.181.2/+esm";
import { EffectComposer } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/RenderPass.js";
import { ShaderPass } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/ShaderPass.js";
import { UnrealBloomPass } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/UnrealBloomPass.js";
// ... rest of composition code using these imports
</script>The examples/js/ path was removed in Three.js r152. Use examples/jsm/ (ES modules) with three@0.181.2 — the version used by the HyperFrames Three.js adapter.
---
2. Magnetic Cursor Distortion (Raw WebGL2)
What it looks like: Content warps and bends toward a moving point, like a magnet pulling on pixels. Chromatic aberration splits RGB channels at the distortion site.
When to use: Interactive feel, product demo with cursor, "look at THIS feature" moment.
Key technique: Custom fragment shader with Gaussian warp + chromatic split. No Three.js needed — just raw WebGL2.
// WebGL2 setup
var gl = document.getElementById("hic-output").getContext("webgl2", {
alpha: false,
preserveDrawingBuffer: true,
});
// Vertex shader — full-screen quad
var VS = `#version 300 es
in vec2 a_pos;
out vec2 v_uv;
void main() {
v_uv = a_pos * 0.5 + 0.5;
gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
// Fragment shader — magnetic warp + chromatic aberration
var FS = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 fragColor;
uniform sampler2D u_tex;
uniform vec2 u_cursor; // cursor position (0-1)
uniform float u_strength; // warp strength (0-1)
void main() {
vec2 uv = v_uv;
vec2 delta = uv - u_cursor;
float dist = length(delta);
float warp = u_strength * exp(-dist * dist * 8.0);
vec2 warped = uv - delta * warp * 0.3;
// Chromatic aberration at distortion site
float aberration = warp * 0.008;
float r = texture(u_tex, warped + vec2(aberration, 0.0)).r;
float g = texture(u_tex, warped).g;
float b = texture(u_tex, warped - vec2(aberration, 0.0)).b;
fragColor = vec4(r, g, b, 1.0);
}`;
// Compile, link, setup quad geometry, upload texture...
// (See registry/blocks/vfx-magnetic/vfx-magnetic.html for complete implementation)
// Drive cursor position from GSAP
var proxy = { cx: 0.2, cy: 0.5, strength: 0.0 };
tl.to(
proxy,
{
cx: 0.8,
cy: 0.4,
strength: 1.0,
duration: BEAT_DURATION,
ease: "power2.inOut",
onUpdate: function () {
captureContent();
// Upload texture, set uniforms, draw
gl.uniform2f(cursorLoc, proxy.cx, proxy.cy);
gl.uniform1f(strengthLoc, proxy.strength);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
},
0,
);---
3. Shatter / Fragment Explosion (Three.js)
What it looks like: Content breaks into geometric fragments that fly apart, revealing what's behind.
When to use: Dramatic transition, "breaking free" moment, tension release.
Key technique: Subdivide the source texture into triangle mesh fragments using BufferGeometry, then animate each fragment's position/rotation with GSAP.
Study registry/blocks/vfx-shatter/vfx-shatter.html for the complete 1156-line implementation. The core idea:
// 1. Capture content to texture (same boilerplate)
// Seeded PRNG for determinism — Math.random() is banned
function mulberry32(seed) {
return function () {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
var rng = mulberry32(42);
// 2. Create N triangle fragments from the texture
var fragments = [];
for (var i = 0; i < NUM_FRAGMENTS; i++) {
var geom = new THREE.BufferGeometry();
var mesh = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ map: texture }));
scene3d.add(mesh);
fragments.push({ mesh: mesh, targetPos: randomExplosionVector(rng), delay: rng() * 0.5 });
}
// 3. Animate: first hold still, then EXPLODE
tl.to({}, { duration: holdTime }, 0);
fragments.forEach(function (frag) {
tl.to(
frag.mesh.position,
{
x: frag.targetPos.x,
y: frag.targetPos.y,
z: frag.targetPos.z,
duration: 0.8,
ease: "power3.in",
},
holdTime + frag.delay,
);
tl.to(
frag.mesh.rotation,
{ x: rng() * 4, y: rng() * 4, duration: 0.8, ease: "power2.in" },
holdTime + frag.delay,
);
});---
4. Liquid / Fluid Surface (Three.js)
What it looks like: Content floats above a rippling liquid surface with real-time wave dynamics. Or content IS the surface, undulating like water.
When to use: Organic/premium feel, ambient background, "living" product showcase.
Key technique: Subdivided PlaneGeometry with vertex displacement driven by noise functions in a vertex shader.
Study registry/blocks/vfx-liquid-background/vfx-liquid-background.html for the 1244-line implementation. Core idea:
// Custom vertex shader with wave displacement
var vertexShader = `
varying vec2 vUv;
uniform float u_time;
void main() {
vUv = uv;
vec3 pos = position;
// Sine wave displacement
pos.z += sin(pos.x * 3.0 + u_time * 2.0) * 0.15;
pos.z += cos(pos.y * 2.5 + u_time * 1.5) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
var mesh = new THREE.Mesh(
new THREE.PlaneGeometry(4, 3, 64, 64), // heavily subdivided for smooth waves
new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: `varying vec2 vUv; uniform sampler2D u_tex;
void main() { gl_FragColor = texture2D(u_tex, vUv); }`,
uniforms: {
u_tex: { value: texture },
u_time: { value: 0 },
},
}),
);---
5. Portal / Dimensional Reveal (Three.js)
What it looks like: A glowing circular portal opens and content emerges through it from another dimension.
When to use: Product reveal, "entering the app" moment, hero feature introduction.
Study registry/blocks/vfx-portal/vfx-portal.html for the complete 863-line implementation.
---
When to Use HTML-in-Canvas vs Standard GSAP
| Scenario | Use | Why |
|---|---|---|
| Hero product screenshot showcase | HTML-in-Canvas (3D rotation + bloom) | Makes flat UI feel cinematic |
| Feature list / stats | Standard GSAP | Content-focused, doesn't need 3D |
| CTA / brand reveal | HTML-in-Canvas (portal or magnetic) | Makes the moment memorable |
| Social proof / logos | Standard GSAP | Orderly cascade, trust is steady |
| Transition between acts | HTML-in-Canvas (shatter) | Dramatic act break |
| Background atmosphere | HTML-in-Canvas (liquid surface) | Premium ambient feel |
| Quick feature cards | Standard GSAP | Speed matters, 3D would slow it down |
---
More Effects You Can Build
These aren't in the VFX blocks — build them yourself from the core boilerplate + a custom fragment shader. Each effect is a single GLSL function applied to the captured texture.
6. Noise Dissolve
Content dissolves into noise particles, revealing what's behind. Great for transitions.
// Fragment shader — noise-based dissolve
uniform float u_progress; // 0.0 = fully visible, 1.0 = fully dissolved
uniform sampler2D u_tex;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
void main() {
vec2 uv = v_uv;
float noise = hash(uv * 50.0);
float threshold = u_progress;
if (noise < threshold) {
// Edge glow at the dissolve boundary
float edge = smoothstep(threshold - 0.05, threshold, noise);
fragColor = vec4(1.0, 0.6, 0.2, 1.0) * (1.0 - edge); // orange edge glow
} else {
fragColor = texture(u_tex, uv);
}
}7. Holographic / Iridescent
Content gets a rainbow-shifting holographic sheen that moves with time. Premium, futuristic feel.
uniform float u_time;
uniform sampler2D u_tex;
void main() {
vec4 color = texture(u_tex, v_uv);
// Iridescent color shift based on position + time
float angle = v_uv.x * 6.28 + v_uv.y * 3.14 + u_time * 0.5;
vec3 holo = vec3(
sin(angle) * 0.5 + 0.5,
sin(angle + 2.094) * 0.5 + 0.5,
sin(angle + 4.189) * 0.5 + 0.5
);
// Blend holographic over content (subtle overlay)
fragColor = vec4(mix(color.rgb, holo, 0.15 + 0.1 * sin(u_time)), color.a);
}8. Scan Lines + CRT
Retro CRT monitor look — scan lines, slight curvature, phosphor glow. Great for "code" or "terminal" beats.
uniform sampler2D u_tex;
uniform float u_time;
void main() {
vec2 uv = v_uv;
// Barrel distortion (CRT curvature)
vec2 centered = uv - 0.5;
float dist = dot(centered, centered);
uv = uv + centered * dist * 0.15;
vec4 color = texture(u_tex, uv);
// Scan lines
float scanline = sin(uv.y * 800.0) * 0.04;
color.rgb -= scanline;
// Slight RGB offset (phosphor)
color.r = texture(u_tex, uv + vec2(0.001, 0.0)).r;
color.b = texture(u_tex, uv - vec2(0.001, 0.0)).b;
// Vignette
float vignette = 1.0 - dist * 2.0;
fragColor = vec4(color.rgb * vignette, 1.0);
}9. Frosted Glass Blur
Content behind frosted glass — visible but softened, with subtle light refraction. Good for "behind the scenes" or "coming soon" moments.
uniform sampler2D u_tex;
uniform float u_blur; // 0.0 = clear, 1.0 = full frost
void main() {
vec2 uv = v_uv;
vec4 color = vec4(0.0);
// Box blur with offset
float radius = u_blur * 0.015;
for (float x = -2.0; x <= 2.0; x += 1.0) {
for (float y = -2.0; y <= 2.0; y += 1.0) {
color += texture(u_tex, uv + vec2(x, y) * radius);
}
}
color /= 25.0;
// Add frost noise texture
float frost = fract(sin(dot(uv * 200.0, vec2(12.9898, 78.233))) * 43758.5453);
color.rgb += frost * 0.03 * u_blur;
fragColor = color;
}10. Pixel Sort / Glitch Art
Pixels rearrange themselves in vertical or horizontal strips — digital art aesthetic. Great for tech/creative brands.
uniform sampler2D u_tex;
uniform float u_intensity; // 0-1
void main() {
vec2 uv = v_uv;
// Random horizontal displacement per row
float row = floor(uv.y * 80.0);
float noise = fract(sin(row * 127.1) * 43758.5);
float displace = step(0.7, noise) * u_intensity * 0.1;
// Shift UV with RGB split
float r = texture(u_tex, uv + vec2(displace, 0.0)).r;
float g = texture(u_tex, uv).g;
float b = texture(u_tex, uv - vec2(displace * 0.5, 0.0)).b;
fragColor = vec4(r, g, b, 1.0);
}---
Creating ANY Custom Effect
The fragment shaders above are templates. The pattern is always:
1. Capture your HTML content with drawElementImage (the boilerplate at the top) 2. Upload the captured canvas as a WebGL texture 3. Write a fragment shader that reads from the texture and outputs modified colors 4. Drive shader uniforms from GSAP via onUpdate
Any GLSL effect from ShaderToy, The Book of Shaders, CodePen, or anywhere else can be adapted:
1. Find an effect you like (search "GLSL [effect name]" or browse shadertoy.com) 2. Copy the fragment shader 3. Replace iResolution with vec2(1920.0, 1080.0), iTime with your u_time uniform 4. Add uniform sampler2D u_tex; for the captured content texture 5. Wire the uniforms to GSAP proxy values
Geometry ideas beyond flat planes:
SphereGeometry— content mapped onto a globe (world map, global reach)CylinderGeometry— content on a rotating cylinder (carousel/scroll feel)TorusGeometry— content wrapped around a ring (infinity, cycle)BoxGeometry— content on a 3D box (product packaging, dice)- GLTF models — content mapped as screen texture on phone, laptop, monitor (see
vfx-iphone-device)
Post-processing stacking (Three.js EffectComposer):
- Bloom + film grain = cinematic
- Bloom + chromatic aberration = lens effect
- Depth of field + vignette = focused attention
- Film grain + scan lines = retro
- Multiple passes stack — add as many as you want
You are not limited to the effects listed here. If you can imagine a visual treatment, you can build it. The HTML-in-Canvas API gives you the source material (any HTML rendered as a texture), and WebGL/Three.js gives you unlimited creative control over how that material is presented.
Lottie for HyperFrames
HyperFrames can seek both lottie-web and dotLottie players through its lottie runtime adapter. Lottie is a strong fit because the animation timeline is already encoded in the asset; HyperFrames only needs a player object it can seek.
Contract
- Load assets from local project files, usually under
assets/. - Set
autoplay: false. - Prefer
loop: falseunless the user explicitly wants a loop. - Register every returned animation or player on
window.__hfLottie. - Keep the Lottie container dimensions stable with CSS.
The adapter seeks lottie-web with goToAndStop(timeMs, false) and dotLottie with frame or percentage APIs depending on player shape.
lottie-web Pattern
<div id="logo-lottie" class="lottie-layer"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.12.2/lottie.min.js"></script>
<script>
const anim = lottie.loadAnimation({
container: document.getElementById("logo-lottie"),
renderer: "svg",
loop: false,
autoplay: false,
path: "assets/logo-reveal.json",
});
window.__hfLottie = window.__hfLottie || [];
window.__hfLottie.push(anim);
</script>.lottie-layer {
width: 100%;
height: 100%;
}dotLottie Pattern
<canvas id="product-lottie" class="lottie-canvas"></canvas>
<script src="https://unpkg.com/@lottiefiles/dotlottie-web"></script>
<script>
const player = new DotLottie({
canvas: document.getElementById("product-lottie"),
src: "assets/product-flow.lottie",
autoplay: false,
loop: false,
});
window.__hfLottie = window.__hfLottie || [];
window.__hfLottie.push(player);
</script>.lottie-canvas {
width: 100%;
height: 100%;
display: block;
}Multiple Animations
Push each player into the same registry:
window.__hfLottie = window.__hfLottie || [];
window.__hfLottie.push(backgroundAnim);
window.__hfLottie.push(iconAnim);
window.__hfLottie.push(confettiAnim);HyperFrames seeks them all to the same composition time.
Good Uses
- After Effects exports that are already known to render correctly in lottie-web.
- Logo reveals, icon loops, decorative accents, and product UI motion.
- Translating Remotion Lottie usage into plain HyperFrames HTML.
Avoid
- Relying on remote
pathURLs at render time. - Starting playback with
play(). - Assuming unsupported After Effects effects will survive export. Test the JSON or
.lottiefile in a browser first. - Loading a player asynchronously and registering it after HyperFrames validation has already inspected the page.
Validation
After editing a Lottie composition:
npx hyperframes lint
npx hyperframes validateCredits And References
- HyperFrames adapter source:
packages/core/src/runtime/adapters/lottie.ts. - lottie-web by Airbnb: https://github.com/airbnb/lottie-web
- lottie-web
loadAnimationoptions: https://github.com/airbnb/lottie-web/wiki/loadAnimation-options - dotLottie web player methods by LottieFiles: https://developers.lottiefiles.com/docs/dotlottie-player/dotlottie-web/methods
Three.js for HyperFrames
HyperFrames supports Three.js through its three runtime adapter. The adapter does not own your scene. It publishes HyperFrames time and dispatches a seek event so your composition can render the exact frame.
Contract
- Create the scene, camera, renderer, materials, and assets synchronously when possible.
- Render from HyperFrames time, not wall-clock time.
- Listen for the
hf-seekevent and render exactly that time. - Load models, textures, and HDRIs before render-critical seeking. Do not fetch them at seek time.
- Avoid
requestAnimationFrameorrenderer.setAnimationLoopas the source of truth for render-critical motion.
The adapter sets window.__hfThreeTime and dispatches new CustomEvent("hf-seek", { detail: { time } }) on each seek.
Basic Pattern
<canvas id="three-layer"></canvas>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.181.2/+esm";
const canvas = document.getElementById("three-layer");
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
// Match these to your composition's frame size.
renderer.setSize(1920, 1080, false);
renderer.setPixelRatio(1);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, 1920 / 1080, 0.1, 100);
camera.position.set(0, 0, 6);
const mesh = new THREE.Mesh(
new THREE.IcosahedronGeometry(1.4, 4),
new THREE.MeshStandardMaterial({ color: 0x64d2ff, roughness: 0.38 }),
);
scene.add(mesh);
scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 2));
function renderAt(time) {
mesh.rotation.y = time * 0.7;
mesh.rotation.x = Math.sin(time * 0.6) * 0.16;
renderer.render(scene, camera);
}
window.addEventListener("hf-seek", (event) => {
renderAt(event.detail.time);
});
renderAt(window.__hfThreeTime || 0);
</script>#three-layer {
width: 100%;
height: 100%;
display: block;
}Loading Addons (GLTFLoader, OrbitControls, etc.)
For anything under three/addons/, use an importmap so bare specifiers resolve. The HyperFrames lint recognizes both this form and the inline +esm import above — pick whichever your composition needs.
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.181.2/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// ...
</script>Pin the three version in both entries to the same value. Mixing versions across the map and bare imports causes silent breakage.
AnimationMixer Pattern
For GLTF or authored clip animation, seek the mixer directly:
function renderAt(time) {
mixer.setTime(time);
renderer.render(scene, camera);
}If several mixers exist, seek all of them from the same time.
Good Uses
- Deterministic 3D objects, product spins, particles with seeded data, and shader plates.
- Camera moves derived from
time. - GLTF animation clips when assets are local and loaded before validation completes.
Avoid
- Using
Date.now(),performance.now(), or clock deltas to update scene state. - Leaving render-critical work inside a free-running animation loop.
- Loading remote models or textures at render time.
- Device-pixel-ratio dependent output. Pin renderer size and pixel ratio for video renders.
- Post-processing passes that depend on previous frame history unless you can reconstruct state from time.
Validation
After editing a Three.js composition:
npx hyperframes lint
npx hyperframes validateCredits And References
- HyperFrames adapter source:
packages/core/src/runtime/adapters/three.ts. - Three.js
WebGLRendererdocs: https://threejs.org/docs/pages/WebGLRenderer.html - Three.js
AnimationMixer.setTime()docs: https://threejs.org/docs/pages/AnimationMixer.html
TypeGPU / WebGPU for HyperFrames
HyperFrames supports TypeGPU and raw WebGPU through its typegpu runtime adapter. The adapter does not own your pipeline. It publishes HyperFrames time and dispatches a seek event so your composition can render the exact GPU frame.
Render-environment prerequisite (WebGPU + html-in-canvas)
The render engine auto-passes --enable-unsafe-webgpu and --enable-features=CanvasDrawElement to its Chrome launch args. Stock Chromium and the bundled headless-shell do not support WebGPU + drawElementImage together — the combo that liquid-glass blocks need (ios26-liquid-glass, macos-tahoe-liquid-glass, liquid-glass-*, vfx-liquid-glass). For those blocks, point the engine at Brave (or Chrome canary) by setting PRODUCER_HEADLESS_SHELL_PATH to the browser binary before running npx hyperframes render / preview. Plain TypeGPU layers without HTML-as-texture work in headless-shell — only the html-in-canvas + WebGPU combination needs the override.
Contract
- Initialize WebGPU asynchronously (
await navigator.gpu.requestAdapter()), but register all GSAP tweens synchronously — before anyawait. The HyperFrames player reads the timeline immediately at page load. - Render from HyperFrames time, not
performance.now(). - Listen for the
hf-seekevent and re-render at exactly that time. - Guard against environments where WebGPU is unavailable — the adapter does not check for you.
- For video renders, call
await device.queue.onSubmittedWorkDone()after submitting GPU work to ensure the canvas is flushed before the frame is captured.
The adapter sets window.__hfTypegpuTime and dispatches new CustomEvent("hf-seek", { detail: { time } }) on each seek.
Basic Pattern
<canvas id="gpu-layer"></canvas>
<script>
(async () => {
if (!navigator.gpu) return;
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return;
const device = await adapter.requestDevice();
const canvas = document.getElementById("gpu-layer");
canvas.width = 1920;
canvas.height = 1080;
const ctx = canvas.getContext("webgpu");
const fmt = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format: fmt, alphaMode: "opaque" });
// Build your pipeline, buffers, bind groups...
const timeUniform = new Float32Array([0]);
const timeBuf = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
function render(t) {
timeUniform[0] = t;
device.queue.writeBuffer(timeBuf, 0, timeUniform);
const enc = device.createCommandEncoder();
const pass = enc.beginRenderPass({
colorAttachments: [
{
view: ctx.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0, a: 1 },
storeOp: "store",
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([enc.finish()]);
}
render(0);
window.addEventListener("hf-seek", (e) => render(e.detail.time));
})();
</script>Timeline Registration
GSAP tweens that drive text, captions, or HTML elements must be registered synchronously — before any await:
const tl = gsap.timeline({ paused: true });
// Caption tweens: synchronous, added before WebGPU init
gsap.set(".cap", { opacity: 0 });
tl.to("#cap-1", { opacity: 1, duration: 0.3 }, 1.0);
tl.to("#cap-1", { opacity: 0, duration: 0.2 }, 3.5);
window.__timelines["my-comp"] = tl;
// GPU-dependent tweens can go inside the async IIFE
(async () => {
// ... WebGPU init ...
const proxy = { value: 0 };
tl.to(proxy, { value: 1, duration: 2, onUpdate: render }, 0.5);
})();Video-Backed Effects (Liquid Glass, Distortion)
To use a <video> as the GPU input texture:
const videoEl = document.getElementById("aroll");
// Wait for video metadata before creating the texture
await new Promise((r) => {
if (videoEl.readyState >= 1) r();
else videoEl.addEventListener("loadedmetadata", r, { once: true });
});
// Create texture at the video's NATIVE resolution
const vw = videoEl.videoWidth,
vh = videoEl.videoHeight;
const bgTex = device.createTexture({
size: [vw, vh],
format: "rgba8unorm",
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
});
function render(t) {
try {
device.queue.copyExternalImageToTexture({ source: videoEl }, { texture: bgTex }, [vw, vh]);
} catch (_) {
/* frame not decoded yet */
}
// ... draw ...
}Render-mode caveat: headless Chrome may fail copyExternalImageToTexture for video elements. For production renders, pre-extract key frames via FFmpeg as PNGs and load them as image textures instead.
Frosted Blur via Downsample Pass
A single-pass Gaussian kernel is too weak for glass-like frosted blur. Use a two-pass approach:
1. Pass 1 — Downsample: render the full-res texture to a small texture (1/6 resolution). Bilinear filtering during the downsample naturally averages pixels. 2. Pass 2 — Glass composite: sample the small texture for the frosted interior (bilinear upscale = heavy smooth blur) and the full-res texture for sharp areas and chromatic refraction.
This matches TypeGPU's textureSampleBias mip-level approach without generating mipmaps.
Transparent vs Opaque Canvas
- `alphaMode: 'opaque'` — the GPU canvas renders the full frame (video + effect). Use when the GPU pipeline handles all visual content.
- `alphaMode: 'premultiplied'` — the GPU canvas is transparent where alpha = 0, letting HTML elements below show through. Use for overlays (particles, path animations) on top of a regular
<video>element.
WGSL Full-Screen Triangle
The standard vertex shader for full-screen effects (no vertex buffer needed):
struct Vo { @builtin(position) pos: vec4f, @location(0) uv: vec2f }
@vertex fn vs(@builtin(vertex_index) vi: u32) -> Vo {
let ps = array<vec2f, 3>(vec2f(-1., -1.), vec2f(3., -1.), vec2f(-1., 3.));
let ts = array<vec2f, 3>(vec2f(0., 1.), vec2f(2., 1.), vec2f(0., -1.));
return Vo(vec4f(ps[vi], 0., 1.), ts[vi]);
}Draw with pass.draw(3) — one triangle that covers the viewport.
Rounded-Rect SDF (Liquid Glass Pill)
fn sdf_box(p: vec2f, half_size: vec2f, corner_radius: f32) -> f32 {
let d = abs(p) - half_size + vec2f(corner_radius);
return length(max(d, vec2f(0.))) + min(max(d.x, d.y), 0.) - corner_radius;
}Use this to define inside/ring/outside zones for glass effects. Negative values are inside the shape.
Deterministic Rendering
- No
Math.random()— use a seeded PRNG. - No
requestAnimationFramefor the render loop — render only in response tohf-seek. - No
performance.now()for animation time — readwindow.__hfTypegpuTimeore.detail.time. - After GPU submit, call
await device.queue.onSubmittedWorkDone()for render-mode frame capture.
Web Animations API for HyperFrames
HyperFrames can seek Web Animations API animations through its waapi runtime adapter. WAAPI is useful when you want native browser keyframes with JavaScript-created timing and no GSAP dependency.
Contract
- Create animations synchronously during composition initialization.
- Use
element.animate(...)with finitedurationanditerations. - Use
fill: "both"so seeked states persist. - Pause animations after creation or let the adapter pause them on first seek.
- Avoid callbacks and promises for render-critical state.
The adapter calls document.getAnimations(), sets each animation's currentTime to HyperFrames time in milliseconds, then pauses it.
Basic Pattern
<div id="orb" class="clip orb" data-start="2" data-duration="3" data-track-index="2"></div>
<script>
const orb = document.getElementById("orb");
const animation = orb.animate(
[
{ transform: "translate3d(-160px, 0, 0) scale(0.8)", opacity: 0 },
{ transform: "translate3d(0, 0, 0) scale(1)", opacity: 1, offset: 0.35 },
{ transform: "translate3d(120px, 0, 0) scale(1.08)", opacity: 1 },
],
{
duration: 3000,
delay: 2000,
easing: "cubic-bezier(0.2, 0, 0, 1)",
fill: "both",
iterations: 1,
},
);
animation.pause();
</script>Stagger Pattern
document.querySelectorAll(".token").forEach((token, index) => {
const animation = token.animate(
[
{ transform: "translateY(24px)", opacity: 0 },
{ transform: "translateY(0)", opacity: 1 },
],
{
duration: 620,
delay: index * 80,
easing: "cubic-bezier(0.2, 0, 0, 1)",
fill: "both",
iterations: 1,
},
);
animation.pause();
});Good Uses
- Lightweight DOM motion where CSS keyframes are too rigid and GSAP is unnecessary.
- Generated animations from structured data.
- Simple timelines that can be represented as keyframes, delays, and offsets.
Avoid
- Infinite
iterations. - Depending on
animation.finishedto mutate render-critical DOM. - Running separate clocks with
requestAnimationFrame, timers, orperformance.now(). - Animating layout properties when transforms and opacity can express the motion.
- Assuming clip-local start time is automatic. WAAPI adapter seeks document-level animation time; model clip offsets with
delayor create the animation on an element whose visibility is controlled by HyperFrames timing.
Validation
After editing a WAAPI composition:
npx hyperframes lint
npx hyperframes validateCredits And References
- HyperFrames adapter source:
packages/core/src/runtime/adapters/waapi.ts. - MDN Web Animations API guide: https://developer.mozilla.org/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API
- MDN
Animation.currentTime: https://developer.mozilla.org/en-US/docs/Web/API/Animation/currentTime
Scene Blueprints (load on demand)
Read this file only when authoring a full scene from a pre-designed multi-phase choreography template. For atomic motion techniques, the rules index in SKILL.md is the default starting point.
A blueprint describes a complete multi-phase scene with phase pipeline, glue code, and a working sample composition. Each blueprint has a runnable HTML example under examples/ you can use as ground truth.
Picking a blueprint
Match your scene's narrative role to a blueprint role: social-proof, concept-demo, brand-reveal, takeover, demo, opening-hook, workflow, problem, cta, comparison, metric, messaging.
If two blueprints fit, pick the one whose uses rules are closer to your visual plan — composing existing rules is cheaper than reinventing.
If no blueprint fits, compose rules directly from SKILL.md — blueprints are starting points, not requirements.
Blueprints
<blueprints> <blueprint id="proof-logo-chain" path="blueprints/proof-logo-chain.md" example="examples/proof-logo-chain.html" role="social-proof" duration="6-10s" phases="5" uses="hacker-flip-3d, vertical-spring-ticker, coordinate-target-zoom, avatar-cloud-network" triggers="brand reveal, social proof, #1 tool, million users, trusted by"> Logo threads through 5 phases: hacker-flip text → text swap → logo centers → avatar cloud + counter → partner brand logos. Example (8s): hacker-flip 'HyperFrames' → 'HTML Video' lockup with rolling render / ship ticker → logo recenters → '60 FPS' static label with scale-pulse + avatar cloud + SVG connection lines → partner brand-logo strip. Single paused GSAP timeline drives all five phases. </blueprint>
<blueprint id="concept-demo-decode-pan" path="blueprints/concept-demo-decode-pan.md" example="examples/concept-demo-decode-pan.html" role="concept-demo" duration="6-10s" phases="4" uses="hacker-flip-3d, camera-cursor-tracking, discrete-text-sequence" triggers="decode effect, scene transition, search bar typing, show then demonstrate"> Shot 1 hacker-flip decode → horizontal camera pan with parallax → Shot 2 cursor-tracked typing. Example (7s): Shot 1 "HyperFrames renders" static rise + hacker-flip decode of accent word "video" → horizontal pan with parallax exit + scale-in → Shot 2 cursor-tracked typing "HTML, CSS and JS become MP4" inside a pre-allocated search-bar. Demonstrates browser-native text measurement (no charWidthRatio constant) and piecewise Math.min camera tracking. </blueprint>
<blueprint id="brand-reveal-assemble-zoom" path="blueprints/brand-reveal-assemble-zoom.md" example="examples/brand-reveal-assemble-zoom.html" role="brand-reveal" duration="4-6s" phases="5" uses="discrete-text-sequence, coordinate-target-zoom, sine-wave-loop" triggers="brand reveal, zoom into logo, hero focus, wide to close-up"> Companion text assembles beside hero → companion exits + recenters → camera zooms into hero → hero breathes. Example (5s): 'Just use' discrete-assembly companion beside 'Hyperframes' + logo image → companion slides out and container recenters → camera zooms 5.5× into the logo → logo breathes (sine onUpdate, multiplicative). Demonstrates three nested transform layers (scale → translate → recenter) and brandTextWidth measurement after fonts.ready. </blueprint>
<blueprint id="takeover-ticker-displace" path="blueprints/takeover-ticker-displace.md" example="examples/takeover-ticker-displace.html" role="takeover" duration="5-8s" phases="4" uses="vertical-spring-ticker, reactive-displacement, sine-wave-loop" triggers="rolling text then logo, push text away, slot machine, logo enters forcefully"> Typewriter + ticker build context → hero enters from off-screen and physically pushes the text out → hero breathes. Example (7.5s): 'Ask about any' typewriter + 'audience → topic → market' ticker → pink-magenta logo enters from offscreen-right with rotation+scale impact → text pushed left and fades (40-50% of hero duration) → logo breathes with dual-frequency sine (1.0s scale, 1.33s rotation). Demonstrates reactive-displacement causal link and multiplicative breathing on a non-1 final scale. </blueprint>
<blueprint id="demo-page-scroll-spotlight" path="blueprints/demo-page-scroll-spotlight.md" example="examples/demo-page-scroll-spotlight.html" role="demo" duration="5-9s" phases="4" uses="3d-page-scroll, asr-keyword-glow" triggers="show the feature, product demo, webpage in 3D, scroll to feature"> 3D-tilted webpage card → scrolls to feature section → keywords glow synced to ASR → key element pops forward in 3D with a radial spotlight. Example (9s): OpusClip landing page recreated as a 3D-tilted card with navbar, hero title, CTA row, and video carousel. Six title keywords ('1 long video, 10 viral clips') glow synced to ASR timestamps via CSS --glow variable + per-word two-tween envelopes. Page scrolls down 280 px to reveal the carousel; main video pops forward 80 px in 3D with a radial spotlight dimming surroundings. </blueprint>
<blueprint id="hook-counter-burst" path="blueprints/hook-counter-burst.md" example="examples/hook-counter-burst.html" role="opening-hook" duration="3-5s" phases="4" uses="counting-dynamic-scale, center-outward-expansion, multi-phase-camera, svg-icon-enrichment" triggers="opening hook, statistic, counter, dramatic number"> Counter grows + enriched SVG icons expand outward from center, wrapped in multi-phase camera. Example (3.5s): Counter "0 → 90 %" with dynamic font scaling (0.20W → 0.42W), four enriched SVG icons (clock with linearly rotating minute hand, scissors oscillating ±15°, video frame with phase-offset pulsing red dot, play button with scale pulse) expanding outward from center, multi-phase camera (0.92 → 1.0 → 1.08). Demonstrates shared-ease lockstep sync between counter and icon expansion + a single scene-ticker onUpdate consolidating all internal SVG motion. </blueprint>
<blueprint id="workflow-approve-press" path="blueprints/workflow-approve-press.md" example="examples/workflow-approve-press.html" role="workflow" duration="4-6s" phases="4" uses="press-release-spring" triggers="review and approve, step-by-step workflow, user control, approve button, with-you metaphor"> Headline top + center video demo + 3D-tilted step indicators left + action button right that presses to confirm. Example (5.5s): "AI edits WITH you" headline slides down → center editor mockup scales in → 3 review steps stagger-enter on the left flank (3D-tilted +15°) and snap through pending → active → complete via tl.set({ attr: data-state }) → Approve button (3D-tilted -15°) bouncy entry with finite-yoyo glow pulse → linear depression then linear return → backgroundColor crossfades to success green + label swaps + checkmark pops with back.out(1.6). Demonstrates a discrete state machine driven by timeline-positioned tl.set calls. </blueprint>
<blueprint id="problem-mockup-overwhelm" path="blueprints/problem-mockup-overwhelm.md" example="examples/problem-mockup-overwhelm.html" role="problem" duration="4-6s" phases="4" uses="card-morph-anchor" triggers="too many platforms, overwhelmed creator, complex workflow, surrounded by tasks"> Mockups appear → platform icons scatter → center mockup scales down + crossfades into avatar → task bubbles surround. Example (6s): Three video-platform mockups (YouTube Studio, TikTok Creator, Instagram Reels) spring-in → nine scattered platform icons stagger-enter → at 3.20s center mockup morphs via uniform scale: 1 → 0.6875 + paint-only borderRadius/background/boxShadow → at 85% of morph the mockup container fades revealing an avatar circle underneath → 8 task bubbles stagger-enter in radial pattern → continuous motion consolidated into one shared onUpdate scene-ticker reading tl.time(). </blueprint>
<blueprint id="cta-orbit-collapse" path="blueprints/cta-orbit-collapse.md" example="examples/cta-orbit-collapse.html" role="cta" duration="5-8s" phases="5" uses="orbit-3d-entry, cursor-click-ripple, center-outward-expansion, sine-wave-loop" triggers="works for any genre, multiple categories, click to generate, versatile tool"> Category icons enter with 3D flip and orbit a center CTA → cursor clicks → icons collapse inward → product demo springs out and floats. Example (6.5s): Six genre icons (Music, Gaming, Education, Sports, Vlogs, Podcast) enter staggered with 3D flip and orbit a central CTA at 0.25 rad/s → cursor slides to white button via back.out(1.3), depresses cursor + button + ripple → icons collapse via gsap.parseEase("back.out(1.6)") → demo card springs out from collapse point → CTA + cursor fade out → demo floats with finite-yoyo breathing. Three nested wrappers per icon separate orbit / collapse / entry concerns. </blueprint>
<blueprint id="cta-morph-press" path="blueprints/cta-morph-press.md" example="examples/cta-morph-press.html" role="cta" duration="4-6s" phases="4" uses="sine-wave-loop, scale-swap-transition, physics-press-reaction" triggers="logo morphs into button, CTA animation, cursor clicks button, brand to action"> Hero enters and breathes → morphs into CTA via scale-swap → cursor enters via spring path → physics-based click compresses cursor + CTA together. Example (5.5s): "GWI Spark" lockup with breathing-rotated star logo → morphs into a pink "Find out more" CTA pill via scale-swap (hero shrinks + fades, CTA pops with back.out(2)) → cursor hard-cuts in at off-screen bottom-right and approaches via spring path → physics-based click compresses both cursor and CTA together using a single GSAP target array. </blueprint>
<blueprint id="comparison-split-cards" path="blueprints/comparison-split-cards.md" example="examples/comparison-split-cards.html" role="comparison" duration="4-6s" phases="3" uses="split-tilt-cards, sine-wave-loop" triggers="two features, side by side, brand + team, dual capabilities, scale your"> Title slides down → two feature cards enter from opposite sides with opposing 3D tilts (+12° / -12°) → floating pill badges attach to each card's inner edge. Example (5s): Title slides down → left card (+18° rotateY, shadow falls right) and right card (-18° rotateY, shadow falls left) enter from their sides with power3.out over 0.7s (right staggers ~0.33s after left) → pill badges pop in at the cards' inner edges with back.out(1.7). Continuous floating consolidated in one scene-ticker onUpdate with Math.PI phase offset between left and right. </blueprint>
<blueprint id="metric-video-text-pivot" path="blueprints/metric-video-text-pivot.md" example="examples/metric-video-text-pivot.html" role="metric" duration="5-8s" phases="4" uses="3d-text-depth-layers, sine-wave-loop" triggers="accuracy rate, engagement increase, show feature then stat, big number reveal, metric emphasis"> Product video centered + floating → video slides left and giant stat (3D depth layers) appears right → both exit and kinetic text types center-screen with accent keywords → gradient pill scales behind a closing phrase. Example (6.5s): "HyperFrames" badge top; mock captioned video card centered (3D-tilted +15° rotateY) → at 2.20s video slides to 29% W and "MP4" appears on right as a 5-layer green depth stack → at 3.86s both exit and 23-char "HTML pages become video" + 15-char "frame by frame." type center-screen → gradient pill (purple → green) scales in behind line 2. </blueprint>
<blueprint id="messaging-multi-phrase" path="blueprints/messaging-multi-phrase.md" example="examples/messaging-multi-phrase.html" role="messaging" duration="7-8s" phases="3" uses="dynamic-content-sequencing, context-sensitive-cursor" triggers="multiple phrases typing, sequential statements, typing with highlight, dual-color text"> Multiple phrases type sequentially in hard cuts; each phrase has main + accent segments with a context-sensitive cursor whose color switches at the segment boundary. Timeline computed from chars × charSpeed + hold. Example (7.5s): "Build video with HTML" → "Seek any frame" → "Render to MP4" typed at 150px on black. Three phrases in SCRIPT, timeline computed from chars × 0.083s + hold. One master onUpdate writes textContent, switches cursor background-color between white and cyan at segment boundaries, and drives a 1.0s square-wave blink. </blueprint> </blueprints>
Brand Reveal · Assemble & Zoom (HyperFrames)
This is a "context → focus → idle" emotional arc: the viewer first watches the companion text type itself out and understands what they're looking at; then the hero (logo / icon) pops in to declare where the focus lies; companion yields its space, the layout's center of gravity collapses toward the hero, and the camera pushes in for a close-up; finally the hero settles into a subtle breathing motion, saying "this is the brand, it lives here."
The whole thing runs on a single paused GSAP timeline. Five phases, no hard cuts — every adjacent pair gets a 0.06–0.3s breath of buffer to absorb the previous phase's spring tail.
When to Use
- Need a progressive narrowing from wide composition to tight close-up on a hero
- A supporting element (slogan, category word, tagline) appears first to give context, then exits
- Final frame is a close-up of the hero with subtle ambient motion
- Not for: all elements equally important, pure text scenes, or scenes requiring interaction (use cta-morph-press)
Orchestration
This scene weaves four kinds of motion together; each maps to a rule or an inline pattern:
- Phase 1 — companion typing: use discrete-text-sequence, but in its monotonic simplified form (a string of
tl.setcalls, not the reverse-searchonUpdate). Brand-reveal companions usually don't have the dramatic stuff — no typos, no backspaces — they're just "one phrase that builds out quickly." The full SEQUENCE array machinery from the rule is overkill. Still, drop at least one pacing hold in the middle so the cadence doesn't read as machine-typed. - Phase 2 — hero pop: no rule needed, just inline a single
back.out(BOUNCE_FACTOR)tween that springs the hero fromscale: 0toscale: 1. One tween, no orchestration — making this a rule would be overengineering. For a rubberier feel, swap toelastic.out(amp, period). - Phase 3 — companion exit + layout recenter: the "glue" unique to this blueprint, not covered by any rule. Three concurrent tweens at the same timeline position, sharing
durationandease(companion fade-slides,.recenter-shiftpulls brand-group toward the viewport center). See "Phase 3 seam" below. - Phase 4 — zoom into hero: use coordinate-target-zoom, but its default offset derivation doesn't fit here. The rule's formula assumes a row of N equal-width cards; our layout is the three-piece "companion + brand text + hero icon" assembly, which needs its own offset derivation. See "Phase 4 seam" below.
- Phase 5 — hero breathing: use sine-wave-loop in its multiplicative `onUpdate` form (the one in the rule's GSAP Timeline section), not
fromTo+ yoyo. Phase 2 already set hero's scale to1.0and Phase 4 pushed an outer wrapper toTARGET_SCALE— the breath must multiply onto hero's existing scale, never overwrite it. See "Phase 5 seam" below.
Phase Timing
All boundaries are in seconds. Design pass: pick each phase's internal duration first, then back-derive its start time from the constraints below.
| Phase | Start ≥ | Internal duration | Notes |
|---|---|---|---|
| 1 | 0 (or end of previous scene) | TEXT_END - 0 | Monotonic sequence. End must precede POP_START |
| 2 | TEXT_END + ~0.06s | POP_DUR | Tiny gap so companion settles before pop fires |
| 3 | POP_START + POP_DUR + ~0.2s | SLIDE_DUR | Let back.out spring tail settle |
| 4 | SLIDE_START + SLIDE_DUR + ~0.3s | ZOOM_DUR | Let recenter visually land before zooming |
| 5 | ZOOM_START + ZOOM_DUR + ~0.1s | TOTAL - BREATH_START | Zoom spring tail must finish; otherwise sine fights spring |
The 0.06s gap between Phase 1 and 2 exists so "typing done → next beat" reads as two events, not one. The 0.2s gap between Phase 2 and 3 is the most critical: back.out overshoots past 1.0 and rebounds — pull the layout while that tail is still settling and the hero appears to jitter. The 0.1s gap between Phase 4 and 5 is the same idea: power2.out decays to near-zero velocity but never literally zero, and a sine wave starting on top of that produces visible chatter.
Initial DOM Nesting (Critical)
Phase 3 and Phase 4 each need their own wrapper to carry their transform, for the reason stated directly in coordinate-target-zoom's "Transform order" principle: scale must wrap translate, never the other way around — otherwise the translation gets multiplied by the outer scale and the hero "drifts and accelerates." This blueprint adds one extra layer (.recenter-shift) beyond what the rule shows, because Phase 3's recenter shift and Phase 4's counter-translate are two different motions and cannot share an element.
.zoom-scale ← Phase 4 scale
.zoom-translate ← Phase 4 counter-translate
.recenter-shift ← Phase 3 layout recenter
.layout-row
.companion ← fixed width, right-aligned, lighter font-weight
.companion-text ← Phase 1 typing target
.brand-group
.brand-text ← heavier font-weight; fades + slides out during Phase 4
.hero ← Phase 2 pop target + Phase 5 breath target.companion is a fixed-width container with justify-content: flex-end and white-space: nowrap. The reason maps directly to discrete-text-sequence's "fixed-width container" principle: as characters appear, content length grows; without a fixed width, brand-group gets shoved around as the companion's edge moves. With a fixed width, overflow gets pushed off the container's left edge (not into the brand). The design-time obligation is therefore COMPANION_WIDTH ≥ widthOf(longest SEQUENCE state) — otherwise text leaks past the viewport.
Brand text uses a heavier font-weight than the companion. Even while companion is still on screen, the hero side must read as dominant — otherwise the Phase 3 "weight transfer" feels abrupt rather than inevitable.
Phase 3 Seam: Companion Exit + Recenter
This step has no existing rule — it's the blueprint's core glue. Three tweens fire at the same timeline position, sharing SLIDE_DUR and power3.out:
1. .companion fade-slides out (opacity → 0, x → COMPANION_EXIT_X) 2. .recenter-shift pulls left by FINAL_RECENTER_OFFSET, bringing brand-group to the horizontal midline 3. (Optional) brand text picks up a slight "dragged toward center" lateral slide
`FINAL_RECENTER_OFFSET` is the most common point of failure in this blueprint. Theoretical baseline: shift left by half the (companion + gap) width:
FINAL_RECENTER_OFFSET ≈ -(COMPANION_WIDTH + COMPANION_GAP) / 2But in practice you'll always find the hero ending up slightly off-center to the left. Reason: brand text's visual center of mass isn't its geometric midpoint (bold weights, serifs, letter-spacing all bias the perceived center). Tuned values are usually smaller in magnitude than the theoretical baseline. Tune by eye, then bake the result as a `const` — never recompute per frame. The Phase 4 zoom multiplies everything by 5×+, so any sub-pixel drift in this value becomes visible jitter.
Phase 4 Seam: Zoom into Hero (offset derivation)
The default offset derivation in coordinate-target-zoom assumes "N equal-width cards in a horizontal row" — that formula doesn't apply here. Our layout is companion + gap + brandText + heroGap + hero, with the hero not at center. The hero's distance from viewport center after Phase 3 has to be derived separately:
baseHeroOffset = (COMPANION_WIDTH + COMPANION_GAP + brandTextWidth + HERO_GAP) / 2
HERO_FINAL_OFFSET_X = baseHeroOffset + FINAL_RECENTER_OFFSET
counterTranslateX = -HERO_FINAL_OFFSET_X // feeds .zoom-translate's x tween`baseHeroOffset` does NOT include `HERO_SIZE` — the S in "total flex width" cancels against the S/2 in "icon center to row center distance":
Total flex width T = C + G + B + L + S
Icon center = C + G + B + L + S/2
Layout center = T / 2
Offset = (C + G + B + L + S/2) − T/2
= C/2 + G/2 + B/2 + L/2
= (C + G + B + L) / 2Including S would make the counter-translation overshoot, leaving the icon center-left after zoom.
`brandTextWidth` must be measured with a hidden DOM probe after `document.fonts.ready`, then baked. Before fonts load, getBoundingClientRect() uses fallback metrics — easily off by 10–30 px, which becomes tens of visible pixels after a 5× zoom. The standard DOM probe (placed before timeline registration):
await document.fonts.ready;
const probe = document.createElement("span");
probe.style.cssText =
"position:absolute; left:-99999px; white-space:pre; " +
`font: ${BRAND_WEIGHT} ${BRAND_FONT_SIZE}px ${BRAND_FONT_STACK};`;
probe.textContent = BRAND_TEXT;
document.body.appendChild(probe);
const brandTextWidth = probe.getBoundingClientRect().width;
probe.remove();
// Use brandTextWidth to compute HERO_FINAL_OFFSET_X, bake as constThe zoom itself (outer scale + inner counter-translate tweens) follows coordinate-target-zoom's critical constraints verbatim: shared ZOOM_DUR + ease, no recomputing offset in onUpdate, transform-origin: 50% 50%.
During the zoom, brand text fades + slides out (duration ZOOM_DUR * 0.3–0.5, finishing well before the zoom climax) — otherwise at 5× scale it fills the frame and covers the hero.
Phase 5 Seam: Why Breath Must Be Multiplicative
The multiplicative onUpdate form in sine-wave-loop (gsap.set(hero, { scale: HERO_FINAL_SCALE * (1 + sin(ω) * AMP) })) isn't a recommendation here — it's a hard requirement:
- Phase 2 tweens hero's scale to
1.0(orHERO_FINAL_SCALE) — this is hero's resting state - Phase 4 pushes
.zoom-scale(outer wrapper) toTARGET_SCALE— but this is on the wrapper, not on hero itself - Phase 5 must let hero breathe on top of its own resting scale — so it has to multiply onto
HERO_FINAL_SCALEas a baseline
Using the rule's alternative fromTo + yoyo form here would re-tween hero from 0 to 1, undoing Phase 2's final state (even if "0 to 1" looks identical on paper, GSAP's from-state gets re-applied on every yoyo cycle, killing the post-pop scale).
SCALE_AMP 0.02–0.04 (product photography → small; stylized logo → large), ROTATE_AMP 0–2°, SCALE_PERIOD ~1.5–2s reads as natural breathing — ranges live in the rule, pick by scene.
Key Values to Choose (Not Already in the Rules)
Only listing the parameters unique to this blueprint below; standard parameters (POP_DUR, BOUNCE_FACTOR, SCALE_AMP, etc.) — go to the referenced rule.
- COMPANION_WIDTH: theoretical floor = width of the longest SEQUENCE state in companion font (measure with a probe); 30–50% of viewport width is the practical range. Too small → text leaks past viewport; too large →
FINAL_RECENTER_OFFSETgrows with it and Phase 3 drags. - FINAL_RECENTER_OFFSET: theoretical
-(COMPANION_WIDTH + COMPANION_GAP) / 2, but tuned value is typically smaller in magnitude. Must be tuned by eye and baked as a const, never per-frame (see Phase 3 seam). - TARGET_SCALE: 3× (modest) → 5.5× (cinematic) → 8× (extreme close-up). Constraint: hero source resolution ≥
HERO_SIZE × TARGET_SCALE, otherwise rasters go soft. - BRAND_EXIT_X: magnitude ≥ half viewport width — ensures brand text clears the frame before zoom climax.
- BRAND_FADE_RATIO: 0.3–0.5; brand text disappears before zoom's halfway mark, leaving the climax framed on hero alone.
Critical Constraints (ordered by failure frequency)
- Bake all pre-calculated offset constants:
FINAL_RECENTER_OFFSET,HERO_FINAL_OFFSET_X,HERO_FINAL_OFFSET_Yare allconst. Per-frame recomputation → sub-pixel drift → visible jitter at 5× zoom. - Scale wraps translate (see DOM nesting):
.zoom-scaleouter,.zoom-translateinner. Reversed → translation gets multiplied by scale → hero drifts during zoom. - Companion container width ≥ max text width: overflow during assembly is invisible, but explodes at the final state.
- `baseHeroOffset` excludes heroSize: see Phase 4 seam derivation.
- Breath must be multiplicative: see Phase 5 seam.
- Breath gate: BREATH_START ≥ zoom end + 0.1s: sine and spring tail cannot coexist.
- Measure brand text after `document.fonts.ready`: otherwise hero offset is off by 10–30 px.
- Three nested wrappers only tween GSAP transform aliases (
x/y/scale/rotation) — neverwidth/height/left/top. - Single paused timeline:
gsap.timeline({ paused: true }), registered towindow.__timelines[data-composition-id].
Spring → Ease Selection
Four phases, four spring feels, four ease choices. The complete spring → ease mapping table lives in hyperframes-animation/SKILL.md; this blueprint's defaults are:
- Phase 2 elastic pop →
back.out(BOUNCE_FACTOR)(swap toelastic.outfor rubberier feel) - Phase 3 tight exit →
power3.out - Phase 4 cinematic push →
power2.out - Phase 5 continuous oscillation →
onUpdate Math.sin
Golden Sample
- brand-reveal-assemble-zoom.html — runnable 5-second composition with concrete values for every named constant above; single paused GSAP timeline drives all five phases. Run this first, then change values — much faster than building from scratch.
Comparison · Split Cards (HyperFrames)
A "concept → dual proof" emotional arc: the title states the idea, then two cards arrive from opposite wings with mirrored 3D tilts so the eye reads them as equal weight, and finally a pair of pill badges land at the cards' inner edges to punctuate them. The whole scene runs on one paused GSAP timeline with a single shared scene-ticker for all idle motion, so cards and badges share one rhythm instead of competing on three independent clocks.
When to Use
- Two paired features of equal weight, presented simultaneously rather than sequentially
- Premium "book-open" depth feel from mirrored card tilts adds value to the comparison
- Badges or short labels need to attach visually to each card, not float in empty space
Orchestration
Three phases, two rules. The middle phase is the heaviest.
- Phase 1 — title slide-down: inline. A single
power3.outopacity/ytween. Making this a rule would be overengineering — there's no orchestration to share. Direction isy: -TITLE_RISE → 0so the title drops in from above the final position; that direction matters because the cards in Phase 2 come from the _sides_, and a downward-arriving title plus side-arriving cards forms a non-conflicting T-shape of motion vectors. - Phase 2 — split cards entry: use split-tilt-cards, but in a two-layer DOM variation the rule's template doesn't show:
.card-poscarries the slide-inxandscaleand the idley;.card-tilt(a child) carries the staticrotationYplus the idle rotation float. The rule's single-layer template tweens both on the same element, which works for its yoyo bob but conflicts here because Phase 2's entryxand Phase 3's continuousywould land on the same element and the sine ticker would overwrite the entry's final state. Splitting the transforms across two layers means each tween owns one alias and nothing collides. The tilts are mirrored per the rule (+BASE_TILTleft,-BASE_TILTright) and shadow direction follows from the rule's tilt-shadow principle. - Phase 3 — badges attach: inline
back.out(BOUNCE_FACTOR)spring per badge. This is the floating-badge variation of split-tilt-cards' "Asymmetric content density" section — badges live on the stage, NOT inside the card divs, so they don't inherit the cards'rotationY. One inline tween per badge; making it a rule would not earn its keep. - Continuous (from `t = 0`) — shared scene-ticker: use sine-wave-loop in its `onUpdate` form driven by `tl.time()` (not the rule's preferred "drive a phase tween that starts at IDLE_START_TIME" form). One onUpdate writes y/rotation onto both cards and y onto both badges. The reason for choosing
tl.time()over the rule'sphasetween is laid out in the seam section below; the reason for one onUpdate instead of three is also there.
Phase Timing
All boundaries in seconds. Pick each phase's internal duration first; back-derive starts from the gap constraints below.
| Phase | Start ≥ | Internal duration | Notes |
|---|---|---|---|
| 1 | 0 | TITLE_DUR | TITLE_AT typically 0.1–0.3s; lower = punchier opener, higher = breath |
| 2L | TITLE_AT + TITLE_DUR * 0.5 | ENTRY_DUR | Intentional overlap: title's tail and cards' beginnings co-arrive |
| 2R | LEFT_AT + 0.1–0.4s | ENTRY_DUR | Small stagger; 0 reads mechanical, >0.4s fragments the pair |
| 3L | RIGHT_AT + ENTRY_DUR + BADGE_GAP | BADGE_ENTRY_DUR | Let power3.out tail settle on the cards before badges punctuate |
| 3R | BADGE_LEFT_AT + 0.2–0.4s | BADGE_ENTRY_DUR | Mirror the card stagger feel |
| Idle | 0 (runs the whole composition) | TOTAL_DUR | sin(0) = 0 ⇒ the float is invisible during entries |
Two gaps deserve specific reasoning.
The Phase 1→2 overlap is deliberate: LEFT_AT lands while the title is still tweening (TITLE_AT + TITLE_DUR * 0.5 ≤ LEFT_AT < TITLE_AT + TITLE_DUR). Read separately, "title done THEN cards" feels like two beats; read overlapped, the title's tail and the cards' wings read as one arrival. If LEFT_AT > TITLE_AT + TITLE_DUR, the scene develops dead air.
The Phase 2→3 gap (BADGE_GAP ≈ 0.1–0.3s) is the inverse: badges must NOT overlap the card entry. Cards use power3.out which decays smoothly — popping a back.out badge on top of a card that's still moving makes the badge's overshoot read as the card jittering. 0.1s feels continuous, 0.3s reads as deliberate punctuation. Pick higher when the scene is editorial, lower when it's snappy.
The idle ticker starts at `t = 0`, not at "entry end + buffer" as the rule defaults. This works only because sin(0) = 0: at composition start, every floating offset evaluates to zero, so the cards' y is 0 and the badges' y is 0 — meaning the entry tweens see no floating contribution to fight. By the time entries end, the sine is already mid-cycle and the float is naturally visible. The alternative (gating idle to start after entry settles) would require setting a separate idle-start time per element; running from t = 0 shares one phase across the whole stage.
DOM Nesting (the two-layer card)
The two-layer structure inside each card is the difference between this blueprint and the rule's single-layer template — keep it strict:
.cards-row ← single perspective parent (rule's `perspective` principle)
.card.card-left
.card-pos ← Phase 2 entry x, scale; idle y
.card-tilt ← static rotationY (+BASE_TILT); idle rotation float
.card-image / .card-label / .card-subtitle
.card.card-right
.card-pos ← Phase 2 entry x, scale; idle y
.card-tilt ← static rotationY (−BASE_TILT); idle rotation float
...
.badge.badge-left ← stage-level, NOT inside .card (rule's badge variation)
.badge.badge-rightThe rule's "Shadow direction must match tilt" still applies — left card's box-shadow falls right, right card's falls left. .card-tilt needs transform-style: preserve-3d because it has nested transformed children that should render in the 3D plane.
Phase 2 Seam: Why two layers, not one
If you tween x, scale, AND rotationY on the same element (.card) — which is what the rule's template implies — then the Phase 2 entry tween and the continuous sine-ticker both want write access to that element's transform alias bucket. The entry tween animates x: -SLIDE_DIST → 0; the ticker writes y: sin(t * FLOAT_Y_SPEED) * FLOAT_Y_AMP. GSAP merges these correctly _for one frame_, but the rotation float (rotationY: ±BASE_TILT + sin(...) * FLOAT_R_AMP) wants the same alias the entry tween's mirrored rotationY would otherwise drive. Separate the concerns:
.card-posowns translation (xfor entry,yfor float) andscale(entry only). The ticker writes onlyy..card-tiltownsrotationY(static + float). The ticker writes onlyrotationY.
The static tilt is set once at composition init via gsap.set('.card-left .card-tilt', { rotationY: BASE_TILT }) and the float adds onto it inside onUpdate: gsap.set(leftTilt, { rotationY: BASE_TILT + Math.sin(t * FLOAT_R_SPEED) * FLOAT_R_AMP }). The BASE_TILT term must be re-added every frame because gsap.set writes absolute values, not deltas.
Phase 3 Seam: Badge anchoring
Badges are absolutely positioned on the stage at each card's inner edge — the edge between the card and the gap, not the outer corner and not floating in empty viewport space. The eye must read them as belonging to their card. A rule of thumb: a badge's outer edge should overlap its card by 10–20% of badge width so it visually attaches rather than orbits.
Badges are NOT nested inside .card-left / .card-right because that would inherit rotationY and tilt the badge off-axis with the card — see split-tilt-cards' "Don't put badges inside the card divs" constraint. The trade-off is that the badge's x / y no longer auto-follow the card's idle float; the shared ticker handles that explicitly by writing a small y onto the badge.
Idle Seam: One onUpdate, six writes
Five DOM nodes participate in the idle (two .card-pos, two .card-tilt, two #badge-*). The naive structure is five independent tl.to(...{ onUpdate }) tweens, each evaluating its own sine and writing one alias. That fires five callbacks per frame, with five separate transform-write barriers.
The blueprint uses one tl.to({ tick: 0 }, { tick: 1, duration: TOTAL_DUR, ease: 'none', onUpdate }) over the full composition. Inside the callback, read tl.time() once, evaluate four sines (cards-y, cards-rotation, badges-y, plus the Math.PI phase-opposed companions), and issue six gsap.set calls. The browser batches the transform writes within a single tick — measurably cheaper than five independent tweens, and conceptually the cards and badges share one clock.
The two per-card opposition (`+ Math.PI` between left and right) isn't optional: synchronized floating makes the pair look conveyor-belted. Opposed floating makes them feel like two living things sharing a rhythm. The badges are typically in-phase with each other (both pop in the same direction) because they read as a pair of satellites, not as another comparison axis — but opposing them is a valid stylistic choice if the scene wants extra differentiation.
The four sine frequencies are intentionally non-harmonic. FLOAT_Y_SPEED and FLOAT_R_SPEED for cards differ by ~25% so the y bob and the rotation wobble don't visibly sync into a single rocking motion. BADGE_Y_SPEED runs faster than the card y so badges feel like lighter satellites — heavy bodies move slowly, light bodies move faster. Tune BADGE_Y_AMP < FLOAT_Y_AMP for the same reason: a badge that out-bobs its card looks like a balloon escaping.
Key Values to Choose (Not Already in the Rules)
Standard parameters (BASE_TILT range, BOUNCE_FACTOR range, FLOAT_AMP range, SCALE_AMP range) live in the referenced rules. The values unique to this blueprint:
- TITLE_RISE — pixels above resting position the title starts from. 24–64 px; below 24 barely reads as motion, above 64 reads as "dropped from offscreen" rather than "slid into place from just above."
- ENTRY_SCALE — initial scale of each card before settling to 1. 0.7–0.95. Lower combined with the side-slide reads as "popping into focus from a distance"; higher reads as a near-flat lateral slide. Couples with
SLIDE_DIST: high slide + high entry scale reads as conveyor-belt, low slide + low entry scale reads as zoom-pop. - BADGE_GAP — settle beat between cards finishing entry and badges starting (0.1–0.3s). See Phase 2→3 gap reasoning above.
- GLOW_OPACITY — opacity of the dual-glow ambient (0.08–0.18). Below 0.08 invisible; above 0.18 the glow competes with the card content. The two glow colors are usually the comparison's two brand accents, one centered at 30%, one at 70% — the layout's symmetry axis is at 50%, so the glows reinforce the side-identity without bleeding into each other.
Critical Constraints (ordered by failure frequency)
- Two layers per card (`.card-pos` outside `.card-tilt`): collapsing into one layer is the most common mistake. The static tilt and the rotation float both want
rotationY; the entryx/scaleand the idleyboth want the position bucket. Two layers, two scopes. See Phase 2 seam. - Badges on the stage, not inside cards: nesting badges inside
.card-*makes them inherit the tilt and the float — they end up tilted off-axis and translated twice. Position absolutely on the stage at each card's inner edge. - Cards entered opposite-direction with phase-opposed float (`+ Math.PI`): left enters from
-x, right from+x; left floats sin(t), right floats sin(t + π). Same direction or same phase makes the pair look mechanical. - `BADGE_GAP ≥ 0.1s` between card entry end and badge entry start: badges popping while cards are still settling reads as card jitter, not badge bounce.
- Sine ticker runs continuously from `t = 0`, not gated: the invariant
sin(0) = 0is what makes this safe — gating to start after entry would require per-element start times and lose the shared phase. - `BADGE_Y_AMP ≤ FLOAT_Y_AMP`: a badge that out-bobs its card looks like a runaway accessory.
- Equal card width + shared single `perspective` parent: per-card perspective produces inconsistent depth, and unequal widths break the comparison symmetry that the whole layout is selling.
- Idle ticker writes only one alias per element:
yon.card-pos,rotationYon.card-tilt,yon badges. Mixing inscaleoropacityinside the ticker would overwrite the entry's final state.
Spring → Ease Selection
Three different feels across the three phases — full mapping table lives in hyperframes-animation/SKILL.md.
- Phase 1 title settle →
power3.out - Phase 2 card entries →
power3.out(consistency: the scene's "arriving" feel) - Phase 3 badge pops →
back.out(BOUNCE_FACTOR)(the only spring with overshoot — earns the punctuation) - Continuous idle →
Math.sin(tl.time() * …)in onUpdate, ticker tween usesease: 'none'
Golden Sample
- comparison-split-cards.html — runnable composition with concrete values for every constant above. Single paused GSAP timeline drives all three phases over
TOTAL_DURseconds, with the shared scene-ticker handling cards-and-badges floating. Start here, then change values.
ftypavifavifmif1miaf�meta!hdlrpictpitm4ilocD@����8iinfinfeav01infeav01�iprp�ipcocolrnclx�av1C�ispexxpixi8auxCurn:mpeg:mpegB:cicp:systems:auxiliary:alphaav1C� ispexxpixiipma������irefauxl�mdat
��aP2�<P���Zd��`B����6�Wn�}jsi��Yw���S,�+���� Fx25$~���t��<%������.E�l������`S��<���V�f�������8�1.�@ ;�����~SHQ
�'[�x��
�DnY�����v�\�(��yS�ʝ�ǹ@��n�$��}��p�i���b��JV��QucRl��tR �Sa?��{qn���`i7��1'���%���c"�O���>�f K?^�A0Ӧ�Ag�r3��&�Ls�֨��� �go#��B��5��J�} �L���lX����-j��_��?���ċT!S=l��yu��47L�Ɠ
p���;�����[n0|�l�44���4����.�WL,�\Vw�B�ar?��{�(v�/KV��-�p���Eϻ,�3����E �80\��C��TW��=��\I�t�KBL
8��`�2�
�$�@��/�ĸ���
�,Ku�(�>.M�Ng/Rϵ�N�����cbP%+E4]��9B�q�G���aWP��'����Y�$w!V��6���: �E��d��6�U�s�7���}��XQ�(�D����tC\�.�k���,�O�����N\�S�v��mF#�b��B�(�e�Q�E79b�P�3B��D���Y�dI&,�,b�
�-�%䠂��퇔D�i,"���X���y�c3aS)d`ߴ
��Y?��:�
E�=8}o����[���TC8�Ć�ͻ�L_(��؝��~������M�/����nQ7.r�I3�R�Rׯ:>Yq�a�a�X/���!�߯��L���%r�F����[�D�L"����p����]��4��C�0N�����>���%R�q�-���~�i�����(>Tt��|As�7-��c&P.e�;���xdR�/N����3�c�CK������9I������#x%���+���s'��0�,�ŧBˋ�\[��j�%i�-�X�vg���)�"��'I� �f������Nk����UYi
��,rY��a��}^�{8M������
��-���W ����-zV��` 8�D�=$��
q7qX?���t�(Px<��8��7]e�t� �(�Y�-ݟ�x&�y��r|�!�A�&��p?��1�W^�0q�����9"����v#����ԜRx����5/`�OFR�2�X�]6�:[~hJeW�:;�q|�)y
��4����8�R�B�Qj��������@�!�_�
�-�P��վ��칃r�ߔ�$��)?�ٵ����b(w�/m����|F�%� }s�&y~��3u�����3S_:MaF��������)���D�&��B�|'��_��D\@
�S8�zO5Fb���[�gmN���o�H������Ks+�'���!�f*#�y����T��w;@��)�X�%�Kz/�>tiWA��m�=����g��Uͺ��)�ˤ���+����5���+
d���7.�KA��%S���Xd9T���R��jؗ�����A�$�:�����������Q��yTx�x�G��U.hmd23�� �-�-�v�(f��ʿCX���t�`�˄u���T,!��pM*�'�V�r�yBv��'�F����`���T��5@�<[+��N�y�1����j�M#�����˘�{�o���wRV��'�"�51+u ����:t���~�����e��k�IQ�V���ή�I��cg���0E�+[m�S0�0��xHaa��{�!���S���E���4��U�U�%�ܛs��{OE05�QH6.'zxg{a�9�$Nl�5s���2_�G
=���5�q1N�aDd�oK/���`���s�?�r�*S=�أk���M>C`Q��X>�\��!�@%eA��Ÿ��V�N((%<3���n��tJx�G_���|G�5���☟eld�LZ
�9��S�X�q�Ʒ)�����+���_�R%3�
��fB{S}�4��I`��1���3k(�j��t饉I�wl{�
á%�n�Q�8r8����:�""<���+ixʆ�z�l$-��~�₮q^�ub�u"��Z%����d��a��z���H�<co8��)��{���r ���*��[�8�T��ftypavifavifmif1miaf�meta!hdlrpictpitm4ilocD@��h{8iinfinfeav01infeav01�iprp�ipcocolrnclx�av1C�ispe
ppixi8auxCurn:mpeg:mpegB:cicp:systems:auxiliary:alphaav1C� ispe
ppixiipma������irefauxl/mdat
!�;�T2�TP�V��%���|��g�L�(y�ۢ��@(--���<Y�k�����m���Z�*�/^�����}���� Qw�}M�1�`��z���uG�H��F`>���00�2;N��BK�g��ɏ���ؕ��>�!�UN ѨM��:("3yE��̈́
��ס�5���m��(E�_ؑ���r�eʲE�#M;��D���gDU<$��«�P��Q����F�����<.t��TE��{��g���
�B��oyCl%�C�8��K''3M��L6�'��~:r���}\ߌB���6���:�������fhj�x�u�,p&�B}��I"u▐�p
.ig�o&g8?<�u�}��3iD+<��S��c՚���v�n�}fTT"���Br@���i�s�(
���g@ꦄy��� �x8��"����#�܈�J�q�>���y?'A:�O��s�D��ݣ3t@;|^��S��l?{��-�~Ƣ60��߹��6�������'Z�f]���ָ�J[�דw>@��֣~5�2d0ۋۺ�b�XW�V��A��\��{�x�A��g�n�f��]�����!��8u[��8�N�Fo'���N�DrN0&*PN����Vw���Ӫ7�^f���T������jxs�h���wF/~.�� �K0�=��#p�ޡ�4��9�Q�j�1��F�
�F$FN$Io`�1�E���u���������ܹ�\]Q���Q�H]�<2_P�1��y����'�
٥"z$M��NrRڭ/u�@���f���D�����+A��f�P�d}�[���нc�/�UV�C�u�ɩ��]z�u�2�|R��w������6}�6Ɯ�O:(/��烠��x_o�D�Ss;�i×+bMxp�-4�t�l�Z�9���
8!�;�$2�D��+u���5�r
/9?�/:�я����4�����|xo��)xy@�CÚ9�.܆;0�����kg�S��==pN�J�UR��U���D�-U ��������Jه�x��\��n;����8�w��,�z{r8_�<n���[��9Rߟ����Ln����D�h���A_�������k���o"B��
��6�rM����d6�� E}Y0ޟ�_�P���r����~gO���َoNA2���Dy:�߂��MU�C�+����;l�-C{n��*P���7�W�4^��<J��[�$ޥ�`��O��g��6:5�M�K��1����-�gU=�簵�l���0�@��]��I�xt`ftypavifavifmif1miaf�meta!hdlrpictpitm4ilocD@���n8iinfinfeav01infeav01�iprp�ipcocolrnclx�av1C�ispeopixi8auxCurn:mpeg:mpegB:cicp:systems:auxiliary:alphaav1C� ispeopixiipma������irefauxl[mdat
!�[�T2�D���V��$!`��[~�
�^Lc��:�ɇ��KhCP`�܃������b���~�Mo`%N��B��<�B�ү��$��c�!d&���pA %G�K��s!s��]
�yR�늹�jT"5��h���9�^���Q}}v\w��:����Rl�~F�ު��1_��<��d���H�F?���F��*Ƣ�p��k���/�������B��D�}�J��
�>U�+>���,Ek�ۊ<��`W���a���zM��
�u���)�~���q�#���Nr�B#����퇊��,��W@�uW�m����M�{���r�z���Z2ք6�k/���c8Y@
T[@�l������L�ҵ��ᄆ��%�;���� ��Q��ZT������V�n.@���K7Ag�vgV?�}n�h�j���'�'����!8/ɠ��?�3�KH�ߚ��8 =V�?pSSdL�흭�Z)��J��4#�L��<���PИ.�oR䓆��}����np`��$�����'P����W�j�EZ��@�֏9䎁<'���Yt�0�8���~o:�{=#���>C�ɿ�!��Pd7l�� r�����χ�xD��k�җ*���AV�A0�D9�7��yF����[b�Nm4���\OU��!=�3��0�E��,^�
��R���͜�f���T����$A��w���K���r}��U�m�
�:b6�ԧ"�҂��*qD�;�gȞO
Z���`?��B�Gl�~j9�������׃d��
I'?W��i���"3r��?u�����ٍC��j_���m�M8�5�x�ݩf�[���l�����]��~���8���T`f��-.�X��I�G0b[��c�=*!}�C�kL��O?-�Ӱ
1���F��A/�}�ކ4P�����
�7#�`c�hC
��iQ�n��n����rلT�X>S��z
8!�[�$2�D��+u���5�r
/SO�ݟ���?���ml���b��v��0���
�G���������(��C ��wV�O��z2|Y��A<���3w]/pc2�$��}�6H��Fס+~�
�z�9�,�- d0��JJJP�o�\Sϊ�N[{����7`k���VQ&��P��-�q�'����)�K�����cEX�WS��<��!���G=�BЅ'�g�� ���^"��^l�|�����J��H�!�� &% Z*��E4��N�m��c�At1XW���mo�8� P�L#�5�F
ǐ{2����@���0*w�0���}T�����5~d1��H?�$x�l-��k�|ftypavifavifmif1miaf�meta!hdlrpictpitm4ilocD@�,�B8iinfinfeav01infeav01�iprp�ipcocolrnclx�av1C�ispeopixi8auxCurn:mpeg:mpegB:cicp:systems:auxiliary:alphaav1C� ispeopixiipma������irefauxlvmdat
!�[�T2�LP����1�Y�dy�Ӏ3�1�TJ����h�%�� ���P�e�jI0���VǼ��ߡ�_��Y�4r0;?��N,{�ߪ*�BT�W�{Y+l�5���|/X�!@^��I��tQ��JS��%���t:��A�朝�vO%�L
D�s�w��r"~||6��s�������9�r5I��̤�
���.��Y
ovN��e��S�f�r� �I��
}��癬@���ȡ1DdH������w!� w��GC�%�1*�bD������8�2>�Tpn0~څ���k�ƓƷ�aVdz���Du����=����(�����l��CM@_�m�2���T=ԥ����&�A�o�mI�a���e�6?X�˗�p}�N=��� �Ц�3�?��` V��G�(�4Wzw.!�7��J��#��E��G�1TϨ'�}�r%*�4m-��t�v����
�� @�~\������P�l'��-J
q���Z�X�̱�����S�2.^���pͱ����hu|��.��Ѱ�Ʃux9�t���c�IM+0>�0C�T�HNFo� �q,�ug��n�ڲ�)�F�w��Q�3�%9$�p��}�.�3G�Z���H��|��Q�@Z6�Ɲ�/�K�[BPs��W����m),7��}�r.뚇#<0�52�BQT����x�+��K�j�[S?�ӧ訓���ƁI2.V�S���M�h��W��+���W��@����䕅�.�!ͩ��i����b��e/��K�^� �'q^�F&"͊��&��cX
8!�[�$2�D����;��],�
�s�[��4]ypA�\E��e�Q+�r�LXÓ!(tT�>���>�2�_`�=g�>m�3�?�������X�]H�j���'�vp��;���0����'W�N�w�8p���-��|��o[��b��¬�F���1!}�N�M����Hp0/���]�X X��g!�o�OG��.0A�ϋJ��!� �)�R��Y@]G�Re�����)�/)��:�h�/�%/
q�?��-,�t���'�?
j]�,\��v�e}�%,�J�5���\{7S�t��T?�Hu�A�N��+�y�3D
3D Card Flip
180° Y-axis rotation. Requires CSS: backface-visibility: hidden; transform-style: preserve-3d; on both scene-inners. Parent needs perspective: 1200px.
tl.set(new, { rotationY: -180, opacity: 1 }, T);
tl.to(old, { rotationY: 180, duration: 0.6, ease: "power2.inOut" }, T);
tl.to(new, { rotationY: 0, duration: 0.6, ease: "power2.inOut" }, T);
tl.set(old, { opacity: 0 }, T + 0.6);Grid
Grid Dissolve
Grid of colored cells covers the frame in a ripple from center. Scene swaps at 50% coverage. Cells fade out in ripple.
12-cell (4x3, each 480x270): standard 120-cell (12x10, each 160x108): dense variant — lower opacity (0.75), tighter ripple
Cells are created dynamically in JS, sorted by distance from center for ripple stagger.
Related skills
How it compares
Use hyperframes-animation for atomic motion and runtime APIs; pair with hyperframes-core for composition structure and hyperframes-creative for brand direction.
FAQ
What animation libraries are supported?
GSAP, Lottie, Three.js, Anime.js, CSS keyframes, and WAAPI. Each has adapter patterns for consistent authoring.
What does seek-safe mean?
Animations can jump to any frame without playing intermediate frames, essential for video rendering where you need frame-accurate output.