Subchapter 21.4
references/determinism-rules.mdMarkdown7 KBView on GitHub
HyperFrames seeks compositions frame-by-frame. Every frame must be reproducible from its time value alone — same input time → same pixels. Three contracts enforce this: the animation runtime contract, the determinism rules, and the .
GSAP is the primary runtime. The core requirement is generic: animation state must be seekable from HyperFrames time.
For GSAP:
gsap.timeline({ paused: true }).window.__timelines["<composition-id>"], keyed by the composition root’s data-composition-id. You do not need to write window.__timelines = window.__timelines || {} first: the runtime creates the registry before your inline scripts evaluate.document.fonts.ready(...) and friends are the documented setup path. What you must not do is register the key before the build finishes. An empty timeline registered early is treated as ready and nested empty, so the animation renders blank (lint: gsap_timeline_registered_before_async_build, error). Assign window.__timelines[id] = tl at the end of the callback, after the tweens are added, and optionally call window.__hfForceTimelineRebind() right after.data-composition-id, the runtime still binds it when it is the only registered timeline. With two or more registered, a mismatched key leaves the render frozen at t=0.tl.play() for render-critical motion.data-duration on the clip instead.Use the hyperframes-animation skill for tween syntax, position parameters, eases, and performance rules. Non-GSAP duration inference lives in hyperframes-animation/adapters/.
Rendered frames must be reproducible from the requested time. Do not use any of the following for visual state:
Date.now(), performance.now(), or any render-time clock.Math.random(). Use a seeded PRNG if random-looking placement is needed.repeat: -1. Compute a finite count: repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1) — floor, not ceil (ceil overshoots data-duration and trips the gsap_repeat_ceil_overshoot lint; max(0, …) avoids a negative repeat = infinite).Also avoid:
display, raw visibility, or autoAlpha on a clip element: HyperFrames timing owns a clip’s visibility, and lint rejects it (gsap_animates_clip_element). Fade with opacity, or tween a child wrapper. Do not tween class="clip".lint enforces a denylist, so filter, clipPath, strokeDashoffset, width, height and similar are all legitimate targets. Prefer transforms and opacity where you have the choice, for performance rather than correctness. The per-runtime detail lives in hyperframes-animation/adapters/.Build the visible end-state in static HTML and CSS first, then animate from/to that state.
data-duration before scripts run, like data-width / data-height. A script or --variables value that rewrites the root data-duration afterward is ignored. To vary render length per output, author the root data-duration directly. (A clip’s own data-duration is re-read from the live DOM, so scripts/variables can still drive clip lengths. Only when the root omits data-duration does the renderer probe the live DOM / timeline for total length.)width: 100%; height: 100%; box-sizing: border-box.max-width for layout. Avoid positioning main content with hardcoded top/left offsets when a layout container can do it.position: absolute for layers and decorative elements, not as the default content-layout strategy.max-width, wrapping, or window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight }).window.__hyperframes.pretext. Measure off a canvas instead of writing into the page and reading it back, so nothing reflows: pretext.prepare(text, font) then pretext.layout(prepared, maxWidth, lineHeight) → { lineCount, height }. prepare does the font measurement; everything downstream of a prepared string is arithmetic and cheap enough to run per frame. fitTextFontSize is built on it.
layout gives you height, not width. To size a container to its text (shrinkwrap), use pretext.prepareWithSegments(text, font) and then pretext.measureNaturalWidth(prepared) for the single-line width, or pretext.measureLineStats(prepared, maxWidth) for { lineCount, maxLineWidth }.font is a CSS font shorthand string, e.g. "700 90px Inter".clearCache and setLocale are deliberately not exposed: they mutate state shared across compositions, which would make a render depend on what ran before it.<br> in body text. Forced breaks ignore the actual rendered font width and produce an extra break when the line already wraps naturally, causing overlap. Let text wrap via max-width. Exception: short display titles where each word is deliberately on its own line.transform/scaleX/scaleY is a no-op on an inline <span>, and scaling an auto-width (0px) element shows nothing → invisible bars/fills. Give them display: block/inline-block/flex-item and a real width/height (e.g. width: 100% inside a sized parent). (Silent — automated gates may miss it.)yoyo scale, back.out) need clearance at their peak size and must not straddle an overflow: hidden edge — else they overlap a neighbor or get clipped. Position for the largest frame, not the resting one. (silent.)