0
# Inertia Animation
1
2
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.
3
4
## Capabilities
5
6
### Inertia Function
7
8
Creates an inertia-based animation that uses decay motion with spring boundaries.
9
10
```typescript { .api }
11
/**
12
* Creates an inertia animation with decay and boundary springs
13
* @param options - Inertia configuration including boundaries and physics
14
* @returns PlaybackControls for stopping the animation
15
*/
16
function inertia(options: InertiaOptions): PlaybackControls;
17
18
interface InertiaOptions extends DecayOptions {
19
/** Spring stiffness when hitting boundaries */
20
bounceStiffness?: number;
21
/** Spring damping when hitting boundaries */
22
bounceDamping?: number;
23
/** Minimum boundary value */
24
min?: number;
25
/** Maximum boundary value */
26
max?: number;
27
/** Speed threshold for completion detection */
28
restSpeed?: number;
29
/** Distance threshold for completion detection */
30
restDelta?: number;
31
/** Custom driver for animation timing */
32
driver?: Driver;
33
/** Called with latest value on each frame */
34
onUpdate?: (v: number) => void;
35
/** Called when animation completes */
36
onComplete?: () => void;
37
/** Called when animation is stopped */
38
onStop?: () => void;
39
}
40
41
interface DecayOptions {
42
/** Starting value */
43
from?: number;
44
/** Target value (optional, not used in decay calculation) */
45
to?: number;
46
/** Initial velocity */
47
velocity?: number;
48
/** Decay power factor (default: 0.8) */
49
power?: number;
50
/** Time constant for decay rate */
51
timeConstant?: number;
52
/** Function to modify calculated target */
53
modifyTarget?: (target: number) => number;
54
/** Distance threshold for completion */
55
restDelta?: number;
56
}
57
58
interface PlaybackControls {
59
/** Stop the animation immediately */
60
stop: () => void;
61
}
62
```
63
64
**Usage Examples:**
65
66
```typescript
67
import { inertia } from "popmotion";
68
69
// Basic momentum scrolling
70
let scrollY = 0;
71
72
inertia({
73
from: scrollY,
74
velocity: -800, // Initial upward velocity
75
min: 0,
76
max: maxScrollHeight,
77
power: 0.8,
78
timeConstant: 750, // Default value
79
bounceStiffness: 500, // Default value
80
bounceDamping: 10, // Default value
81
onUpdate: (y) => {
82
scrollY = y;
83
scrollContainer.scrollTop = y;
84
},
85
onComplete: () => console.log("Scrolling stopped")
86
});
87
88
// Horizontal drag with boundaries
89
const controls = inertia({
90
from: currentX,
91
velocity: dragVelocity,
92
min: 0,
93
max: containerWidth - elementWidth,
94
power: 0.9,
95
bounceStiffness: 400,
96
bounceDamping: 30,
97
onUpdate: (x) => {
98
element.style.transform = `translateX(${x}px)`;
99
}
100
});
101
102
// Stop animation on user interaction
103
element.addEventListener('pointerdown', () => controls.stop());
104
```
105
106
### Behavior Characteristics
107
108
Inertia animation combines two physics models:
109
110
1. **Decay Phase**: When within boundaries, uses exponential decay
111
2. **Spring Phase**: When beyond boundaries, uses spring physics to bounce back
112
113
```typescript
114
// Example of combined behavior
115
inertia({
116
from: 50,
117
velocity: 1000, // Fast rightward motion
118
min: 0,
119
max: 100,
120
121
// Decay parameters (used within boundaries)
122
power: 0.8,
123
timeConstant: 325,
124
125
// Spring parameters (used at boundaries)
126
bounceStiffness: 300,
127
bounceDamping: 40,
128
129
onUpdate: (value) => {
130
// Animation will:
131
// 1. Decay from 50 towards calculated target (~200)
132
// 2. Hit max boundary (100) and spring back
133
// 3. Settle within 0-100 range
134
console.log(value);
135
}
136
});
137
```
138
139
## Implementation Patterns
140
141
### Momentum Scrolling
142
143
```typescript
144
import { inertia } from "popmotion";
145
146
class MomentumScroller {
147
private currentY = 0;
148
private maxScroll: number;
149
private animation?: PlaybackControls;
150
151
constructor(private element: HTMLElement) {
152
this.maxScroll = element.scrollHeight - element.clientHeight;
153
this.setupEventListeners();
154
}
155
156
private setupEventListeners() {
157
let startY = 0;
158
let lastY = 0;
159
let velocity = 0;
160
161
this.element.addEventListener('pointerdown', (e) => {
162
this.stopAnimation();
163
startY = e.clientY;
164
lastY = e.clientY;
165
velocity = 0;
166
});
167
168
this.element.addEventListener('pointermove', (e) => {
169
if (startY === 0) return;
170
171
const currentY = e.clientY;
172
const delta = currentY - lastY;
173
174
// Calculate velocity (with time consideration)
175
velocity = delta * 60; // Approximate pixels per second
176
177
this.currentY = Math.max(0, Math.min(this.maxScroll,
178
this.currentY - delta
179
));
180
181
this.updateScroll();
182
lastY = currentY;
183
});
184
185
this.element.addEventListener('pointerup', () => {
186
if (Math.abs(velocity) > 100) {
187
this.startInertia(velocity);
188
}
189
startY = 0;
190
});
191
}
192
193
private startInertia(velocity: number) {
194
this.animation = inertia({
195
from: this.currentY,
196
velocity: -velocity, // Invert for scroll direction
197
min: 0,
198
max: this.maxScroll,
199
power: 0.8,
200
timeConstant: 325,
201
bounceStiffness: 300,
202
bounceDamping: 40,
203
onUpdate: (y) => {
204
this.currentY = y;
205
this.updateScroll();
206
}
207
});
208
}
209
210
private updateScroll() {
211
this.element.scrollTop = this.currentY;
212
}
213
214
private stopAnimation() {
215
this.animation?.stop();
216
this.animation = undefined;
217
}
218
}
219
```
220
221
### Draggable Element with Boundaries
222
223
```typescript
224
import { inertia } from "popmotion";
225
226
class BoundedDraggable {
227
private currentX = 0;
228
private currentY = 0;
229
private animation?: PlaybackControls;
230
231
constructor(
232
private element: HTMLElement,
233
private bounds: { left: number; right: number; top: number; bottom: number }
234
) {
235
this.setupDragHandlers();
236
}
237
238
private setupDragHandlers() {
239
let startX = 0;
240
let startY = 0;
241
let velocityX = 0;
242
let velocityY = 0;
243
let lastTime = Date.now();
244
245
this.element.addEventListener('pointerdown', (e) => {
246
this.stopAnimation();
247
startX = e.clientX - this.currentX;
248
startY = e.clientY - this.currentY;
249
lastTime = Date.now();
250
});
251
252
this.element.addEventListener('pointermove', (e) => {
253
if (startX === 0 && startY === 0) return;
254
255
const now = Date.now();
256
const deltaTime = now - lastTime;
257
258
const newX = e.clientX - startX;
259
const newY = e.clientY - startY;
260
261
// Calculate velocity
262
velocityX = (newX - this.currentX) / deltaTime * 1000;
263
velocityY = (newY - this.currentY) / deltaTime * 1000;
264
265
this.currentX = newX;
266
this.currentY = newY;
267
this.updatePosition();
268
269
lastTime = now;
270
});
271
272
this.element.addEventListener('pointerup', () => {
273
if (Math.abs(velocityX) > 50 || Math.abs(velocityY) > 50) {
274
this.startInertia(velocityX, velocityY);
275
}
276
startX = startY = 0;
277
});
278
}
279
280
private startInertia(velocityX: number, velocityY: number) {
281
// Start separate inertia for X and Y axes
282
const xAnimation = inertia({
283
from: this.currentX,
284
velocity: velocityX,
285
min: this.bounds.left,
286
max: this.bounds.right,
287
power: 0.8,
288
bounceStiffness: 400,
289
bounceDamping: 40,
290
onUpdate: (x) => {
291
this.currentX = x;
292
this.updatePosition();
293
}
294
});
295
296
const yAnimation = inertia({
297
from: this.currentY,
298
velocity: velocityY,
299
min: this.bounds.top,
300
max: this.bounds.bottom,
301
power: 0.8,
302
bounceStiffness: 400,
303
bounceDamping: 40,
304
onUpdate: (y) => {
305
this.currentY = y;
306
this.updatePosition();
307
}
308
});
309
310
// Store reference to stop both animations
311
this.animation = {
312
stop: () => {
313
xAnimation.stop();
314
yAnimation.stop();
315
}
316
};
317
}
318
319
private updatePosition() {
320
this.element.style.transform =
321
`translate(${this.currentX}px, ${this.currentY}px)`;
322
}
323
324
private stopAnimation() {
325
this.animation?.stop();
326
this.animation = undefined;
327
}
328
}
329
```
330
331
## Parameter Guidelines
332
333
### Decay Parameters
334
- **Power**: 0.7-0.9 (higher = slower decay, more coasting) - Default: 0.8
335
- **TimeConstant**: 200-800ms (higher = longer momentum) - Default: 750
336
- **Velocity**: Measured in pixels/second from user interaction
337
338
### Bounce Parameters
339
- **BounceStiffness**: 200-600 (higher = snappier bounce back) - Default: 500
340
- **BounceDamping**: 10-60 (higher = less oscillation) - Default: 10
341
- **RestSpeed**: 0.01-1 (threshold for stopping)
342
- **RestDelta**: 0.01-1 (distance threshold for stopping)
343
344
### Boundary Configuration
345
- **Min/Max**: Set appropriate boundaries based on content size
346
- Use `modifyTarget` to implement custom boundary logic
347
- Consider different bounce characteristics for different boundaries