Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 9.2Intermediate ~31 min · shot 2 of 7

A Spring by Hand in JavaScript

In one line

A spring is a loop: work out the pull, change the speed, move, repeat. A fixed small time step keeps it stable, and rest thresholds say when to stop.

0/7 · 0%

Think of it like this

A game of tag in slow steps. Every few steps you look at where the other player is, turn toward them and move a little. If you may only look every ten seconds, you run so far in one go that you end up far on the wrong side.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

time step
How much simulated time one update covers. Smaller is more exact and costs more updates.
semi-implicit Euler
Update the speed first, then move with the new speed. More stable than moving first.
frame gap
A frame much longer than usual, from a busy page or a background tab.
rest threshold
The distance and speed below which a spring counts as arrived: restDelta and restSpeed.

Step by step

01Run the hand-written spring

Press Go to 160. The label shows x and speed each frame. Press Back to 0 halfway: retarget aims the same spring elsewhere.

02Read the frame-gap console

Six frames, one of 300 ms, run two ways. The naive loop ends at -10.63 for a 0 to 1 move; spring.js at 0.88.

03Compare the rest rules

Frames to rest at 60 Hz: 45 for the default, 71 for the strict rule, 22 for the loose one (final snap 5.88 px).

04Find the safety stop

The last rest rule says rest after 1200 frames: 20 s of 60 Hz frames, the limit in spring.js.

Try it yourself

  1. 1

    Change the step

    In the first example's spring.js change 1 / 240 to 1 / 30: stiffness 170, damping 26 still settles. Also set stiffness: 2000, damping: 90 in goTo and the position explodes: every update is about twice as large as the one before and it reaches about 10^19 px within a second (60 Hz frames on a fixed clock). With 1/240 the same spring settles at 160.

  2. 2

    Remove the clamp

    In "A 300 ms frame" change MAX_FRAME = 0.064 to 1. The spring.js line becomes 0.09, 0.28, 0.48, 1.00: no explosion, but the whole gap was played at once and the spring is already at the end.

Code & diagrams

The spring you will ship

`spring.js` of the build inlined (a preview page cannot import your project files). Go, Back, and press Back again while it is moving.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

spring() returns: retarget, stop, velocity
A 300 ms frame

Stiffness 1000, damping 60. Both runs get the same six frame lengths. The naive loop does one Euler step per frame whatever the frame length.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

frame lengths (ms): 16, 16, 16, 300, 16, 16
one step per frame: 0.26, 0.46, 0.60, -10.63, -7.68, -5.34
spring.js:          0.09, 0.28, 0.48, 0.88, 0.92, 0.95
Four rest rules

Same spring and the same 100 px move. Only restDelta and restSpeed change. The last rule never says stop, so the 20 s safety limit does.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

restDelta 0.1,  restSpeed 1   : rested after 45 frames, final snap 0.11 px
restDelta 0.001, restSpeed 0.01: rested after 71 frames, final snap 0.00 px
restDelta 5,    restSpeed 100  : rested after 22 frames, final snap 5.88 px
restDelta 0,    restSpeed 0    : rested after 1200 frames, final snap 0.00 px

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Thresholds of zero

Read the last line of "Four rest rules" (restDelta 0, restSpeed 0).

terminal
$ Read the console
── what you'll see ──
restDelta 0, restSpeed 0 : rested after 1200 frames, final snap 0.00 px

Myth vs fact

Myth

A smaller time step is always better.

Fact

It is more exact, but 1/240 s is already far below what the eye sees, and every halving doubles the work per frame.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    When a tab is hidden requestAnimationFrame stops, and the first frame after it returns carries a huge gap. The 64 ms clamp turns that into a short continuation (15 fixed steps of work, not thousands).

Remember this

  1. 1

    The problem this solves: the formula of Topic 9.1 needs a start at rest and a fixed target. To retarget or drag a spring you simulate it: keep a position and a speed and update them in small steps. That is spring.js of the build.

  2. 2

    One step. Compute acceleration = (-k × (x - target) - c × v) ÷ m. Add acceleration × step to the speed, then add speed × step to the position. Updating the speed first is semi-implicit Euler: it stays stable far longer than moving first with the old speed.

  3. 3

    A fixed step. A frame is 8 to 33 ms and varies by device. spring.js uses a step of 1/240 s and runs as many whole steps as the frame holds. The leftover time waits for the next frame, so the motion is the same on a 30 Hz and a 120 Hz screen.

  4. 4

    Long frames. A busy page or a returning background tab can give a 300 ms frame. A loop with one Euler step per frame uses a 300 ms step and explodes (-10.63 for a 0 to 1 move in the second example). The fixed step cannot explode, but it would spend the whole gap at once and land at 1.00 in one jump. So spring.js simulates at most 64 ms per frame: after a pause the spring carries on from where it was (0.88).

  5. 5

    When to stop. A spring approaches the target for ever. Rest when the speed is below restSpeed and the distance below restDelta, then snap to the target and call onRest. Too loose and the last frame jumps (5 px thresholds jumped 5.88 px). Too strict and it never stops: thresholds of 0 run to the 20 s safety limit.

  6. 6

    Bad numbers. Stiffness 0, a negative mass or a negative damping are not springs: spring.js throws a RangeError at once instead of drawing NaN.

Explain it without notes

01

Why does spring.js use a fixed step instead of the frame length?

02

What happens if the rest thresholds are too small, and too large?

Practice

01

Which rest thresholds suit an opacity spring from 0 to 1?

02

Why must retarget reset the 20 s safety timer?

Done when you can

  • You can write one semi-implicit Euler step

  • You can say why the step is fixed and long frames are clamped

  • You can choose rest thresholds for pixels and for a 0 to 1 value