The animator's toolbox providing comprehensive animation capabilities including keyframe, spring, and decay animations for numbers, colors, and complex strings
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Specialized inertia animation that combines decay with spring physics for scroll-like behaviors with boundaries. Perfect for implementing momentum scrolling, drag interactions, and physics-based UI elements.
Creates an inertia-based animation that uses decay motion with spring boundaries.
/**
* Creates an inertia animation with decay and boundary springs
* @param options - Inertia configuration including boundaries and physics
* @returns PlaybackControls for stopping the animation
*/
function inertia(options: InertiaOptions): PlaybackControls;
interface InertiaOptions extends DecayOptions {
/** Spring stiffness when hitting boundaries */
bounceStiffness?: number;
/** Spring damping when hitting boundaries */
bounceDamping?: number;
/** Minimum boundary value */
min?: number;
/** Maximum boundary value */
max?: number;
/** Speed threshold for completion detection */
restSpeed?: number;
/** Distance threshold for completion detection */
restDelta?: number;
/** Custom driver for animation timing */
driver?: Driver;
/** Called with latest value on each frame */
onUpdate?: (v: number) => void;
/** Called when animation completes */
onComplete?: () => void;
/** Called when animation is stopped */
onStop?: () => void;
}
interface DecayOptions {
/** Starting value */
from?: number;
/** Target value (optional, not used in decay calculation) */
to?: number;
/** Initial velocity */
velocity?: number;
/** Decay power factor (default: 0.8) */
power?: number;
/** Time constant for decay rate */
timeConstant?: number;
/** Function to modify calculated target */
modifyTarget?: (target: number) => number;
/** Distance threshold for completion */
restDelta?: number;
}
interface PlaybackControls {
/** Stop the animation immediately */
stop: () => void;
}Usage Examples:
import { inertia } from "popmotion";
// Basic momentum scrolling
let scrollY = 0;
inertia({
from: scrollY,
velocity: -800, // Initial upward velocity
min: 0,
max: maxScrollHeight,
power: 0.8,
timeConstant: 750, // Default value
bounceStiffness: 500, // Default value
bounceDamping: 10, // Default value
onUpdate: (y) => {
scrollY = y;
scrollContainer.scrollTop = y;
},
onComplete: () => console.log("Scrolling stopped")
});
// Horizontal drag with boundaries
const controls = inertia({
from: currentX,
velocity: dragVelocity,
min: 0,
max: containerWidth - elementWidth,
power: 0.9,
bounceStiffness: 400,
bounceDamping: 30,
onUpdate: (x) => {
element.style.transform = `translateX(${x}px)`;
}
});
// Stop animation on user interaction
element.addEventListener('pointerdown', () => controls.stop());Inertia animation combines two physics models:
// Example of combined behavior
inertia({
from: 50,
velocity: 1000, // Fast rightward motion
min: 0,
max: 100,
// Decay parameters (used within boundaries)
power: 0.8,
timeConstant: 325,
// Spring parameters (used at boundaries)
bounceStiffness: 300,
bounceDamping: 40,
onUpdate: (value) => {
// Animation will:
// 1. Decay from 50 towards calculated target (~200)
// 2. Hit max boundary (100) and spring back
// 3. Settle within 0-100 range
console.log(value);
}
});import { inertia } from "popmotion";
class MomentumScroller {
private currentY = 0;
private maxScroll: number;
private animation?: PlaybackControls;
constructor(private element: HTMLElement) {
this.maxScroll = element.scrollHeight - element.clientHeight;
this.setupEventListeners();
}
private setupEventListeners() {
let startY = 0;
let lastY = 0;
let velocity = 0;
this.element.addEventListener('pointerdown', (e) => {
this.stopAnimation();
startY = e.clientY;
lastY = e.clientY;
velocity = 0;
});
this.element.addEventListener('pointermove', (e) => {
if (startY === 0) return;
const currentY = e.clientY;
const delta = currentY - lastY;
// Calculate velocity (with time consideration)
velocity = delta * 60; // Approximate pixels per second
this.currentY = Math.max(0, Math.min(this.maxScroll,
this.currentY - delta
));
this.updateScroll();
lastY = currentY;
});
this.element.addEventListener('pointerup', () => {
if (Math.abs(velocity) > 100) {
this.startInertia(velocity);
}
startY = 0;
});
}
private startInertia(velocity: number) {
this.animation = inertia({
from: this.currentY,
velocity: -velocity, // Invert for scroll direction
min: 0,
max: this.maxScroll,
power: 0.8,
timeConstant: 325,
bounceStiffness: 300,
bounceDamping: 40,
onUpdate: (y) => {
this.currentY = y;
this.updateScroll();
}
});
}
private updateScroll() {
this.element.scrollTop = this.currentY;
}
private stopAnimation() {
this.animation?.stop();
this.animation = undefined;
}
}import { inertia } from "popmotion";
class BoundedDraggable {
private currentX = 0;
private currentY = 0;
private animation?: PlaybackControls;
constructor(
private element: HTMLElement,
private bounds: { left: number; right: number; top: number; bottom: number }
) {
this.setupDragHandlers();
}
private setupDragHandlers() {
let startX = 0;
let startY = 0;
let velocityX = 0;
let velocityY = 0;
let lastTime = Date.now();
this.element.addEventListener('pointerdown', (e) => {
this.stopAnimation();
startX = e.clientX - this.currentX;
startY = e.clientY - this.currentY;
lastTime = Date.now();
});
this.element.addEventListener('pointermove', (e) => {
if (startX === 0 && startY === 0) return;
const now = Date.now();
const deltaTime = now - lastTime;
const newX = e.clientX - startX;
const newY = e.clientY - startY;
// Calculate velocity
velocityX = (newX - this.currentX) / deltaTime * 1000;
velocityY = (newY - this.currentY) / deltaTime * 1000;
this.currentX = newX;
this.currentY = newY;
this.updatePosition();
lastTime = now;
});
this.element.addEventListener('pointerup', () => {
if (Math.abs(velocityX) > 50 || Math.abs(velocityY) > 50) {
this.startInertia(velocityX, velocityY);
}
startX = startY = 0;
});
}
private startInertia(velocityX: number, velocityY: number) {
// Start separate inertia for X and Y axes
const xAnimation = inertia({
from: this.currentX,
velocity: velocityX,
min: this.bounds.left,
max: this.bounds.right,
power: 0.8,
bounceStiffness: 400,
bounceDamping: 40,
onUpdate: (x) => {
this.currentX = x;
this.updatePosition();
}
});
const yAnimation = inertia({
from: this.currentY,
velocity: velocityY,
min: this.bounds.top,
max: this.bounds.bottom,
power: 0.8,
bounceStiffness: 400,
bounceDamping: 40,
onUpdate: (y) => {
this.currentY = y;
this.updatePosition();
}
});
// Store reference to stop both animations
this.animation = {
stop: () => {
xAnimation.stop();
yAnimation.stop();
}
};
}
private updatePosition() {
this.element.style.transform =
`translate(${this.currentX}px, ${this.currentY}px)`;
}
private stopAnimation() {
this.animation?.stop();
this.animation = undefined;
}
}modifyTarget to implement custom boundary logicInstall with Tessl CLI
npx tessl i tessl/npm-popmotion