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).
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.
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.
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.
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.
Group A keeps the 15 metadata-only shader-transition demos on GSAP with explicit runtime
fields because their sole GSAP timeline is a WebGL or Three.js progress driver with no
author motion to port. The pre-existing gsap_timeline_registered_before_async_build lint
finding on code-shader-dissolve is unrelated to this change; only its registry-item.json
was touched, not its .html file.
Group B ports transitions-3d, transitions-grid, and transitions-distortion fully to
anime.js, sets runtime to animejs, and removes the GSAP dependency from those
compositions.
Group C splits transitions-destruction: title/outro card motion now runs on a separately
registered anime.js timeline, while GSAP remains for the burn-canvas driver and coupled
scene crossfade, so the registry runtime remains gsap.