Skip to content

Animation speed is frame-rate dependent (no delta-time) — runs in slow motion when FPS dips #262

Description

@martin-circles

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

  1. 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).
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions