diff --git a/skills/website-to-hyperframes/references/step-6-build.md b/skills/website-to-hyperframes/references/step-6-build.md index c04c5991e..519e47f79 100644 --- a/skills/website-to-hyperframes/references/step-6-build.md +++ b/skills/website-to-hyperframes/references/step-6-build.md @@ -164,3 +164,64 @@ These exist because the capture engine is deterministic. Violations produce brok - **Never use ANY CSS `transform` for centering** — not `translate(-50%, -50%)`, not `translateX(-50%)`, not `translateY(-50%)`. GSAP animates the `transform` property, which overwrites ALL CSS transforms including centering. The element flies offscreen. Use flexbox centering instead: `display:flex; align-items:center; justify-content:center` on a wrapper div. The linter catches this (`gsap_css_transform_conflict`) but only if you run it. - **Minimum font sizes**: 20px body, 16px labels - **No full-screen dark linear gradients** — H.264 banding + +--- + +## Load-bearing rules for animation authoring + +Rules below came out of two independent website-to-hyperframes builds (2026-04-20) where compositions lint-clean and still ship broken — elements that never appear, ambient motion that doesn't scrub, entrance tweens that silently kill their target. The linter cannot catch these; the rules must be followed by the author. + +- **No iframes for captured content.** Iframes do not seek deterministically with the timeline — the capture engine cannot scrub inside them, so they appear frozen (or blank) in the rendered output. If the source you're stylizing is a live web app, use the screenshots from `capture/` as stacked panels or layered images, not live embeds. + +- **Never stack two transform tweens on the same element.** A common failure: a `y` entrance plus a `scale` Ken Burns on the same ``. The second tween's `immediateRender` resets the first, and the element ends up invisible or offscreen without any lint warning. Fix one of two ways: + + ```html + + + + + + + + +
+ + ``` + +- **Prefer `tl.fromTo()` over `tl.from()` inside `.clip` scenes.** `gsap.from()` sets `immediateRender: true` by default, which writes the "from" state at timeline construction — before the `.clip` scene's `data-start` is active. Elements can flash visible, start from the wrong position, or skip their entrance entirely when the scene is seeked non-linearly (which the capture engine does). Explicit `fromTo` makes the state at every timeline position deterministic: + + ```js + // BRITTLE: immediateRender interacts badly with scene boundaries + tl.from(el, {opacity: 0, y: 50, duration: 0.6}, t); + + // DETERMINISTIC: state is defined at both ends, no immediateRender surprise + tl.fromTo(el, {opacity: 0, y: 50}, {opacity: 1, y: 0, duration: 0.6}, t); + ``` + +- **Ambient pulses must attach to the seekable `tl`, never bare `gsap.to()`.** Auras, shimmers, gentle float loops, logo breathing — all of these must be added to the scene's timeline, not fired standalone. Standalone tweens run on wallclock time and do not scrub with the capture engine, so the effect is absent in the rendered video even though it looks correct in the studio preview: + + ```js + // BAD: lives outside the timeline, never renders in capture + gsap.to(".aura", {scale: 1.08, yoyo: true, repeat: 5, duration: 1.2}); + + // GOOD: seekable, deterministic, renders + tl.to(".aura", {scale: 1.08, yoyo: true, repeat: 5, duration: 1.2}, 0); + ``` + +- **Hard-kill every scene boundary, not just captions.** The caption hard-kill rule above generalizes: any element whose visibility changes at a beat boundary needs a deterministic `tl.set()` kill after its fade, because later tweens on the same element (or `immediateRender` from a sibling tween) can resurrect it. Apply to every element with an exit animation: + + ```js + tl.to (el, {opacity: 0, duration: 0.3}, beatEnd); + tl.set(el, {opacity: 0, visibility: "hidden"}, beatEnd + 0.3); // deterministic kill + ```