Animation speed is frame-rate dependent (no delta-time) — runs in slow motion when FPS dips
Summary
The particle simulation advances by a fixed step per frame rather than by elapsed time, so the animation's wall-clock speed is tied directly to the delivered frame rate. When the page's frame production is uneven (busy main thread, background tabs, variable-refresh displays, heavy compositing), the confetti visibly slows down and speeds up instead of playing at a constant speed.
Details / root cause (v1.9.4)
- Velocity decays a fixed factor per frame:
fetti.velocity *= fetti.decay(src/confetti.js,updateFetti). - Lifetime is counted in frames (ticks), not time:
progress = (fetti.tick++) / fetti.totalTicks, alive whilefetti.tick < fetti.totalTicks. - The rAF loop uses the timestamp only as a maximum frame cap:
var TIME = Math.floor(1000 / 60)andif (lastFrameTime === time || lastFrameTime + TIME - 1 < time) { ... }. This prevents >60fps from running fast, but when frames arrive slower than ~16.6ms it still advances exactly one fixed step — so the effect plays in slow motion. There is no delta-time compensation.
Reproduction
- Fire confetti while the main thread is under variable load (e.g. a video-heavy app, or simply generate input/render churn on the page during the burst).
- Observe the animation speed fluctuate — slow, then normal, then slow — over a single burst. On a 120Hz display vs a throttled tab the difference is also visible.
Expected
The animation should play at a consistent wall-clock speed (e.g. its current ~60fps-equivalent rate) regardless of the actual delivered frame rate.
Suggested fix
Make the simulation time-based using the timestamp already passed to the rAF
callback (onFrame(time)):
- advance
tickbydelta / TIMEinstead of1, - apply decay as
velocity *= Math.pow(decay, delta / TIME), - scale gravity/drift contributions by
delta / TIME.
To preserve backwards compatibility, this could be gated behind an opt-in option
(e.g. frameRateIndependent: true on the global/fire options) rather than changing
default behavior.
Notes
Not a duplicate of #114 (prefers-reduced-motion), #235 (pause at frame), or #230 (mobile). This is specifically about frame-rate independence of the simulation.
Environment: canvas-confetti 1.9.4, Chrome on macOS (also reproducible anywhere frame pacing is uneven).
Source: catdad/canvas-confetti