GSAP's stagger { each: 0.02, from: 'center' } gives every element a UNIQUE
20ms slot in center-out order (total span = each * (n-1) = 3.52s for 177
countries; measured live: 21/57/93/121/155 countries visible at
t=1.5/2.23/2.93/3.5/4.19s, complete at 4.7s). anime.stagger(20,
{ from: 'center' }) scales delay by DISTANCE from center instead, so
equidistant pairs share a slot and the reveal finished at t=2.93s -- the
whole map filled roughly twice too early, matching the damaged t=1.68-4.61s
band against the baseline.
Precompute the GSAP rank order (sort by |i - center|, ties lower-index
first) and pass explicit per-element delays. Live probe after the fix:
21/58/93/121/156/177 -- within one country of GSAP at every checkpoint.
Snapshot frames at t=2.23/2.93/4.19 now match the pinned baseline.
The GSAP original assigned window.__timelines['devices-canvas'] inside
onReady() -- only after both DRACO/GLTF device models finished loading and the
screens were captured -- so the render pipeline's readiness poll waited for
the models before capturing frame 0. The anime.js port moved registration to
script-parse time, letting the pipeline start capturing while the models were
still loading: frame 0 rendered an empty pedestal, and early-window frames
drifted against the baseline.
Move hyperframesAnime.register() into onReady() (after captureScreens), which
also makes the registration-time priming pass paint from a fully-loaded scene,
and drop the duplicate parse-time registration.
Verified by snapshot at t=0/9.13/11.08 vs the pinned GSAP baseline: 9.13 and
11.08 now match exactly (device positions and captured screen content); t=0
shows the authored hero state while the baseline's own opening 0.6s is black
(GSAP-side model-load race at the pinned fork).
Same class as the vfx-shatter fix: hyperframesAnime.register() primes the
timeline (seek to end, back to 0), so the deferred init's tl.seek(0) is a
no-op that never fires onUpdate, and the WebGL canvas stays black for the
first captured frame (t=0 hard-fail at 3.3dB, baseline shows the light
landing page). Call renderPortal(0) directly.
Verified by snapshot at t=0/3.3/3.9 vs the pinned GSAP baseline: t=0 now
shows the landing page. The remaining PSNR gap on this item is baseline-side:
the pinned GSAP baseline's drawElementImage capture is missing ALL text
(logo, nav, hero headline, card copy) for the entire clip, while the anime.js
candidate captures it correctly.
hyperframesAnime.register() primes the timeline (seek to end, then back to 0),
so by the time the deferred init has captured the HTML panels into WebGL
textures, tl.currentTime is already 0 and its tl.seek(0) is a no-op that never
fires onUpdate. Nothing repaints the canvas until the first nonzero seek,
leaving the very first rendered frame solid black (single damaged checkpoint
at t=0, 19.5dB, baseline shows the full hero). Call renderFrame(0) directly
instead of seeking.
Verified via snapshot at t=0/0.5/2.0 against the pinned GSAP baseline: frame 0
now shows the hero and matches.
The engine's browserManager launches render Chrome with
--enable-features=CanvasDrawElement globally, but the snapshot command's own
puppeteer launch omitted it, so any composition using ctx.drawElementImage()
(vfx-shatter, vfx-portal, ios26-liquid-glass, ...) showed its 'API required'
placeholder in snapshots while rendering fine in the real pipeline.
hyperframesAnime.register() primes each timeline by seeking to its end and
back to 0. morph-text drives its word swap from an onUpdate proxy tween with
manual DOM writes, so the mid-word text painted by the priming seek has
nothing for anime.js to restore on the return seek and the wrong headline
stuck at t=0 (baseline shows 'Do more with less.', port showed a later word).
Re-assert applyMorph(0) after registration, and normalize the modulo in
applyMorph so a float-rounding hair-below-zero T from the return seek cannot
floor to the last word. Applied to both demo.html and the distributable
morph-text.html (same pattern in both twins).
Verified by frame comparison against the pinned GSAP baseline at t=0, 10.03,
10.5 and 12.0: t=0 now matches; the t=10-14.9s soft cluster is blur-morph
antialiasing jitter with identical content.
The GSAP source used a plain 1D stagger (stagger: { amount, from }, no grid),
which on a row-major 16x9 element list produces a horizontal-band reveal
(middle rows first). The anime.js port added an explicit grid: [16, 9] option
that wasn't in the original, switching anime.js to true 2D geometric
distance and producing a radial/circular reveal instead, a visibly different
wipe shape confirmed against the pinned baseline at t=2.7s.
Removing the grid option restores the original array-index-distance stagger
and matches the baseline's band-shaped reveal.
anime.js v4.5.0 cannot interpolate a text-shadow tween whose from/to values
have a mismatched layer shape (a 1-layer base shadow vs. a 3-layer glitch
shadow in caption-glitch-rgb, or "none" vs. a 3-layer glow in
caption-neon-glow): it silently freezes at the last-applied value (and can
scramble color channels) instead of animating, so glitch-rgb's per-word RGB
split never resets and neon-glow's glow never appears at all.
Confirmed live via getComputedStyle probes seeking the registered timeline at
10ms increments, and by frame comparison against the pinned GSAP baseline.
Fix both by tweening only plain numbers (--glitch-off/--glitch-alpha,
--glow-r/-g/-b/-alpha) and letting CSS reconstruct the shadow, sidestepping
anime's compound-value interpolation entirely.
The prior implicit-transform-from fix corrected EVERY implicit-from tween
matching a decomposed transform property, not just the first one on a given
target. Confirmed via a real render + PSNR A/B test against the commit
before that fix: registry/examples/product-promo's nested
scene1-logo-intro.html animates each Figma logo piece with two tweens on
the same target+property (an instant duration:0 jump off-screen, then an
implicit-from "converge to center" tween) -- the second tween correctly
inherits its "from" from the first tween's just-set value inside anime.js,
but the prior fix overwrote it with the element's true CSS/page-load
identity value instead, making the piece appear already-assembled at the
center from frame 0. This regressed product-promo's PSNR from ~50.5dB to
~47.0dB.
Only apply the CSS-cascade correction to the tween with the smallest
`_absoluteStartTime` for each distinct (target, property) pair; every later
implicit-from tween on that same pair is left untouched, since anime.js
already chains it correctly from the preceding tween. Verified: the
product-promo regression is resolved (51.2dB, back above baseline) while
flowchart/kinetic-type retain their original fix's improvement.
anime.js v4.5.0 cannot correctly resolve a literal "none" clip-path target
when an earlier tween on the same element used a structured circle()
shape. Confirmed via internal tween inspection: it silently re-templates
"none" as a zeroed copy of the preceding circle()'s numeric parameters
(`_toNumbers` become `[0, 0, 0]`, reusing the prior string template),
rendering as `circle(0% at 0% 0%)` -- a near-fully-clipped, effectively
black/empty box -- instead of the intended fully-revealed state. This is
baked in at `.add()`-call time, before any seek ever runs, so it cannot be
fixed by mutating tween fields afterward.
Patch every timeline's `.add()` to rewrite a literal `clipPath: "none"`
into an equivalent fully-open `circle(150% at X% Y%)` (same shape function
anime already interpolates correctly, radius large enough to cover any
aspect ratio) whenever a tracked preceding value for that element was a
circle() shape. Leaves "none" untouched when there is no preceding
structured shape to conflict with (anime resolves that case correctly on
its own), and leaves every other property/shape untouched.
Verified against registry/blocks/transitions-light and
registry/blocks/transitions-radial (the confirmed "Family B" black/hidden
content defect from the U-final adjudication) with real render+compare-item
PSNR improvement; no regression on the other Family B members
(vfx-portal/vfx-shatter/play-mode), which do not use this clip-path pattern
and remain open with a different, not-yet-identified root cause.
anime.js v4.5.0 does not read an element's CSS-cascaded `transform` value
when resolving an implicit "from" for decomposed transform sub-properties
(translateX/Y, scale, scaleX/Y, rotate) authored without an explicit
`[from, to]` array. It silently defaults to identity instead, so any
element relying on a stylesheet rule like
`.node { transform: translate(-50%, -50%) scale(0); }` to start hidden
renders at (or near) its final position/scale from the very first frame.
Confirmed via isolated puppeteer reproduction: this is baked into anime's
internal tween node (`_fromNumber`) at `.add()` time, independent of seek
order or priming.
Correct each affected tween's `_fromNumber`/`_number` with the true
CSS-cascaded value (decomposed via DOMMatrixReadOnly) before the instance's
first-ever seek, in the existing per-instance priming pass. Verified
against registry/blocks/flowchart and registry/examples/kinetic-type
(same "content resolves ahead of schedule" defect family from the U-final
adjudication) with real render+compare-item PSNR improvement.
The anime.js adapter registers every composition and sub-composition as a
flat entry in window.__hfAnime and seeked ALL of them with the same raw
global time. Unlike GSAP (where sub-composition timelines are nested inside
a parent timeline and inherit correct local time from that nesting), each
anime.js registration needs its own composition's cumulative start time
subtracted before seeking, or a sub-composition mounted at a nonzero
data-start renders far ahead of where it should be (confirmed: at global
t=1.6s with data-start=1.5s, the child received seek(1600ms) instead of the
correct seek(100ms)).
Resolve each registration's host via data-composition-id, subtract its
cumulative start (reusing the existing runtime start-time resolver), and
clamp to the authored/host duration before seeking.
Verified: registry/examples/product-promo average PSNR vs GSAP baseline
20.5dB -> wait, 33.3dB -> 50.5dB (damaged checkpoints 77 -> 32) with no
regressions in the other items spot-checked. Full regression suite passes
(82 files, 1168 tests) plus a new red/green test for the nested-seek case.
x-post.x-card-frame statically centered both axes (translate(-50%,-50%)),
but the GSAP baseline's gsap.to(card,{y:0}) only preserves the axis it
never animates (x); the animated y axis loses the CSS percentage
centering entirely, pinning the card's top edge at raw top:50% instead
of true vertical center. Drop the Y half of the wrapper's static
transform (translateX(-50%) only) so the anime.js port lands at the
same asymmetric resting position as the pinned GSAP baseline.
Verified: avg PSNR vs baseline 20.5dB -> 53.2dB, hard-fail checkpoints
(<30dB) 87 -> 0; residual low-band checkpoints sit at ~44dB (just under
the 45dB soft threshold), consistent with font/antialiasing render
noise rather than a positional defect.
The studio browser bundle imported the parsers and core package roots
(via classifyAnimationRuntime and resolveEase), dragging in node-only
assetPaths through the barrel under vite's import condition. Adds
animation-runtime-detection and ease-map subpath exports and points the
browser imports at them; repo-wide oxfmt pass for files agents missed.