In this article I explain how to create animations with Web Animation API using springs (or rather, physics behind them).
Spring physics sounds intimidating, and that's what kept me from using it in my own animation projects. But as this awesome article by Maxime Heckel shows, you probably already know some of it and the rest isn't very complicated. If you haven't read the article yet, you should read it now, because everything below assumes you know the principles. If you are not familiar with Web Animation API, start here.
Quick recap
For convenience, here's a quick recap:
 Springs have stiffness, mass and a damping ratio (also length but it is irrelevant here).
 One force that acts on a spring when you displace it is:
F = k * x // where k is stiffness and x is displacement
 Another force is damping force. It slows the spring down so it eventually stops:
F = d * v // where d is damping ratio and v is velocity
 If we know acceleration and a time interval, we can calculate speed from previous speed:
v2 = v1 + a*t
 If we know speed and a time interval, we can calculate position from previous position and speed:
p2 = p1 + v*t
Implementation
Here's the Codesandbox that shows the final result. You can play with it and change some default parameters.
Listeners
First of all, we need some listeners.

mousedown
andmousemove
to start tracking the displacement of the square 
mouseup
to calculate and play an animation (more on that below)
This is pretty straightforward, so I am going to omit the details.
Drag transform
Strictly speaking, we are not dragging the element using native browser API. But we want to make it look like we move it! To do that, we set a CSS transform string directly to the element on each mousemove
event.
function transformDrag(dx, dy) {
square.style.transform = `translate(${dx}px, ${dy}px)`;
}
function handleMouseMove(e) {
const dx = e.clientX  mouseX;
const dy = e.clientY  mouseY;
dragDx = dragDx + dx;
dragDy = dragDy + dy;
transformDrag(dragDx, dragDy);
}
Generating keyframes
Now, the most important part of the animation. When we release (mouseup
) the square, we need to animate how it goes back to its original position. But to make it look natural, we use a spring.
Any animation that uses WAAPI requires a set of keyframes which are just like the keyframes you need for a CSS animation. Only in this case, each keyframe is a Javascript object. Our task here is to generate an array of such objects and launch the animation.
We need a total of 5 parameters to be able to generate keyframes:
 Displacement on xaxis
 Displacement on yaxis
 Stiffness
 Mass
 Damping ratio
In the codesandbox above we use these defaults for physical parameters 35: 600
, 7
and 1
. For simplicity, we assume that the spring has length 1
.
function createSpringAnimation(
dx,
dy,
stiffness = 600,
damping = 7,
mass = 1
) {
const spring_length = 1;
const k = stiffness;
const d = damping;
// ...
dx
and dy
are dynamic: we will pass them to the function on mouseup
event.
A time interval in the context of the browser is one frame, or ~0.016s.
const frame_rate = 1 / 60;
To generate one keyframe we simply apply the formulas from the article above:
let x = dx;
let y = dy;
let velocity_x = 0;
let velocity_y = 0;
let Fspring_x = k * (x  spring_length);
let Fspring_y = k * (y  spring_length);
let Fdamping_x = d * velocity_x;
let Fdamping_y = d * velocity_y;
let accel_x = (Fspring_x + Fdamping_x) / mass;
let accel_y = (Fspring_y + Fdamping_y) / mass;
velocity_x += accel_x * frame_rate;
velocity_y += accel_y * frame_rate;
x += velocity_x * frame_rate;
y += velocity_y * frame_rate;
const keyframe = { transform: `translate(${x}px, ${y}px)` }
Ideally we need a keyframe for each time interval to have a smooth 60fps animation. Intuitively, we need to loop until the end of animation duration (duration divided by one frame length times). There's a problem, however  we don't know when exactly the spring will stop beforehand! This is the biggest difficulty when trying to animate springs with browser APIs that want the exact duration time from you. Fortunately, there is a workaround: loop a potentially large number of times, but break when we have enough keyframes. Let's say we want it to stop when the largest movement does not exceed 3 pixels (in both directions) for the last 60 frames  simply because it becomes not easy to notice movement. We lose precision but reach the goal.
So, this is what this heuristic looks like in code:
const DISPL_THRESHOLD = 3;
let frames = 0;
let frames_below_threshold = 0;
let largest_displ;
let positions = [];
for (let step = 0; step <= 1000; step += 1) {
// Generate a keyframe
// ...
// Put the keyframe in the array
positions.push(keyframe);
largest_displ =
largest_displ < 0
? Math.max(largest_displ  Infinity, x)
: Math.min(largest_displ  Infinity, x);
if (Math.abs(largest_displ) < DISPL_THRESHOLD) {
frames_below_threshold += 1;
} else {
frames_below_threshold = 0; // Reset the frame counter
}
if (frames_below_threshold >= 60) {
frames = step;
break;
}
}
After we break, we save the number of times we looped as the number of frames. We use this number to calculate the actual duration. This is the mouseup
handler:
let animation;
function handleMouseUp(e) {
const { positions, frames } = createSpringAnimation(dragDx, dragDy);
square.style.transform = ""; // Cancel all transforms right before animation
const keyframes = new KeyframeEffect(square, positions, {
duration: (frames / 60) * 1000,
fill: "both",
easing: "linear",
iterations: 1
});
animation = new Animation(keyframes);
animation.play();
}
Note that the easing
option of the animation is set to linear
because we already solve it manually inside the createSpringAnimation
function.
This is all you need to generate a nice smooth 60fps spring animation!
Posted on by:
Kirill Vasiltsov
I am interested in everything that is related to UI. I'm also learning about lowlevel stuff and experimenting with Rust.
Discussion