The U-final CSS-cascade correction picked the earliest IMPLICIT tween per
(target, property), so an implicit fade tween stacked after an explicit
burst tween (e.g. apple-money-count's confetti: translateY [0, y] then an
implicit drift-and-fade) was rewritten to the page-load identity value,
discarding anime.js's correct chain from the burst's end value and
collapsing the burst mid-flight (22.0dB at t=4.42s vs the GSAP baseline).
Correct an implicit tween only when it is the first tween of ANY kind on
its (target, property) pair. Verified by full re-render: apple-money-count
t=4.42s recovers to 50.0dB and vfx-liquid-glass's t=0.2-0.6s low band
recovers to 45.1dB; the product-promo stacked-implicit case that motivated
the earliest-implicit narrowing keeps its behavior (unit-tested).
Same defect class as the earlier world-map and parallax fixes: GSAP's
stagger { each, from: "center" } assigns every element a UNIQUE slot,
ordered center-out (total span = each * (n-1)). anime.stagger(value,
{ from: "center" }) instead scales delay by distance from center, so
equidistant pairs share a slot and the reveal finishes in about half
the time.
us-map: live probe (GSAP fork-point timeline vs anime, same instants)
showed the old anime reveal at 6/46/50/50 states visible at
t=1.197/2.393/3.59/4.787s against GSAP's 4/24/44/50 -- the state map
was fully lit two seconds before the GSAP baseline. Precomputed rank
delays now match GSAP exactly at every probed instant (4/24/44/50).
spain-map: same fix for the region reveal and region-label stagger.
Old anime reveal was fully lit (19/19 regions) by t=2.393s vs GSAP's
17/19; fixed version now shows 18/19 at that instant (within the
one-element tolerance already established by the world-map fix).
primeAnimeInstance() seeks every registered timeline to its full
duration and back to 0 at registration time, purely to force
lazily-rendered CSS onto elements before the composition's first real
seek. anime.js fires tl.call()/onComplete callbacks as a side effect
of any seek that crosses their position, including this internal
priming seek -- so a composition using tl.call() (or a raw onComplete
tween) renders as if it had already reached that callback's end state
from frame 0 onward, regardless of the time actually requested later.
anime.js's own Timer.prototype.seek(time, muteCallbacks) already
suppresses onBegin/onComplete/onUpdate/onLoop/onRender for a given
seek; pass true on both priming seeks instead of reimplementing that
suppression. Widened RuntimeAnimeInstance.seek to accept the optional
second argument.
Confirmed against liquid-glass-widgets (a tl.call() status-text swap
scheduled at t=5.2s was rendering already-fired at t=1.434s) and
flowchart/flowchart-vertical (their addTimelineCall()/onComplete
helper had the whole diagram, cursor, and text-correction sequence
visible from frame 0). Added a regression test exercising the same
tl.call()-during-priming scenario directly against the adapter.
The validate static guard greps compositions for GSAP usage; the explanatory
comment added with the deterministic-entrances fix mentioned the original
API by name and produced a false 'uses GSAP but no GSAP script is loaded'
error. Reword the comment.
The intro title was hidden with the individual CSS 'scale: 0' property.
anime.js animates scale through its own inline transform and never clears
the individual property, so the two multiplied and the headline stayed at
scale 0 for the whole clip (the cleanest Family B evidence on record: the
sibling pink pill rendered while the headline was missing at t=0.9). GSAP
folds individual transform properties into its transform management, which
is why the identical authoring worked in the baseline.
Drop the CSS 'scale: 0' (opacity 0 already hides the title) and drive scale
explicitly from the timeline (scale: [0, 1.1]). Also pin the ambient wobble
to rotate: ['0deg', '-3deg']: the pinned baseline renders the entrance
untilted because GSAP dropped the stylesheet rotate(-5deg) when taking over
the transform (its known axis-loss behavior, reproduced for parity, per the
x-post precedent).
Verified by snapshot at t=0.9/1.3/2.49 vs the pinned baseline: headline now
present and matching; note the baseline clip is 10s while the composition
is 15.6s (end card beyond baseline coverage).
Scene entrances and problem fragments fired wall-clock anime.animate() calls
from the visibility controller (mirroring the GSAP original's gsap.from()).
Autoplaying animations run on anime's own rAF engine, which does not advance
under the seek-driven render pipeline, so slide content rendered stuck at
opacity 0 or mid-fade for the entire scene: the ADOPTION STRATEGY slide was
completely empty at t=63.7s and the MARKET SIZE footnote missing at t=41s,
while the GSAP baseline only survived because GSAP's ticker happened to keep
ticking between captures.
Compute entrance and fragment progress as a pure function of composition
time inside updateVisibility (same authored motion: 400ms/70ms-stagger
outCubic entrances, 350ms fragment slides) -- deterministic under seeking in
both directions, and the interactive slideshow path behaves the same.
Verified by snapshot at t=9.72/41.03/63.7 vs the pinned baseline: all slides
now render their full content; the only residual difference is fragments
mid-authored-fade where the baseline shows them prematurely settled.
Same defect class as the world-map fix: GSAP's stagger { each, from:
'center' } assigns each card a unique slot in center-out order (span =
each * (n-1)), while anime.stagger(value, { from: 'center' }) scales delay
by distance from center so equidistant pairs share slots and the cascade
finishes in half the time.
parallax-zoom: all 15 cards were lit by t=0.36s vs the GSAP baseline's
~0.9s completion, matching the damaged t=0.12-0.6s opening window. Live
probe after the fix: 1/5/9/13/15 cards visible at t=0.12/0.24/0.36/0.48/0.6
-- on the GSAP schedule; snapshots at 0.36/0.42 match the baseline pattern.
parallax-unzoom: same stagger on the non-focus card fade-in at t=2.0s,
matching its damaged t=2.27-3.16s window; snapshots at 2.39/2.57/2.86 now
match the baseline cascade.
(The parallax-zoom snippet twin has no card stagger; demo-only.)
The exit wipe used one multi-target add with delay: anime.stagger(40). For
any word whose reveal window overlaps the group exit window (the last words
of each caption group), anime bakes a duplicate exit tween node into the
word's reveal child with the implicit 'from' resolved to the PRE-REVEAL
hidden state (inset right 100%), and that node clobbers the reveal -- the
word never appears ('starts' stayed invisible through t=0.85-1.15s while the
GSAP baseline shows it wiping in; verified via anime internals dump:
duplicate clip-path node from [0,100,0,0] at st=120 inside the reveal child).
Replace the staggered add with a per-word loop at explicit positions
(g.end - 0.2 + i*40), which produces correctly chained tweens. Probed
computed clip-paths now match GSAP math exactly: w3 inset right 25.4% at
t=0.96 (was 100%), w23 exit left inset 17.56% = inCubic(0.56) at t=6.69,
w24 overlap blend matches GSAP replace values. Applied to both twins.
Residual damaged checkpoints at t=5.58/6.13-6.21 are sub-200ms group-exit
transients right before the group visibility cut, not missing content.
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.
onUpdate on a plain JS-object tween target only re-fires while the seek
position is inside that specific tween's own active window, unlike
tracked inline-style properties which the core adapter's priming/
restore logic re-derives for any seek. Without an explicit hold at
t=0, primeAnimeInstance's seek(duration) pass ran every tween once
(setting the search label to its final text and the slider to 82%),
and seek(0) never re-invoked those onUpdate callbacks since 0 sits
outside their windows, so both stayed stranded at the final value
across the whole opening portion of the clip.
Seed both the slider value and a new searchPhase value with a
duration:0 identity tween at t=0 (same onUpdate) so seeking back to
the opening frames re-fires the callback and reads back "Search"/34%.
Also converts the two tl.call() text swaps to explicit-from/to phase
tweens, since call() side effects don't participate in the seek
lifecycle at all.
The GSAP source seeded #lt-name/#lt-role with an explicit initial set
(opacity 0, x -10) before building the timeline. The anime.js port
dropped that seed and used implicit-from {opacity: 1, translateX: 0}
tweens instead, which have no CSS rule or earlier keyframe to read a
hidden state from, so the tween was a no-op and both text lines were
visible from frame 0 instead of animating in at t=420/500ms.
Give the reveal tweens explicit from/to arrays so the pre-reveal state
is correctly established.
Add an anime.js v4 anime_template (and anime_template_horizontal/vertical for
push-slide) alongside each gsap_template in the transition registry, vendored
identically into product-launch-video, pr-to-video, and faceless-explainer.
transitions.mjs now detects the target composition's runtime and stamps GSAP
onto window.__timelines["main"] or anime onto
hyperframesAnime.get("main").instance accordingly, defaulting to anime per the
anime-first contract. Anime templates use explicit [from, to] arrays,
decomposed transform keys, ms-based tokens, and percentage transformOrigin
pairs (a real Chrome anime.js 4.5.0 bug: keyword syntax silently resolves both
endpoints to (0,0)).
<hyperframes-player> couldn't play a raw anime.js composition from src: the
inline hyperframesAnime.register() call throws before the runtime exists,
and the existing late-injection fallback only fires for GSAP's __timelines
and appends the runtime after parse, which is too late for a call that
already ran.
Add a runtime-src attribute to override the runtime URL, and a pre-parse
path in the probe: once it observes a same-origin composition that
references hyperframesAnime but never got a runtime/timeline/registry,
fetch the original src, inject the runtime script + a <base> tag ahead of
the composition's own scripts, and reload via srcdoc (once per src).
shouldInjectRuntime now also refuses to late-inject once an anime registry
is present, since that path can never help these compositions.
The T1-T3 hf-src goldens and sequencing.md still authored GSAP timelines
while the skill's api-map/timing/transitions docs and the porter itself
already default to anime.js. Convert each golden's paused GSAP timeline to
anime.createTimeline({ autoplay: false }) + hyperframesAnime.register,
translating positions/durations to milliseconds and easing names via the
documented GSAP->anime.js table, while preserving the original tween
structure (implicit vs explicit from-values) exactly. Re-measured SSIM
against the Remotion baselines and refreshed expected.json's calibration
numbers to match.
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.