anim-engine 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -132
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,10 @@ import { createAnimation, getTicker } from "anim-engine";
|
|
|
20
20
|
getTicker().start();
|
|
21
21
|
|
|
22
22
|
const anim = createAnimation({
|
|
23
|
-
from: 0,
|
|
23
|
+
from: 0,
|
|
24
|
+
to: 100,
|
|
25
|
+
durationMs: 1000,
|
|
26
|
+
ease: "outCubic",
|
|
24
27
|
onUpdate: (value) => (sprite.x = value),
|
|
25
28
|
});
|
|
26
29
|
|
|
@@ -62,7 +65,7 @@ Anim Engine works strictly with numbers to stay renderer-agnostic. Each function
|
|
|
62
65
|
- [Dynamic values](#dynamic-values)
|
|
63
66
|
- [Color](#color)
|
|
64
67
|
- [Ticker](#ticker)
|
|
65
|
-
- [
|
|
68
|
+
- [Rendering Library integration](#rendering-library-integration)
|
|
66
69
|
- [Benchmarks](#benchmarks)
|
|
67
70
|
|
|
68
71
|
---
|
|
@@ -71,10 +74,10 @@ Anim Engine works strictly with numbers to stay renderer-agnostic. Each function
|
|
|
71
74
|
|
|
72
75
|
Two families with complementary shapes:
|
|
73
76
|
|
|
74
|
-
| Family
|
|
75
|
-
|
|
76
|
-
| **Timed**
|
|
77
|
-
| **Continuous** | `createSpring`, `createSmoothDamp`, `createLerp` | Chases a live target until stopped
|
|
77
|
+
| Family | Functions | Lifespan |
|
|
78
|
+
| -------------- | ------------------------------------------------ | ----------------------------------------- |
|
|
79
|
+
| **Timed** | `createAnimation`, `createTimeline` | Fixed motion path, runs once per `play()` |
|
|
80
|
+
| **Continuous** | `createSpring`, `createSmoothDamp`, `createLerp` | Chases a live target until stopped |
|
|
78
81
|
|
|
79
82
|
All return a control handle with `value`, `velocity`, `status`, and lifecycle methods.
|
|
80
83
|
|
|
@@ -86,54 +89,54 @@ All return a control handle with `value`, `velocity`, `status`, and lifecycle me
|
|
|
86
89
|
|
|
87
90
|
```ts
|
|
88
91
|
type Animation = {
|
|
89
|
-
play: () => Promise<void
|
|
90
|
-
pause: () => void
|
|
91
|
-
resume: () => void
|
|
92
|
-
stop: () => void
|
|
93
|
-
skipToEnd: () => void
|
|
94
|
-
setProgress: (value: number) => void
|
|
95
|
-
|
|
96
|
-
value: number
|
|
97
|
-
velocity: number
|
|
98
|
-
progress: number
|
|
99
|
-
status: "playing" | "paused" | "stopped"
|
|
100
|
-
durationMs: number
|
|
101
|
-
}
|
|
92
|
+
play: () => Promise<void>;
|
|
93
|
+
pause: () => void;
|
|
94
|
+
resume: () => void;
|
|
95
|
+
stop: () => void;
|
|
96
|
+
skipToEnd: () => void;
|
|
97
|
+
setProgress: (value: number) => void;
|
|
98
|
+
|
|
99
|
+
value: number;
|
|
100
|
+
velocity: number;
|
|
101
|
+
progress: number; // 0 → 1
|
|
102
|
+
status: "playing" | "paused" | "stopped";
|
|
103
|
+
durationMs: number;
|
|
104
|
+
};
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
**Options — single tween:**
|
|
105
108
|
|
|
106
109
|
```ts
|
|
107
110
|
type TweenOptions = {
|
|
108
|
-
from: DynamicValue
|
|
109
|
-
to: DynamicValue
|
|
110
|
-
durationMs: DynamicValue
|
|
111
|
-
ease?: EaseName | EaseFunction
|
|
112
|
-
onStarted?: () => void
|
|
113
|
-
onUpdate?: (value: number, velocity: number) => void
|
|
114
|
-
onProgress?: (progress: number) => void
|
|
115
|
-
onEnded?: () => void
|
|
116
|
-
ticker?: ExternalTicker
|
|
117
|
-
}
|
|
111
|
+
from: DynamicValue;
|
|
112
|
+
to: DynamicValue;
|
|
113
|
+
durationMs: DynamicValue;
|
|
114
|
+
ease?: EaseName | EaseFunction;
|
|
115
|
+
onStarted?: () => void;
|
|
116
|
+
onUpdate?: (value: number, velocity: number) => void;
|
|
117
|
+
onProgress?: (progress: number) => void;
|
|
118
|
+
onEnded?: () => void;
|
|
119
|
+
ticker?: ExternalTicker;
|
|
120
|
+
};
|
|
118
121
|
```
|
|
119
122
|
|
|
120
123
|
**Options — keyframes:**
|
|
121
124
|
|
|
122
125
|
```ts
|
|
123
126
|
type KeyframeOptions = {
|
|
124
|
-
keyframes: Keyframe[]
|
|
125
|
-
onStarted?: () => void
|
|
126
|
-
onUpdate?: (value: number, velocity: number) => void
|
|
127
|
-
onProgress?: (progress: number) => void
|
|
128
|
-
onEnded?: () => void
|
|
129
|
-
ticker?: ExternalTicker
|
|
130
|
-
}
|
|
127
|
+
keyframes: Keyframe[];
|
|
128
|
+
onStarted?: () => void;
|
|
129
|
+
onUpdate?: (value: number, velocity: number) => void;
|
|
130
|
+
onProgress?: (progress: number) => void;
|
|
131
|
+
onEnded?: () => void;
|
|
132
|
+
ticker?: ExternalTicker;
|
|
133
|
+
};
|
|
131
134
|
|
|
132
135
|
type Keyframe = {
|
|
133
|
-
value: DynamicValue
|
|
134
|
-
gap?: DynamicValue
|
|
135
|
-
ease?: EaseName | EaseFunction
|
|
136
|
-
}
|
|
136
|
+
value: DynamicValue;
|
|
137
|
+
gap?: DynamicValue; // ms from previous keyframe
|
|
138
|
+
ease?: EaseName | EaseFunction;
|
|
139
|
+
};
|
|
137
140
|
```
|
|
138
141
|
|
|
139
142
|
### Timed — `Timeline`
|
|
@@ -142,44 +145,44 @@ Layers multiple keyframe animations in parallel or sequence.
|
|
|
142
145
|
|
|
143
146
|
```ts
|
|
144
147
|
type Timeline = {
|
|
145
|
-
play: () => Promise<void
|
|
146
|
-
pause: () => void
|
|
147
|
-
resume: () => void
|
|
148
|
-
stop: () => void
|
|
149
|
-
skipToEnd: () => void
|
|
150
|
-
setProgress: (value: number) => void
|
|
151
|
-
|
|
152
|
-
values: number[]
|
|
153
|
-
velocities: number[]
|
|
154
|
-
progress: number
|
|
155
|
-
status: "playing" | "paused" | "stopped"
|
|
156
|
-
durationMs: number
|
|
157
|
-
}
|
|
148
|
+
play: () => Promise<void>;
|
|
149
|
+
pause: () => void;
|
|
150
|
+
resume: () => void;
|
|
151
|
+
stop: () => void;
|
|
152
|
+
skipToEnd: () => void;
|
|
153
|
+
setProgress: (value: number) => void;
|
|
154
|
+
|
|
155
|
+
values: number[]; // one per layer
|
|
156
|
+
velocities: number[]; // one per layer
|
|
157
|
+
progress: number;
|
|
158
|
+
status: "playing" | "paused" | "stopped";
|
|
159
|
+
durationMs: number;
|
|
160
|
+
};
|
|
158
161
|
```
|
|
159
162
|
|
|
160
163
|
```ts
|
|
161
164
|
type TimelineLayer =
|
|
162
|
-
| { animation: KeyframeOptions; at: DynamicValue }
|
|
163
|
-
| { animation: KeyframeOptions; gap: number }
|
|
165
|
+
| { animation: KeyframeOptions; at: DynamicValue } // absolute position
|
|
166
|
+
| { animation: KeyframeOptions; gap: number }; // relative to previous layer end
|
|
164
167
|
```
|
|
165
168
|
|
|
166
169
|
### Continuous — `Interpolation`
|
|
167
170
|
|
|
168
171
|
```ts
|
|
169
172
|
type Interpolation = {
|
|
170
|
-
resume: () => void
|
|
171
|
-
stop: () => void
|
|
172
|
-
setValue: (value: number) => void
|
|
173
|
-
|
|
174
|
-
value: number
|
|
175
|
-
velocity: number
|
|
176
|
-
status: "active" | "inactive"
|
|
177
|
-
}
|
|
173
|
+
resume: () => void;
|
|
174
|
+
stop: () => void;
|
|
175
|
+
setValue: (value: number) => void;
|
|
176
|
+
|
|
177
|
+
value: number;
|
|
178
|
+
velocity: number;
|
|
179
|
+
status: "active" | "inactive";
|
|
180
|
+
};
|
|
178
181
|
```
|
|
179
182
|
|
|
180
183
|
**Options:**
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
````ts
|
|
183
186
|
type SpringOptions = {
|
|
184
187
|
to: () => number
|
|
185
188
|
stiffness?: DynamicValue // default 180
|
|
@@ -209,28 +212,33 @@ type LerpOptions = {
|
|
|
209
212
|
onEnded?: () => void
|
|
210
213
|
ticker?: ExternalTicker
|
|
211
214
|
}
|
|
212
|
-
|
|
215
|
+
|
|
216
|
+
### Smooth clamp (pure function)
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
createSmoothClamp(threshold: number): (input: number) => number
|
|
220
|
+
````
|
|
213
221
|
|
|
214
222
|
### Common option patterns
|
|
215
223
|
|
|
216
|
-
|
|
|
217
|
-
|
|
218
|
-
| `createAnimation` (tween)
|
|
219
|
-
| `createAnimation` (keyframes) | — (first keyframe) | — (last keyframe) | single `(value, velocity)`
|
|
220
|
-
| `createTimeline`
|
|
221
|
-
| `createSpring`
|
|
222
|
-
| `createSmoothDamp`
|
|
223
|
-
| `createLerp`
|
|
224
|
+
| | `from` | `to` / target | `onUpdate` values | `durationMs` / `smoothTimeMs` |
|
|
225
|
+
| ----------------------------- | ------------------ | ----------------- | -------------------------------- | ----------------------------- |
|
|
226
|
+
| `createAnimation` (tween) | ✅ `DynamicValue` | ✅ `DynamicValue` | single `(value, velocity)` | ✅ `DynamicValue` |
|
|
227
|
+
| `createAnimation` (keyframes) | — (first keyframe) | — (last keyframe) | single `(value, velocity)` | — (sum of gaps) |
|
|
228
|
+
| `createTimeline` | — | — | multi `(values[], velocities[])` | — (computed) |
|
|
229
|
+
| `createSpring` | — (chases current) | ✅ `() => number` | single `(value, velocity)` | — (physics params) |
|
|
230
|
+
| `createSmoothDamp` | — (chases current) | ✅ `() => number` | single `(value, velocity)` | ✅ `DynamicValue` |
|
|
231
|
+
| `createLerp` | — (chases current) | ✅ `() => number` | single `(value, velocity)` | ✅ `DynamicValue` |
|
|
224
232
|
|
|
225
233
|
### Shared options
|
|
226
234
|
|
|
227
235
|
```ts
|
|
228
|
-
type DynamicValue = number | (() => number)
|
|
236
|
+
type DynamicValue = number | (() => number);
|
|
229
237
|
|
|
230
238
|
type ExternalTicker = {
|
|
231
|
-
add: (handler: TickHandler) => void
|
|
232
|
-
remove: (handler: TickHandler) => void
|
|
233
|
-
}
|
|
239
|
+
add: (handler: TickHandler) => void;
|
|
240
|
+
remove: (handler: TickHandler) => void;
|
|
241
|
+
};
|
|
234
242
|
```
|
|
235
243
|
|
|
236
244
|
---
|
|
@@ -241,7 +249,10 @@ type ExternalTicker = {
|
|
|
241
249
|
|
|
242
250
|
```ts
|
|
243
251
|
const anim = createAnimation({
|
|
244
|
-
from: 0,
|
|
252
|
+
from: 0,
|
|
253
|
+
to: 100,
|
|
254
|
+
durationMs: 2000,
|
|
255
|
+
ease: "outElastic",
|
|
245
256
|
onUpdate: (v) => (sprite.x = v),
|
|
246
257
|
});
|
|
247
258
|
await anim.play();
|
|
@@ -251,11 +262,7 @@ await anim.play();
|
|
|
251
262
|
|
|
252
263
|
```ts
|
|
253
264
|
createAnimation({
|
|
254
|
-
keyframes: [
|
|
255
|
-
{ value: 0 },
|
|
256
|
-
{ value: 50, gap: 300, ease: "outCubic" },
|
|
257
|
-
{ value: 100, gap: 400 },
|
|
258
|
-
],
|
|
265
|
+
keyframes: [{ value: 0 }, { value: 50, gap: 300, ease: "outCubic" }, { value: 100, gap: 400 }],
|
|
259
266
|
onUpdate: (v) => (sprite.x = v),
|
|
260
267
|
}).play();
|
|
261
268
|
```
|
|
@@ -266,7 +273,8 @@ createAnimation({
|
|
|
266
273
|
const anim = createAnimation({
|
|
267
274
|
from: () => (forward ? 1 : 1.3),
|
|
268
275
|
to: () => (forward ? 1.3 : 1),
|
|
269
|
-
durationMs: 600,
|
|
276
|
+
durationMs: 600,
|
|
277
|
+
ease: "inOutSine",
|
|
270
278
|
onUpdate: (v) => sprite.scale.set(v),
|
|
271
279
|
});
|
|
272
280
|
|
|
@@ -279,29 +287,32 @@ for (let i = 0; i < 6; i++) {
|
|
|
279
287
|
### Timeline
|
|
280
288
|
|
|
281
289
|
```ts
|
|
282
|
-
createTimeline(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
createTimeline(
|
|
291
|
+
[
|
|
292
|
+
{
|
|
293
|
+
at: 0,
|
|
294
|
+
animation: {
|
|
295
|
+
keyframes: [{ value: 0 }, { value: 1, gap: 500 }],
|
|
296
|
+
onEnded: () => console.log("layer 1 done"),
|
|
297
|
+
},
|
|
288
298
|
},
|
|
289
|
-
|
|
299
|
+
{
|
|
300
|
+
at: 0,
|
|
301
|
+
animation: {
|
|
302
|
+
keyframes: [{ value: -100 }, { value: 0, gap: 800, ease: "outBack" }],
|
|
303
|
+
onUpdate: (v) => (sprite.y = v),
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
],
|
|
290
307
|
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
308
|
+
// values[i] / velocities[i] correspond to layer i in definition order
|
|
309
|
+
onUpdate: (values, velocities) => {
|
|
310
|
+
sprite.x = values[0];
|
|
311
|
+
sprite.y = values[1];
|
|
295
312
|
},
|
|
313
|
+
onProgress: (p) => console.log(p),
|
|
296
314
|
},
|
|
297
|
-
|
|
298
|
-
// values[i] / velocities[i] correspond to layer i in definition order
|
|
299
|
-
onUpdate: (values, velocities) => {
|
|
300
|
-
sprite.x = values[0];
|
|
301
|
-
sprite.y = values[1];
|
|
302
|
-
},
|
|
303
|
-
onProgress: (p) => console.log(p),
|
|
304
|
-
});
|
|
315
|
+
);
|
|
305
316
|
```
|
|
306
317
|
|
|
307
318
|
### Spring
|
|
@@ -309,7 +320,8 @@ createTimeline([
|
|
|
309
320
|
```ts
|
|
310
321
|
const spring = createSpring({
|
|
311
322
|
to: () => mouseX,
|
|
312
|
-
stiffness: 180,
|
|
323
|
+
stiffness: 180,
|
|
324
|
+
damping: 12,
|
|
313
325
|
onUpdate: (v) => (sprite.x = v),
|
|
314
326
|
});
|
|
315
327
|
```
|
|
@@ -318,7 +330,8 @@ const spring = createSpring({
|
|
|
318
330
|
|
|
319
331
|
```ts
|
|
320
332
|
createSmoothDamp({
|
|
321
|
-
to: () => 100,
|
|
333
|
+
to: () => 100,
|
|
334
|
+
smoothTimeMs: 300,
|
|
322
335
|
onUpdate: (v) => (sprite.x = v),
|
|
323
336
|
});
|
|
324
337
|
```
|
|
@@ -327,7 +340,8 @@ createSmoothDamp({
|
|
|
327
340
|
|
|
328
341
|
```ts
|
|
329
342
|
createLerp({
|
|
330
|
-
to: () => 100,
|
|
343
|
+
to: () => 100,
|
|
344
|
+
smoothTimeMs: 300,
|
|
331
345
|
onUpdate: (v) => (sprite.x = v),
|
|
332
346
|
});
|
|
333
347
|
```
|
|
@@ -347,18 +361,39 @@ clamp(1000); // ≈ 44.96 (asymptotic to 45)
|
|
|
347
361
|
|
|
348
362
|
```ts
|
|
349
363
|
type EaseName =
|
|
350
|
-
| "linear"
|
|
351
|
-
| "
|
|
352
|
-
| "
|
|
353
|
-
| "
|
|
354
|
-
| "
|
|
355
|
-
| "
|
|
356
|
-
| "
|
|
357
|
-
| "
|
|
358
|
-
| "
|
|
359
|
-
| "
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
| "linear"
|
|
365
|
+
| "inQuad"
|
|
366
|
+
| "outQuad"
|
|
367
|
+
| "inOutQuad"
|
|
368
|
+
| "inCubic"
|
|
369
|
+
| "outCubic"
|
|
370
|
+
| "inOutCubic"
|
|
371
|
+
| "inQuart"
|
|
372
|
+
| "outQuart"
|
|
373
|
+
| "inOutQuart"
|
|
374
|
+
| "inQuint"
|
|
375
|
+
| "outQuint"
|
|
376
|
+
| "inOutQuint"
|
|
377
|
+
| "inSine"
|
|
378
|
+
| "outSine"
|
|
379
|
+
| "inOutSine"
|
|
380
|
+
| "inExpo"
|
|
381
|
+
| "outExpo"
|
|
382
|
+
| "inOutExpo"
|
|
383
|
+
| "inCirc"
|
|
384
|
+
| "outCirc"
|
|
385
|
+
| "inOutCirc"
|
|
386
|
+
| "inBack"
|
|
387
|
+
| "outBack"
|
|
388
|
+
| "inOutBack"
|
|
389
|
+
| "inElastic"
|
|
390
|
+
| "outElastic"
|
|
391
|
+
| "inOutElastic"
|
|
392
|
+
| "inBounce"
|
|
393
|
+
| "outBounce"
|
|
394
|
+
| "inOutBounce";
|
|
395
|
+
|
|
396
|
+
type EaseFunction = (t: number) => number;
|
|
362
397
|
```
|
|
363
398
|
|
|
364
399
|
Custom bezier:
|
|
@@ -396,11 +431,20 @@ createSpring({ to: () => mouseX, ... });
|
|
|
396
431
|
Oklab (perceptually uniform) color interpolation via `lerpRgba`.
|
|
397
432
|
|
|
398
433
|
```ts
|
|
399
|
-
|
|
434
|
+
type RgbaTuple = [number, number, number, number]
|
|
435
|
+
|
|
436
|
+
hexToRgba(hex: string): RgbaTuple
|
|
437
|
+
lerpRgba(from: RgbaTuple, to: RgbaTuple, t: number): RgbaTuple
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const from = hexToRgba("#ff6b6b"); // [1, 0.42, 0.42, 1]
|
|
400
442
|
const to = hexToRgba("#4ecdc4");
|
|
401
443
|
|
|
402
444
|
createAnimation({
|
|
403
|
-
from: 0,
|
|
445
|
+
from: 0,
|
|
446
|
+
to: 1,
|
|
447
|
+
durationMs: 2000,
|
|
404
448
|
onUpdate: (t) => {
|
|
405
449
|
const [r, g, b, a] = lerpRgba(from, to, t);
|
|
406
450
|
sprite.setColor(r, g, b, a);
|
|
@@ -433,7 +477,7 @@ Stop with `getTicker().stop()`.
|
|
|
433
477
|
|
|
434
478
|
---
|
|
435
479
|
|
|
436
|
-
##
|
|
480
|
+
## Rendering Library integration
|
|
437
481
|
|
|
438
482
|
### PixiJS
|
|
439
483
|
|
|
@@ -443,7 +487,11 @@ const ticker = getTicker();
|
|
|
443
487
|
|
|
444
488
|
app.ticker.add((delta) => ticker.update(delta.deltaMS));
|
|
445
489
|
|
|
446
|
-
createAnimation({
|
|
490
|
+
createAnimation({
|
|
491
|
+
from: 0,
|
|
492
|
+
to: 300,
|
|
493
|
+
durationMs: 2000,
|
|
494
|
+
ease: "outElastic",
|
|
447
495
|
onUpdate: (x) => (sprite.x = x),
|
|
448
496
|
}).play();
|
|
449
497
|
```
|
|
@@ -468,16 +516,16 @@ animate();
|
|
|
468
516
|
|
|
469
517
|
vs GSAP (vitest bench, Apple Silicon M-series, Node 24). Matched easing functions.
|
|
470
518
|
|
|
471
|
-
| Benchmark
|
|
472
|
-
|
|
473
|
-
| Single tween (cubic, 1000 frames)
|
|
474
|
-
| Single tween (bezier, 1000 frames)
|
|
475
|
-
| Keyframe (3 segments, 1000 frames)
|
|
476
|
-
| 50 concurrent tweens (500 frames)
|
|
477
|
-
| 200 concurrent tweens (500 frames)
|
|
478
|
-
| 1000 concurrent tweens (500 frames) | 37 ops/s
|
|
479
|
-
| 50-layer timeline (500 frames)
|
|
480
|
-
| 50 tweens re-play (500 frames)
|
|
519
|
+
| Benchmark | anim-engine | GSAP | Ratio |
|
|
520
|
+
| ----------------------------------- | ------------ | ------------ | ----- |
|
|
521
|
+
| Single tween (cubic, 1000 frames) | 40,974 ops/s | 10,656 ops/s | 3.8× |
|
|
522
|
+
| Single tween (bezier, 1000 frames) | 20,190 ops/s | 11,948 ops/s | 1.7× |
|
|
523
|
+
| Keyframe (3 segments, 1000 frames) | 26,661 ops/s | 3,578 ops/s | 7.5× |
|
|
524
|
+
| 50 concurrent tweens (500 frames) | 866 ops/s | 436 ops/s | 2.0× |
|
|
525
|
+
| 200 concurrent tweens (500 frames) | 187 ops/s | 112 ops/s | 1.7× |
|
|
526
|
+
| 1000 concurrent tweens (500 frames) | 37 ops/s | 13 ops/s | 2.8× |
|
|
527
|
+
| 50-layer timeline (500 frames) | 684 ops/s | 406 ops/s | 1.7× |
|
|
528
|
+
| 50 tweens re-play (500 frames) | 523 ops/s | 133 ops/s | 3.9× |
|
|
481
529
|
|
|
482
530
|
vs GSAP `onUpdate` (apples-to-apples): **2.8–4.5× faster**.
|
|
483
531
|
|