From 44653e186abf36b95c635826245bb3468bfec04b Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 8 Jul 2026 21:26:46 -0700 Subject: [PATCH] docs(figma-skill): verbatim motion translation, wrap-marker decoding, export_video validation Field lesson from translating a real Motion timeline: the two returned encodings window durations differently, and keyframes at times ~0.9999 are loop-wrap resets, not authored motion. Hand-normalizing across encodings and inventing visible returns produced a render that diverged from Figma. The skill now mandates verbatim single-encoding translation, wrap-via-repeat, and a frame-grid comparison against export_video ground truth before completion. Co-Authored-By: Claude Fable 5 --- skills-manifest.json | 2 +- skills/figma/SKILL.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 49bbd956b..ce45e5ca0 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -10,7 +10,7 @@ "files": 18 }, "figma": { - "hash": "ee1408c58b76db8e", + "hash": "17dd858344329586", "files": 1 }, "general-video": { diff --git a/skills/figma/SKILL.md b/skills/figma/SKILL.md index 3184b2c5a..7d2a70f28 100644 --- a/skills/figma/SKILL.md +++ b/skills/figma/SKILL.md @@ -88,7 +88,8 @@ Node tree → editable HTML at exact figma geometry, packaged as a registry item No REST equivalent exists. You drive the MCP tools, then hand output to the pure helpers in `@hyperframes/core/figma`: 1. `get_motion_context(fileKey, nodeId)` — use `recursive:true` on the parent frame (one call for the whole scene, not one per element). Save the raw JSON next to the project (`.media/figma-cache/`) so retranslation is free. -2. Normalize into a `MotionDoc`: per animated property a `MotionTrack` { property (motion.dev name), values, times (0..1), ease[] (named or `[x1,y1,x2,y2]` bezier), duration, repeat }. Selector = the element's stable id (`#` from Phase-3 output or the authored scene). +2. Normalize into a `MotionDoc`: per animated property a `MotionTrack` { property (motion.dev name), values, times (0..1), ease[] (named or `[x1,y1,x2,y2]` bezier), duration, repeat }. Selector = the element's stable id (`#` from Phase-3 output or the authored scene). **Translate values VERBATIM — never paraphrase, simplify, or invent keyframes.** Decoding rules (field-tested): the response carries two encodings — the motion.dev snippet is the timeline-cohort window (all tracks share the cohort duration), the CSS snippet may stretch per-track durations; use ONE encoding consistently (prefer motion.dev, cohort-windowed). Keyframes at times ≈0.9999→1 are **loop-wrap markers** (the instant reset at the loop boundary), NOT authored motion — drop them and realize the wrap via the tween's `repeat` restart; inventing a visible return/fade-out where the source has a wrap snap is the known failure mode. + 2b. **Validate against ground truth before calling it done**: `export_video` on the cohort's `rootNodeId` gives Figma's own render of the timeline. Extract a frame grid from both videos at the same interval (e.g. `fps=5` contact sheets) and compare — element positions, fade states, and rotation phase must match frame-for-frame. A translation that hasn't been compared to the export is unverified. 3. `motionToGsap(doc)` → `emitTimelineScript(spec)` → inject as a `