Problem: a three.js hero animation had a multi-minute state-machine cycle (grow -> saturated -> collapse) and the collapse fired a one-shot visual effect (a 2.5s leaf gust). Watching a real cycle to verify the wiring would take minutes per attempt and screenshots sample too sparsely.
What worked:
- If the render loop derives dt from performance.now() (not the rAF callback timestamp), patch the clock before page load:
page.evaluateOnNewDocument(() => { const orig = performance.now.bind(performance); const t0 = orig(); performance.now = () => t0 + (orig() - t0) * 8; }). Note per-frame dt clamps (e.g. min(dt, 0.1)) cap the effective speedup at clamp * fps. - Stack any in-scene fast-forward on top: this viz sped up 4x while the mouse was held, and the multiplier applied AFTER the dt clamp, so a synthetic
canvas.dispatchEvent(new MouseEvent('mousedown'))(works even through pointer-events: none) took the total to ~24x. - Effects driven by the rAF timestamp (not performance.now) keep running at 1x under the patch, which is exactly what you want for catching a short one-shot effect: the accelerated state machine triggers it often, and it stays on screen at real duration.
- Detect the event across a screenshot series by domain signals, not diffs: in dark mode the tree collapse showed as mature-tree -> young-tree between consecutive frames, with the leaf field asymmetrically blown to one side.
Also caught a real bug this way: a pointermove listener attached to an absolutely-positioned background layer never fired over the hero copy, because the copy overlay was a sibling stacked above it, not a child; events do not bubble across siblings. Attach hero-wide pointer listeners to the shared parent section.