Documentation menu
Guides
Inertia
Inertia takes a value that is already moving and lets it glide to a stop, the way a list keeps scrolling after you flick it. It can bounce off bounds and snap to a point you choose.
A first throw
Pass type: "inertia" and a velocity in units per second.
Inertia starts from the value's current position. The number in the values object is not a target: { x: 0 } only names the property.
import { animate } from "@animaly/dom";
animate(".box", { x: 0 }, { type: "inertia", velocity: 800 });import { animate } from "@animaly/dom";
animate(".box", { y: 0 }, { type: "inertia", velocity: -600 });Without a velocity the glide takes over the value's current velocity, so inertia can continue a spring or tween that is still moving.
Distance and duration
Power
The natural resting point is the starting position plus velocity × power. Lower power stops sooner.
import { animate } from "@animaly/dom";
animate(".short", { x: 0 }, { type: "inertia", velocity: 800, power: 0.3 });
animate(".far", { x: 0 }, { type: "inertia", velocity: 800, power: 1.2 });Time constant
timeConstant is in milliseconds and sets how quickly the glide loses speed. The value approaches its resting point exponentially.
import { animate } from "@animaly/dom";
animate(".quick", { x: 0 }, { type: "inertia", velocity: 800, timeConstant: 150 });
animate(".slow", { x: 0 }, { type: "inertia", velocity: 800, timeConstant: 700 });Bounds and bouncing
min and max keep the value inside a range. When the glide crosses one, animaly hands the value to a spring that pulls it back to the bound. bounceStiffness and bounceDamping configure that spring.
import { animate } from "@animaly/dom";
animate(".box", { x: 0 }, { type: "inertia", velocity: 2000, min: 0, max: 240 });import { animate } from "@animaly/dom";
animate(".box", { x: 0 }, {
type: "inertia",
velocity: 2000,
min: 0,
max: 240,
bounceStiffness: 200,
bounceDamping: 20,
});Snapping
modifyTarget receives the point where the glide would naturally stop and returns where it should stop instead. The glide then ends exactly there.
import { animate } from "@animaly/dom";
animate(".box", { x: 0 }, {
type: "inertia",
velocity: 900,
modifyTarget: (ideal) => Math.round(ideal / 100) * 100,
});import { animate } from "@animaly/dom";
const slideWidth = 300;
const slides = 4;
const lastOffset = -(slides - 1) * slideWidth;
const fling = (velocity: number): void => {
animate(".track", { x: 0 }, {
type: "inertia",
velocity,
min: lastOffset,
max: 0,
modifyTarget: (ideal) => Math.round(ideal / slideWidth) * slideWidth,
});
};
fling(-1400);import { animate } from "@animaly/dom";
const points = [0, 220, 380];
const closest = (ideal: number): number =>
points.reduce((best, point) => (Math.abs(point - ideal) < Math.abs(best - ideal) ? point : best));
animate(".sheet", { y: 0 }, { type: "inertia", velocity: 900, modifyTarget: closest });import { animate } from "@animaly/dom";
const spin = (): void => {
animate(".wheel", { rotate: 0 }, {
type: "inertia",
velocity: 1200 + Math.random() * 1200,
timeConstant: 900,
modifyTarget: (ideal) => Math.round(ideal / 45) * 45,
});
};
document.querySelector<HTMLButtonElement>(".spin")!.addEventListener("click", spin);
spin();Drag and throw
animaly has no gesture layer, so you track the pointer yourself and start inertia on release. A motion value keeps track of velocity for you: set() it while dragging and pass getVelocity() to inertia.
import { animate, motionValue } from "@animaly/dom";
const box = document.querySelector<HTMLElement>(".box")!;
const x = motionValue(0);
x.on("change", (latest) => {
box.style.transform = `translateX(${latest}px)`;
});
let startPointer = 0;
let startX = 0;
box.addEventListener("pointerdown", (event) => {
x.stop();
startPointer = event.clientX;
startX = x.get();
box.setPointerCapture(event.pointerId);
});
box.addEventListener("pointermove", (event) => {
if (!box.hasPointerCapture(event.pointerId)) return;
x.set(startX + event.clientX - startPointer);
});
box.addEventListener("pointerup", (event) => {
box.releasePointerCapture(event.pointerId);
animate(x, x.get(), { type: "inertia", velocity: x.getVelocity(), min: -200, max: 200 });
});
animate(x, 0, { type: "inertia", velocity: 600, min: -200, max: 200 });Without a motion value, measure the velocity from the last pointer events.
import { animate } from "@animaly/dom";
const box = document.querySelector<HTMLElement>(".box")!;
let offset = 0;
let lastX = 0;
let lastTime = 0;
let velocity = 0;
box.addEventListener("pointerdown", (event) => {
lastX = event.clientX;
lastTime = event.timeStamp;
velocity = 0;
box.setPointerCapture(event.pointerId);
});
box.addEventListener("pointermove", (event) => {
if (!box.hasPointerCapture(event.pointerId)) return;
const elapsed = (event.timeStamp - lastTime) / 1000;
const delta = event.clientX - lastX;
if (elapsed > 0) velocity = delta / elapsed;
offset += delta;
lastX = event.clientX;
lastTime = event.timeStamp;
animate(box, { x: offset }, { duration: 0 });
});
box.addEventListener("pointerup", () => {
animate(box, { x: offset }, { type: "inertia", velocity, min: -240, max: 240 });
});import { animate, motionValue } from "@animaly/dom";
const frame = document.querySelector<HTMLElement>(".frame")!;
const strip = document.querySelector<HTMLElement>(".strip")!;
const min = frame.clientWidth - strip.scrollWidth;
const x = motionValue(0);
x.on("change", (latest) => {
strip.style.transform = `translateX(${latest}px)`;
});
let startPointer = 0;
let startX = 0;
frame.addEventListener("pointerdown", (event) => {
x.stop();
startPointer = event.clientX;
startX = x.get();
frame.setPointerCapture(event.pointerId);
});
frame.addEventListener("pointermove", (event) => {
if (!frame.hasPointerCapture(event.pointerId)) return;
x.set(Math.min(0, Math.max(min, startX + event.clientX - startPointer)));
});
frame.addEventListener("pointerup", () => {
animate(x, x.get(), { type: "inertia", velocity: x.getVelocity(), min, max: 0, power: 0.9 });
});Combining with springs
Await the inertia controls and start a spring to bring the element home. Each axis is its own value, so throws on two axes are two calls.
import { animate } from "@animaly/dom";
const toss = async (velocity: number): Promise<void> => {
await animate(".box", { x: 0 }, { type: "inertia", velocity, min: -300, max: 300 });
await animate(".box", { x: 0 }, { type: "spring", stiffness: 180, damping: 16 });
};
toss(1600);import { animate } from "@animaly/dom";
const box = document.querySelector<HTMLElement>(".box")!;
animate(box, { x: 0 }, { type: "inertia", velocity: 700, min: -200, max: 200 });
animate(box, { y: 0 }, { type: "inertia", velocity: -400, min: -150, max: 150 });import { animate } from "@animaly/dom";
const box = document.querySelector<HTMLElement>(".box")!;
await animate(box, { x: 0 }, { type: "inertia", velocity: 900, min: -200, max: 200 });
box.dataset.settled = "true";Motion values, rest and delay
import { animate, motionValue } from "@animaly/dom";
const readout = document.querySelector<HTMLOutputElement>(".readout")!;
const offset = motionValue(0);
offset.on("change", (latest) => {
readout.textContent = latest.toFixed(0);
});
animate(offset, 0, { type: "inertia", velocity: 500 });import { animate } from "@animaly/dom";
animate(".box", { x: 0 }, { type: "inertia", velocity: 800, restDelta: 4 });import { animate } from "@animaly/dom";
animate(".box", { x: 0 }, { type: "inertia", velocity: 800, delay: 0.3 });Inertia cannot animate color or backgroundColor, cannot run inside a timeline, and cannot play in reverse: setting a negative speed on its controls throws.
Options
| Option | Type | Default | What it does |
|---|---|---|---|
| type | "inertia" | tween | Selects the inertia driver. |
| velocity | number | current | Starting velocity in units per second. Defaults to the value's current velocity. |
| power | number | 0.8 | Scales how far the value travels for a given velocity. |
| timeConstant | number | 325 | Milliseconds the glide takes to lose most of its speed. Higher glides longer. |
| min | number | none | Lower bound. Crossing it switches to a spring that bounces back. |
| max | number | none | Upper bound. Crossing it switches to a spring that bounces back. |
| bounceStiffness | number | 500 | Stiffness of the spring used at min and max. |
| bounceDamping | number | 10 | Damping of the spring used at min and max. |
| modifyTarget | (ideal: number) => number | none | Receives the natural resting point and returns the one to stop at, for snapping. |
| restDelta | number | 0.5 | Distance from the resting point below which the glide may stop. |
| restSpeed | number | none | Speed below which the glide may stop, in units per second. |
| delay | number | stagger() | 0 | Seconds to wait before starting. |