Skip to content

Fix spring animation velocity loss on Chrome (vsync-aligned mousemove)#3577

Closed
mattgperry wants to merge 1 commit into
mainfrom
worktree-fix-issue-3407
Closed

Fix spring animation velocity loss on Chrome (vsync-aligned mousemove)#3577
mattgperry wants to merge 1 commit into
mainfrom
worktree-fix-issue-3407

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

Bug

Spring animations using followValue/springValue/useSpring behave incorrectly in Chrome when following a pointer. The ball feels "stuck" and resists orbiting the cursor, while Firefox and Safari work correctly.

Fixes #3407

Root Cause

Chrome fires mousemove events at vsync rate (aligned with requestAnimationFrame), which means a spring animation is consistently interrupted after exactly 1 frame whenever the target changes.

In attachFollow's startAnimation(), the old code called stopAnimation() before reading value.getVelocity(). After only 1 frame, getVelocity() computes a finite difference between the previous animation's last rendered position and the new animation's first keyframe — values from different animations, possibly with different targets. This cross-animation delta is often near zero or directionally wrong, causing the spring to restart with near-zero velocity every time.

Fix

Read velocity from the active animation's spring generator before stopping it, using a 5ms finite difference against the generator's formula directly (calcGeneratorVelocity + sampleAt). This gives accurate instantaneous spring velocity even when the animation has only run for a single frame.

Three new members were added to JSAnimation:

  • canSampleVelocity — guards that the animation is running and the generator is accessible
  • sampleAt(t) — samples the generator's position at time t (milliseconds)
  • elapsed — exposes currentTime for use as the sampling reference point

The startAnimation() function in follow-value.ts now uses these to read velocity before calling stopAnimation().

Tests

Added 6 unit tests in JSAnimation.test.ts under "JSAnimation velocity sampling (for spring interruption fix)" covering the new API members and the velocity calculation.

Chrome fires mousemove events at vsync rate, causing spring animations
in attachFollow to be interrupted after exactly 1 frame. The previous
code called stopAnimation() before reading velocity, so getVelocity()
computed a cross-animation finite difference (old animation vs new
keyframe) - often near-zero or wrong direction.

Fix: read velocity from the spring generator's sampleAt() BEFORE
stopping, using calcGeneratorVelocity for a 5ms precision window.
Add canSampleVelocity, sampleAt(), and elapsed to JSAnimation to
support this.

Fixes #3407

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 2, 2026

Greptile Summary

Fixed critical velocity loss bug in spring animations on Chrome by reading velocity before stopping animations. Chrome fires mousemove events at vsync rate, causing animations to be interrupted after exactly one frame. The old code read velocity after stopping, which computed incorrect cross-animation deltas. The fix samples velocity directly from the spring generator (using a 5ms finite difference) before interruption, ensuring smooth spring behavior across all browsers.

Key Changes:

  • Added canSampleVelocity, sampleAt, and elapsed to JSAnimation for accurate generator sampling
  • Modified startAnimation() in follow-value.ts to preserve velocity before stopping
  • Added 6 unit tests covering the new velocity sampling API

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The fix directly addresses the root cause with a clean implementation, adds proper API guards (canSampleVelocity), includes comprehensive tests, and has no breaking changes or security concerns
  • No files require special attention

Important Files Changed

Filename Overview
packages/motion-dom/src/animation/JSAnimation.ts Added three well-designed API members (canSampleVelocity, sampleAt, elapsed) to enable accurate velocity sampling from running spring animations
packages/motion-dom/src/value/follow-value.ts Fixed velocity loss by reading spring velocity before stopping animation, using generator sampling for accurate single-frame velocity calculation
packages/motion-dom/src/animation/tests/JSAnimation.test.ts Added 6 unit tests covering the new velocity sampling API, verifying correct behavior of canSampleVelocity, sampleAt, and elapsed

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[startAnimation called] --> B{currentValue == targetValue?}
    B -->|Yes| C[stopAnimation & return]
    B -->|No| D[Get velocity]
    
    D --> E{activeAnimation?.canSampleVelocity?}
    E -->|Yes| F[Sample velocity from generator<br/>using calcGeneratorVelocity]
    E -->|No| G[Use value.getVelocity<br/>cross-frame finite difference]
    
    F --> H[stopAnimation]
    G --> H
    
    H --> I[Create new JSAnimation<br/>with preserved velocity]
    
    style F fill:#90EE90
    style G fill:#FFB6C1
    style I fill:#87CEEB
Loading

Last reviewed commit: d1b51bd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Spring animation is broken on Chrome (works on Firefox/Safari) (Mac)

1 participant