Every SKILL.md already declared the new name — name: frames, frames-cli, frames-core, and so on — but the directories were never moved, so the tree carried frames-* in content and hyperframes-* on disk. Everything that resolves a skill by path missed: the core-skill list, the plugin marketplace pin, and the docs the CLI reads at runtime. Eight tests failed on main because the files they open do not exist. The directories now match what the files say they are, and the guide the claude-design contract reads takes the name that contract already used.
Tier 3 — stargazed-data-driven
What it tests
A purpose-built data-driven fixture that exercises the realistic shape of a production Remotion composition without using the runtime adapter from PR #214. If a translation passes T3, the skill correctly handles:
- A
<Composition>with az.objectschema and typeddefaultProps - Custom React subcomponents reused with different props across scenes
- A nested data structure (
stats[]) materialized as repeated HTML with per-instance attributes - A frame-driven count-up animation (
AnimatedNumber→ GSAPonUpdate) - Two different
springconfigs translated to two differentback.outovershoots - Per-instance delays via component props (
delayInFrames→ GSAP timeline offsets)
Composition shape
Stargazed (10 s @ 30 fps, 1280×720)
├── Sequence 0–3 s TitleScene
│ ├── title ← spring scale
│ └── subtitle ← linear fade
├── Sequence 3–7 s StatsScene
│ ├── StatCard "Stars" 1247 #fbbf24 (delay 0 frames)
│ ├── StatCard "Forks" 312 #60a5fa (delay 12 frames)
│ └── StatCard "Issues" 48 #f87171 (delay 24 frames)
└── Sequence 7–10 s OutroScene
└── UnderlinedText "thanks for watching" ← scale-in underline
Each StatCard is a custom subcomponent that internally uses AnimatedNumber
to count from 0 to the target. AnimatedNumber itself derives the displayed
value from useCurrentFrame() + a manual 1 - (1 - t)^3 ease.
The lossy parts (and why threshold = 0.90)
-
spring → back.out(N): two different spring configs in this composition.{ damping: 12, stiffness: 100, mass: 1 }(title) →back.out(1.4){ damping: 14, stiffness: 90, mass: 1 }(stat card) →back.out(1.2)
Overshoot ratio (1.4 vs 1.2) approximates the damping difference. The late-tail curve of GSAP's back ease and Remotion's spring don't match exactly — costs ~0.03 mean SSIM per spring instance.
-
Count-up easing:
AnimatedNumberuses1 - (1 - t)^3(cubic ease-out) manually computed in the component. GSAP'spower3.outis the same curve shape — should match closely. The displayed integer is rounded each frame in both renderers; minor mismatches occur when the rounded value flips between two numbers on a sub-frame timing difference. -
Font rendering: same caveat as T1/T2. System Helvetica/Arial fallback produces minor anti-aliasing differences between renderers. Affects the stat card numbers (large weight 800) most.
A mean SSIM below 0.90 in T3 indicates a structural mismatch (wrong scene durations, wrong stagger timing, missing prop wiring), not approximation drift. That's the failure signal we care about. The calibrated mean against Remotion @ 4.0 with PNG/BT.709 output is 0.953.
Translation walk-through (skill cheat sheet)
| Remotion | Frames |
|---|---|
<Composition schema={z.object({...})} defaultProps={...} /> |
data-* attributes on root #stage div |
nested array prop (stats[]) |
repeated HTML markup with per-instance data-* attrs |
| custom React subcomponent | inline repeated HTML using the component's prop interface as the template |
<AnimatedNumber from={0} to={value} dur={45} /> (cubic ease-out count-up) |
tween on { v: 0 } object with onUpdate rewriting textContent, ease power3.out |
spring({damping:12, stiffness:100}) |
back.out(1.4) over ~0.7 s |
spring({damping:14, stiffness:90}) |
back.out(1.2) over ~0.7 s |
delayInFrames={i * 12} (per-instance) |
GSAP timeline offset (i * 0.4) s |
useVideoConfig() to get fps |
dropped — composition fps is in data-fps on #stage |
How to render and evaluate
# Render Remotion baseline (no setup.sh — no binary assets in this fixture)
cd remotion-src && npm install && npm run render
# Render Frames translation
cd ../hf-src && npx @hanzo/frames render --output ../hf.mp4
# Compare
../../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff