* feat(cli): one owner for init media placeholders, audio stays on the video Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(cli): init keeps a video's sound on the clip; silent files scaffold muted Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com> Co-Authored-By: kapelame <kapelame@users.noreply.github.com> * feat(registry): example templates keep the a-roll's sound on the video Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): video audio rules recommend keeping sound on the video Update video_missing_muted and video_audio_double_source message and fixHint text to recommend the new default: keep audio on the video with data-has-audio="true" (not muted). Tests verify the new wording is in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(parsers): a video is either audible (data-has-audio) or muted, never both Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): follow the compiler's audio rule in the audio-track toggle Confirm before background removal drops a video's sound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): hyperframes-core teaches sound on the video by default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): workflows and scaffold guidance keep a video's sound on the clip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: footage with sound keeps it on the video clip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): init from-file a-roll is timed, muted strip is attribute-exact The from-file a-roll video gets data-start="0" so a scaffold lints clean, and the orphaned audio-only slot comment is dropped. setVideoAudio now walks the tag attribute by attribute, so a class value containing the word muted survives. The ffprobe-missing warning says the video is assumed to have sound and how to switch to muted, and DEFAULT_META notes the failure surfaces at render until lint support lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): keep has-audio in step with the muted toggle, test cutout confirm Muting a video now clears has-audio and unmuting sets it to true, in both the Design and flat media panels, matching the Has audio track toggle. Audio elements are unchanged. Adds coverage for both panels plus the cutout window.confirm guard: declined leaves src alone, accepted swaps src and mutes, a silent video never prompts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false An audio element or an undefined hasAudio no longer picks up a muted attribute; only an explicit false on a video writes muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: video_missing_muted rule text, silent-footage notes, consistent audible examples lint.mdx and cli.mdx describe the current video_missing_muted rule instead of the retired unmuted-video wording. The editing recipes say how to adapt for silent footage and require the outgoing clip's sound to end at a J-cut. The Remotion media example shows the audible mapping consistently, data-attributes states that has-audio is required unless muted, and the hairline frame note no longer claims the video is muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): encode init media filenames and probe --audio duration Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded. --audio now passes the probed track length as the composition duration in both init paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): audio-state-aware media rules and time-aware double-source check video_missing_muted honors any explicit data-has-audio. video_audio_double_source only considers audible videos and only fires when the same-src clips overlap in time. duplicate_audio_track now covers audible videos. audio_file_without_element no longer claims silence when an audible video carries the sound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): audio-stream check for remotion ports, followable J-cut recipe Remotion Video maps to data-has-audio only when the file has an audio stream. The J/L-cut recipe now shows both shots with muted pictures and separate audio. embedded-captions removes the audio.mp3 scratch file it extracts. skills-manifest.json regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(lint): trim comments to satisfy the comment ratchet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): atomic muted edits, cutout keeps sound on a linked audio Muted and Has audio track commit one batched write (one persist, one undo entry) and remove data-has-audio instead of writing an empty marker. Unmuting a video probes its src: audible -> true, silent -> false, probe failed -> absent. Remove background on an audible video inserts a linked sibling <audio> with the original sound in the same write, replacing the drop-the-sound confirm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(producer): skip hidden videos when auto-adding video audio A video hidden in Studio is dropped by the mixer, so its src never reached existingAudioSrcs and the render re-added it at full volume. The video parser now records the shared hidden predicate and the auto-detect step skips hidden videos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): keep useDomEditSession under the 600-line cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com> Co-authored-by: kapelame <kapelame@users.noreply.github.com>
16 KiB
Visual Techniques Reference
13 proven techniques from production HyperFrames videos. Use these in your storyboard and compositions to create visually rich, professional output. Each technique includes a minimal code pattern you can adapt.
These are NOT advanced — they're standard motion design patterns that every composition should use at least 2-3 of.
Contents
- SVG path drawing
- Canvas 2D procedural art
- CSS 3D transforms
- Per-word kinetic typography
- Lottie animation
- Video compositing
- Character-by-character typing
- Variable font axis animation
- GSAP MotionPathPlugin
- Velocity-matched transitions
- Audio-reactive animation
- Clip-path reveal masks
- WebGL fragment shader art
- When to use what
Capturing live HTML/CSS as a GPU texture (3D rotation + bloom, magnetic warp, shatter, liquid surface, portal, plus GLSL post-processing) is a separate, heavier capability — see
adapters/html-in-canvas-patterns.md. Use it for 1–3 hero beats per video, not every beat.Easing vocabulary — the full ease-family palette (character + mood mapping) lives in
adapters/gsap-easing-and-stagger.md. Every composition should use at least 3 different easings.Named text-animation effects (24 IDs like
typewriter,kinetic-center-build,soft-blur-in) come from the externalanimate-textskill — seeadapters/animate-text.mdfor the vocabulary and how to load it.
1. SVG Path Drawing
A path draws itself in real-time, like someone tracing with a pen. Use for revealing diagrams, arrows, connector lines, or brand marks.
<svg viewBox="0 0 400 200">
<path
class="draw-path"
d="M 50 100 L 200 50 L 350 100"
stroke="#c84f1c"
stroke-width="4"
fill="none"
stroke-linecap="round"
/>
</svg>
<style>
.draw-path {
stroke-dasharray: 280;
stroke-dashoffset: 280;
}
</style>
<script>
tl.to(".draw-path", { strokeDashoffset: 0, duration: 0.7, ease: "power2.out" }, 0.5);
</script>
Use path.getTotalLength() to calculate the dasharray value dynamically.
2. Canvas 2D Procedural Art
Animated noise, particle fields, data visualizations — anything that evolves frame-by-frame. Drive it with a GSAP proxy.
<canvas id="proc-canvas" width="1920" height="1080"></canvas>
<script>
var canvas = document.getElementById("proc-canvas");
var ctx = canvas.getContext("2d");
function hash(x, y) {
var n = x * 374761393 + y * 668265263;
n = (n ^ (n >> 13)) * 1274126177;
return ((n ^ (n >> 16)) & 0x7fffffff) / 0x7fffffff;
}
function drawFrame(t) {
ctx.fillStyle = "#0a0a0a";
ctx.fillRect(0, 0, 1920, 1080);
for (var i = 0; i < 200; i++) {
var x = hash(i, 0) * 1920;
var y = hash(i, 1) * 1080;
var brightness = hash(i, Math.floor(t * 10)) * 255;
ctx.fillStyle = "rgba(255, 255, 255, " + brightness / 255 + ")";
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fill();
}
}
var proxy = { time: 0 };
tl.to(
proxy,
{
time: 5,
duration: 5,
ease: "none",
onUpdate: function () {
drawFrame(proxy.time);
},
},
0,
);
</script>
The hash() function is deterministic — same frame renders identically every time.
3. CSS 3D Transforms
Perspective rotations create depth. Use for product showcases, card flips, architectural reveals.
<div class="stage" style="perspective: 900px;">
<div class="card-3d" style="transform-style: preserve-3d;">
<div class="face front">Product</div>
<div class="face back" style="transform: rotateY(180deg);">Details</div>
</div>
</div>
<script>
tl.to(".card-3d", { rotationY: 360, rotationX: 15, duration: 1.2, ease: "sine.inOut" }, 0);
</script>
Always set perspective on the parent, transform-style: preserve-3d on the animated element.
4. Per-Word Kinetic Typography
Words appear one-by-one, synced to transcript.json timestamps. The core technique for narration-driven videos.
<div class="headline">
<span class="word w-0">Anything</span>
<span class="word w-1">a</span>
<span class="word w-2">browser</span>
<span class="word w-3">can</span>
<span class="word w-4">render</span>
</div>
<style>
.word {
display: inline-block;
opacity: 0;
margin: 0 0.12em;
}
</style>
<script>
// Word onset times from transcript.json (seconds relative to beat start)
var timings = [0.0, 0.23, 0.28, 0.63, 0.78];
var slides = [80, 60, 50, 25, 12]; // horizontal slide decay (px)
document.querySelectorAll(".word").forEach(function (word, i) {
tl.from(
word,
{
x: slides[i],
y: 14,
opacity: 0,
duration: 0.35,
ease: "power2.out",
},
timings[i],
);
});
</script>
The slide distance DECAYS per word (80→12px) — mimics a camera settling.
5. Lottie Animation
Vector animations that play inside a composition. Use for logos, character animations, icons.
<div id="logo-anim" class="lottie" style="width:500px;height:500px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.12.2/lottie.min.js"></script>
<script>
window.__hfLottie = window.__hfLottie || [];
const anim = lottie.loadAnimation({
container: document.getElementById("logo-anim"),
renderer: "svg",
loop: false,
autoplay: false,
path: "../capture/assets/lottie/animation-0.json",
});
window.__hfLottie.push(anim); // REQUIRED — adapter seeks every registered instance
gsap.set("#logo-anim", { scale: 0.3, opacity: 0 });
tl.to("#logo-anim", { scale: 1, opacity: 1, duration: 0.35, ease: "back.out(1.6)" }, 0.2);
</script>
autoplay: false + window.__hfLottie.push() are mandatory — HyperFrames seeks each registered player to composition time, so a player left on autoplay runs in wall-clock and renders non-deterministically. loop: true is seek-safe: the adapter wraps composition time into the animation's own length, so a short cycle repeats for the whole scene. There is no playback-rate scaling: bake non-default speed into the Lottie asset. Characters (walk cycles, mascots, gestures) and the full .lottie/dotLottie contract: adapters/lottie.md.
6. Video Compositing
Embed real video footage inside compositions. Videos are playsinline; add muted for silent footage or b-roll, or data-has-audio="true" when the clip's own sound should play.
<div class="video-frame" style="width:680px;height:840px;border-radius:16px;overflow:hidden;">
<video
id="footage"
src="../capture/assets/videos/clip.mp4"
muted
playsinline
style="width:100%;height:100%;object-fit:cover;"
></video>
</div>
<script>
// Video playback is controlled by the framework — don't call play() manually
tl.from(".video-frame", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.out" }, 0);
</script>
The HyperFrames runtime handles video seeking and playback.
7. Character-by-Character Typing
Terminal typing effect using tl.call() to update text content character by character.
<div class="terminal-line">
<span class="prompt">❯</span>
<span class="typed" id="typed-text"></span>
<span class="cursor" style="width:11px;height:22px;background:#333;display:inline-block;"></span>
</div>
<script>
var CMD = "npx hyperframes init";
var typed = document.getElementById("typed-text");
// Cursor blinks
tl.to(".cursor", { opacity: 0, duration: 0.12, yoyo: true, repeat: 20, ease: "steps(1)" }, 0);
// Type each character
for (var i = 0; i < CMD.length; i++) {
(function (idx) {
tl.call(
function () {
typed.textContent = CMD.substring(0, idx + 1);
},
null,
(idx / CMD.length) * 0.9,
);
})(i);
}
</script>
Use ease: "steps(1)" for cursor blink — creates discrete on/off.
8. Variable Font Axis Animation
Animate font-variation-settings to reshape glyphs in real-time. Works with variable fonts that have axes like optical size (opsz), weight (wght), softness (SOFT).
<style>
/* Load the captured local variable font — do NOT use Google Fonts @import.
Replace this placeholder with an @font-face pointing to ../capture/assets/fonts/. */
@font-face {
font-family: "Fraunces";
src: url("../capture/assets/fonts/Fraunces-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: block;
}
.wordmark {
--opsz: 144;
--wght: 440;
font-family: "Fraunces", serif;
font-variation-settings:
"opsz" var(--opsz),
"wght" var(--wght);
font-size: 200px;
}
</style>
<script>
tl.to(".wordmark", { "--opsz": 72, "--wght": 300, duration: 0.45, ease: "power2.out" }, 0);
</script>
The glyph subtly reshapes as axes animate — optical size adjusts detail, weight changes thickness.
9. GSAP MotionPathPlugin
Animate an element along an arbitrary SVG path. Use for sliders following curves, particles along trajectories, guided reveals.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js"></script>
<div class="dot" style="width:20px;height:20px;background:#2a8a7c;border-radius:50%;"></div>
<script>
gsap.registerPlugin(MotionPathPlugin);
tl.to(
".dot",
{
motionPath: { path: "M 12 300 C 280 280 520 80 820 50 S 1200 48 1308 38" },
duration: 1.5,
ease: "power2.out",
},
0,
);
</script>
10. Velocity-Matched Transitions
Exit one beat and enter the next with matched velocities — creates perceived continuous motion.
// EXIT (in outgoing composition): accelerating with blur
tl.to(
".content",
{
y: -150,
filter: "blur(30px)",
opacity: 0,
duration: 0.33,
ease: "power2.in", // accelerates
},
beatDuration - 0.33,
);
// ENTRY (in incoming composition): decelerating from blur
gsap.set(".content", { y: 150, filter: "blur(30px)" });
tl.to(
".content",
{
y: 0,
filter: "blur(0px)",
duration: 1.0,
ease: "power2.out", // decelerates
},
0,
);
The fastest point of both curves meets at the cut — the viewer perceives smooth camera motion. Match ease families: .in for exits, .out for entries.
11. Audio-Reactive Animation
Drive any GSAP-tweenable property from the playing audio. Bass pulses a logo on kick drums. Treble glows a CTA on cymbals. Amplitude breathes a background during quiet phrases. The result: motion that feels locked to the track in a way pre-authored tweens never can.
When to use: Any video with music or dramatic narration — brand reels, product launches, hype edits. Skip for calm/tutorial pacing.
How it works: Pre-extract audio frequency bands into a JSON file, then sample per-frame via tl.call():
// audio-data.json: { fps: 30, totalFrames: 900, frames: [{ bands: [0.82, 0.45, 0.31, ...] }, ...] }
for (var f = 0; f < AUDIO_DATA.totalFrames; f++) {
tl.call(
(function (frame) {
return function () {
var bass = frame.bands[0]; // 0–1
var treble = frame.bands[13];
gsap.set(".logo", { scale: 1 + bass * 0.04 }); // 3–4% pulse on bass
gsap.set(".cta", { filter: `drop-shadow(0 0 ${treble * 24}px #00C3FF)` });
};
})(AUDIO_DATA.frames[f]),
[],
f / AUDIO_DATA.fps,
);
}
Per-frame sampling is required — a single tween will not react. Use the extract script:
python3 skills/hyperframes-creative/scripts/extract-audio-data.py narration.wav --fps 30 --bands 16 -o audio-data.json
Keep text/logo intensity subtle (≤5% scale, ≤30% glow) — audio-reactive motion on tiny elements reads as jitter. Bigger backgrounds can push to 10–30%.
Never do: equalizer bars, spectrum analyzers, waveform displays, strobing, rainbow color cycling. The audio provides timing and intensity; the visual vocabulary still comes from the brand. See skills/hyperframes-creative/references/audio-reactive.md for the full API and anti-patterns.
12. Clip-Path Reveal Masks
A fixed window that content slides through — text or images enter from one side and are clipped by an invisible boundary. Different from SVG path drawing: the mask is static, the content moves.
<div id="reveal-mask">
<div id="reveal-content">Your headline text here</div>
</div>
<style>
#reveal-mask {
position: absolute;
inset: 0;
clip-path: inset(0 200px 0 0); /* clips 200px from right */
display: flex;
align-items: center;
justify-content: center;
}
#reveal-content {
font-size: 108px;
white-space: nowrap;
}
</style>
<script>
// Content starts offscreen right, slides left through the mask window
gsap.set("#reveal-content", { x: 400, opacity: 0 });
tl.to("#reveal-content", { x: 0, opacity: 1, duration: 1, ease: "power2.out" }, 0);
</script>
Variations: clip-path: circle(0% at 50% 50%) → circle(100%) for iris reveals. clip-path: polygon(...) for custom shapes.
13. WebGL Fragment Shader Art
Full GPU generative backgrounds — domain-warped FBM noise, cosine palette coloring, iridescent organic patterns. Far richer than Canvas 2D.
<canvas id="shader-bg" width="1920" height="1080"></canvas>
<script>
var canvas = document.getElementById("shader-bg");
var gl = canvas.getContext("webgl");
if (!gl) {
/* fallback to gradient */
}
var fsrc = `
precision mediump float;
varying vec2 v_uv;
uniform float u_time;
uniform vec2 u_res;
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i+vec2(1,0)), f.x),
mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), f.x), f.y);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
mat2 R = mat2(0.8, 0.6, -0.6, 0.8);
for (int i = 0; i < 5; i++) { v += a*noise(p); p = R*p*2.02; a *= 0.5; }
return v;
}
vec3 palette(float t) {
return vec3(0.5)+vec3(0.5)*cos(6.28318*(vec3(1)*t+vec3(0.0,0.33,0.67)));
}
void main() {
vec2 uv = v_uv; uv.x *= u_res.x/u_res.y;
float t = u_time * 0.4;
vec2 q = vec2(fbm(uv*3.0+t*0.3), fbm(uv*3.0+vec2(5.2,1.3)+t*0.2));
vec2 r = vec2(fbm(uv*3.0+q*4.0+vec2(1.7,9.2)+t*0.15), fbm(uv*3.0+q*4.0+vec2(8.3,2.8)+t*0.1));
float n = fbm(uv*3.0+r*2.0);
vec3 col = palette(n*2.0+t*0.2);
col = mix(col, palette(length(q)*3.0+t*0.1), 0.4);
col *= 0.7+0.3*n;
float vig = 1.0-0.4*length(v_uv-0.5);
gl_FragColor = vec4(col*vig, 1.0);
}
`;
// Compile, link, set up fullscreen quad, then render via GSAP proxy:
var proxy = { time: 0.5 };
tl.to(
proxy,
{
time: 5,
duration: BEAT_DUR,
ease: "none",
onUpdate: function () {
gl.uniform1f(uTime, proxy.time);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
},
0,
);
</script>
Always include a Canvas 2D gradient fallback for environments without WebGL.
When to Use What
| Video energy | Techniques to combine |
|---|---|
| High impact (launches, promos) | Per-word typography + velocity transitions + counter animations |
| Cinematic (tours, stories) | SVG path drawing + video compositing + 3D transforms |
| Technical (dev tools, APIs) | Character typing + Canvas 2D procedural + MotionPath |
| Premium (luxury, enterprise) | Variable font animation + Lottie + slow velocity transitions |
| Data-driven (stats, metrics) | Canvas 2D procedural + counter animations + SVG path drawing |