animaly
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.

Flick a box to the right
import { animate } from "@animaly/dom";

animate(".box", { x: 0 }, { type: "inertia", velocity: 800 });
Throw upward
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.

Power: how far a throw travels
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.

Time constant: how long the glide lasts
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.

Stop at a wall and bounce back
import { animate } from "@animaly/dom";

animate(".box", { x: 0 }, { type: "inertia", velocity: 2000, min: 0, max: 240 });
A softer bounce off the bounds
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.

Snap to a 100px grid
import { animate } from "@animaly/dom";

animate(".box", { x: 0 }, {
  type: "inertia",
  velocity: 900,
  modifyTarget: (ideal) => Math.round(ideal / 100) * 100,
});
Snap a carousel to the nearest slide
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);
Snap to the closest of a list of points
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 });
A wheel spinner that slows down
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.

Drag and throw with pointer events
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.

Measure pointer velocity yourself
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 });
});
Momentum scrolling of a strip
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.

Throw, glide, then spring back home
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);
Throw on both axes
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 });
Run code when the glide ends
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

Inertia on a motion value
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 });
Stop sooner with a larger rest distance
import { animate } from "@animaly/dom";

animate(".box", { x: 0 }, { type: "inertia", velocity: 800, restDelta: 4 });
Delay a throw
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

Inertia options
OptionTypeDefaultWhat it does
type"inertia"tweenSelects the inertia driver.
velocitynumbercurrentStarting velocity in units per second. Defaults to the value's current velocity.
powernumber0.8Scales how far the value travels for a given velocity.
timeConstantnumber325Milliseconds the glide takes to lose most of its speed. Higher glides longer.
minnumbernoneLower bound. Crossing it switches to a spring that bounces back.
maxnumbernoneUpper bound. Crossing it switches to a spring that bounces back.
bounceStiffnessnumber500Stiffness of the spring used at min and max.
bounceDampingnumber10Damping of the spring used at min and max.
modifyTarget(ideal: number) => numbernoneReceives the natural resting point and returns the one to stop at, for snapping.
restDeltanumber0.5Distance from the resting point below which the glide may stop.
restSpeednumbernoneSpeed below which the glide may stop, in units per second.
delaynumber | stagger()0Seconds to wait before starting.