🪟 The browser frame
A persistent window surrounds each screenshot — it gives the video a “browser” feel and disguises the fact that the content is a screenshot.
The window #appwin lives in the bg-layer with data-layout-ignore: border, radius, large shadow, title bar with 3 dots (red/yellow/green), and a mono URL pill with a lock + window.urlLabel. The screenshots (1 clip per scene) go inside from it, in (WIN_L, SHOT_T) with size VW×VH.
"window": {
"top": 96, // WIN_T — topo da janela no canvas
"titleH": 52, // altura da barra de título
"urlLabel": "localhost:8000"
}
// derivados no build-demo.mjs:
WIN_L = (1920 - VW) / 2 // centraliza horizontal
SHOT_T = WIN_T + TITLE_H // topo do screenshot
- ✓Mark the frame with
data-layout-ignore - ✓Use the
urlLabelreal captured app - ✓Keep the persistent window (don’t recreate it for each scene)
- ✗Draw the screenshot outside the window area
- ✗Load the live site inside the frame (rendering is deterministic)
- ✗Animate the frame as if it were a scene
➡️ The animated cursor and the path to the target
A single global cursor glides along the main timeline and lands exactly in the center of the bounding box real control.
The capture runs in a fixed viewport (e.g., 1280×800)—every bbox is in this space. One point (sx, sy) of the screenshot becomes canvasX = WIN_L + sx, canvasY = SHOT_T + sy. The cursor target is the center of the mapped bbox.
// hotspot na ponta da seta ≈ (6,3) num SVG de 42px
const HOT = { x: 6, y: 3 };
// a PONTA cai no centro do alvo, não o canto
tl.to("#cursor", {
x: alvoX - HOT.x,
y: alvoY - HOT.y,
duration: 0.7,
ease: "power3.inOut"
}, s.start + 0.35); // deixa a cena aparecer antes
The bounding box comes from getBoundingClientRect() during capture—not from a guess "by eye." That’s exactly what makes the cursor look professional: it lands on the right pixel of the button or field.
👆 The click and the visual effect
When the cursor "clicks," a pulse on the cursor itself and an amber ripple communicate the action to the viewer.
~start + 0.35scursor startsThe cursor moves from its previous position toward the target, letting the scene appear first.
~start + 1.05sreaches the targetThe pointer lands at the center of the bbox; the highlight is already pulsing around the control.
~start + 1.15sclickCursor does scale:.82 in yoyo (pulse) and the #ripple (amber circle) expands and disappears at the center of the target.
Read-only steps (without click:true) don't trigger the ripple—good for the home screen (intro:true), where you only present the app without interacting.
🔍 Zoom and highlight the active region
The highlight draws the eye to the control; the zoom adds a cinematic push-in when the result appears.
.hlbox (destaque)
Rectangle only with box-shadow (amber ring + glow) on the mapped bbox with ~6px of clearance. Comes in with back.out and pulses the glow a few times — draws focus without covering anything.
zoom: true
In the results step, the <img> receives scale 1 → 1.12 throughout the narration, with transformOrigin in the center of the target (e.g., the results panel).
{
"shot": "04-result.png",
"target": { "x": 668, "y": 128, "w": 484, "h": 324 },
"zoom": true,
"caption": "O resultado aparece pronto para baixar.",
"narration": "E pronto: o resultado já está disponível."
}
The final content step usually shows the result with zoom:true — a light "ta-da" that closes the walkthrough arc before the CTA.
🎨 Premium dark amber house style
A fixed palette of 6 colors, with amber as the only dominant accent — the visual identity of all the videos.
ONE dominant accent: amber. It appears in the click ripple, the highlight ring, the background glow, and the CTA. Identical background in every scene — never flat.
Persistent background layer with data-layout-ignore: breathing amber radial glow, giant drifting ghost text, hairline grid, and grain. Large scale: 64–172px headline, decorative elements at 12–25% opacity.
🔤 The embedded fonts
Sora, Inter, and JetBrains Mono as .woff2 local—never CDN, because rendering is offline and deterministic.
Sora 700–800Titles and headlines (geometric, high impact).
Inter 400–600Body and captions (high legibility).
JetBrains MonoURL pill, values, and code.
The render environment may not have internet access. External sources fall back and break the visuals. The fonts come from assets/fonts/ (the Latin subset covers PT-BR); the build-demo.mjs injects the fonts.css rewriting the paths.
⏱️ Each step's timing matching the narration
The cursor responds to the narration: it reaches the target before the explanation and clicks at the right moment, at a pace defined by LEAD/TAIL/FADE.
LEAD 0.5s — the scene appears before the voice starts.
TAIL 0.7s — breathing room after the audio ends.
FADE 0.4s — transition between scenes.
The cursor reaches the target ~0.7s before the main explanation; the click lands near start + 1.15s.
Also, video is tiring, and capture/editing becomes a major task. 5–8 steps + CTA ≈ 35–50s—the ideal length for a walkthrough.
🎯 What you learned
- ✓The frame
#appwinwraps the shot and gives it a browser look - ✓The global cursor targets the real bbox, with the hotspot at its tip and curved easing
- ✓The click triggers an amber pulse + ripple; the zoom pushes in on the result
- ✓Dark premium amber palette and local Sora/Inter/JetBrains fonts
- ✓LEAD/TAIL/FADE timing syncs the animation with the narration
Next: Module 3.2 — how to generate local narration with Kokoro and put it all together with the build-demo.mjs.