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 while fetti.tick < fetti.totalTicks.
- The rAF loop uses the timestamp only as a maximum frame cap:
var TIME = Math.floor(1000 / 60) and if (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
tick by delta / TIME instead of 1,
- 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).
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)
fetti.velocity *= fetti.decay(src/confetti.js,updateFetti).progress = (fetti.tick++) / fetti.totalTicks, alive whilefetti.tick < fetti.totalTicks.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
app, or simply generate input/render churn on the page during the burst).
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)):tickbydelta / TIMEinstead of1,velocity *= Math.pow(decay, delta / TIME),delta / TIME.To preserve backwards compatibility, this could be gated behind an opt-in option
(e.g.
frameRateIndependent: trueon the global/fire options) rather than changingdefault 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).