anim-engine 0.5.0 → 0.5.2
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 +31 -13
- package/SKILL.md +515 -0
- package/dist/timeline/create-timeline.js +11 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ const anim = createAnimation({
|
|
|
27
27
|
await anim.play();
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
> AI agents: this package includes a [`SKILL.md`](SKILL.md) with in-depth usage guidance for agent-assisted development.
|
|
31
|
+
|
|
30
32
|
## Design
|
|
31
33
|
|
|
32
34
|
Anim Engine is built around the same concepts used in animation applications, applied to pure numbers.
|
|
@@ -278,11 +280,27 @@ for (let i = 0; i < 6; i++) {
|
|
|
278
280
|
|
|
279
281
|
```ts
|
|
280
282
|
createTimeline([
|
|
281
|
-
{
|
|
282
|
-
|
|
283
|
+
{
|
|
284
|
+
at: 0,
|
|
285
|
+
animation: {
|
|
286
|
+
keyframes: [{ value: 0 }, { value: 1, gap: 500 }],
|
|
287
|
+
onEnded: () => console.log("layer 1 done"),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
at: 0,
|
|
292
|
+
animation: {
|
|
293
|
+
keyframes: [{ value: -100 }, { value: 0, gap: 800, ease: "outBack" }],
|
|
294
|
+
onUpdate: (v) => (sprite.y = v),
|
|
295
|
+
},
|
|
296
|
+
},
|
|
283
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
|
+
},
|
|
284
303
|
onProgress: (p) => console.log(p),
|
|
285
|
-
onUpdate: (values, velocities) => { /* one entry per layer */ },
|
|
286
304
|
});
|
|
287
305
|
```
|
|
288
306
|
|
|
@@ -452,16 +470,16 @@ vs GSAP (vitest bench, Apple Silicon M-series, Node 24). Matched easing function
|
|
|
452
470
|
|
|
453
471
|
| Benchmark | anim-engine | GSAP | Ratio |
|
|
454
472
|
|---|---|---|---|
|
|
455
|
-
| Single tween (cubic, 1000 frames) | 40,
|
|
456
|
-
| Single tween (bezier, 1000 frames) |
|
|
457
|
-
| Keyframe (3 segments, 1000 frames) | 26,
|
|
458
|
-
| 50 concurrent tweens (500 frames) |
|
|
459
|
-
| 200 concurrent tweens (500 frames) |
|
|
460
|
-
| 1000 concurrent tweens (500 frames) |
|
|
461
|
-
| 50-layer timeline (500 frames) |
|
|
462
|
-
| 50 tweens re-play (500 frames) |
|
|
463
|
-
|
|
464
|
-
vs GSAP `onUpdate` (apples-to-apples): **
|
|
473
|
+
| Single tween (cubic, 1000 frames) | 40,974 ops/s | 10,656 ops/s | 3.8× |
|
|
474
|
+
| Single tween (bezier, 1000 frames) | 20,190 ops/s | 11,948 ops/s | 1.7× |
|
|
475
|
+
| Keyframe (3 segments, 1000 frames) | 26,661 ops/s | 3,578 ops/s | 7.5× |
|
|
476
|
+
| 50 concurrent tweens (500 frames) | 866 ops/s | 436 ops/s | 2.0× |
|
|
477
|
+
| 200 concurrent tweens (500 frames) | 187 ops/s | 112 ops/s | 1.7× |
|
|
478
|
+
| 1000 concurrent tweens (500 frames) | 37 ops/s | 13 ops/s | 2.8× |
|
|
479
|
+
| 50-layer timeline (500 frames) | 684 ops/s | 406 ops/s | 1.7× |
|
|
480
|
+
| 50 tweens re-play (500 frames) | 523 ops/s | 133 ops/s | 3.9× |
|
|
481
|
+
|
|
482
|
+
vs GSAP `onUpdate` (apples-to-apples): **2.8–4.5× faster**.
|
|
465
483
|
|
|
466
484
|
Run locally: `npm run bench`
|
|
467
485
|
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anim-engine
|
|
3
|
+
description: Renderer-agnostic numeric animation library for JavaScript runtimes. Use when a user has installed 'anim-engine' and needs help writing animation code with tweens, keyframes, timelines, springs, smooth damp, lerp, or color interpolation. Covers all exported primitives, common recipes, game engine integration, and the ticker setup pattern.
|
|
4
|
+
metadata:
|
|
5
|
+
triggers: "anim-engine, animation, createAnimation, createTimeline, createSpring, createSmoothDamp, createLerp, createSmoothClamp, lerpRgba, hexToRgba, animation library, js animation, tween, keyframe animation, timeline animation, spring physics"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# anim-engine
|
|
9
|
+
|
|
10
|
+
A renderer-agnostic animation library for JavaScript runtimes. Pure-numeric, tree-shakeable, zero dependencies. Works with DOM, Canvas, WebGL, Three.js, PixiJS, or any other renderer.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install anim-engine
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Getting Started
|
|
19
|
+
|
|
20
|
+
Import the primitives you need and start a ticker once. The ticker drives all animations — it does **not** auto-start.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createAnimation, getTicker } from "anim-engine";
|
|
24
|
+
|
|
25
|
+
// Start the ticker (rAF mode)
|
|
26
|
+
getTicker().start();
|
|
27
|
+
|
|
28
|
+
const anim = createAnimation({
|
|
29
|
+
from: 0,
|
|
30
|
+
to: 100,
|
|
31
|
+
durationMs: 1000,
|
|
32
|
+
ease: "outCubic",
|
|
33
|
+
onUpdate: (value) => (element.style.transform = `translateX(${value}px)`),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await anim.play();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Ticker Setup
|
|
42
|
+
|
|
43
|
+
The ticker must be started before any animations will advance. There are two modes:
|
|
44
|
+
|
|
45
|
+
### Automatic (rAF)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { getTicker } from "anim-engine";
|
|
49
|
+
getTicker().start(); // Drives via requestAnimationFrame
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Custom game loop for all animations
|
|
53
|
+
|
|
54
|
+
Pass `deltaMs` (milliseconds) each frame:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const ticker = getTicker();
|
|
58
|
+
function loop(dt: number) {
|
|
59
|
+
ticker.update(dt);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### External ticker (game engine integration)
|
|
64
|
+
|
|
65
|
+
Pass an `ExternalTicker` to individual animations to use the engine's own tick:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
type ExternalTicker = {
|
|
69
|
+
add: (handler: TickHandler) => void;
|
|
70
|
+
remove: (handler: TickHandler) => void;
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Three.js:**
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const clock = new THREE.Clock();
|
|
78
|
+
const ticker = getTicker();
|
|
79
|
+
|
|
80
|
+
function animate() {
|
|
81
|
+
requestAnimationFrame(animate);
|
|
82
|
+
ticker.update(clock.getDelta() * 1000);
|
|
83
|
+
renderer.render(scene, camera);
|
|
84
|
+
}
|
|
85
|
+
animate();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**PixiJS:**
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const ticker = getTicker();
|
|
92
|
+
app.ticker.add((delta) => ticker.update(delta.deltaMS));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Primitives
|
|
98
|
+
|
|
99
|
+
Two animation families:
|
|
100
|
+
|
|
101
|
+
| Family | Factories | What it does |
|
|
102
|
+
| ----------------- | ------------------------------------------------ | ------------------------------------------------------------ |
|
|
103
|
+
| **Timed** | `createAnimation`, `createTimeline` | Fixed motion path. Runs once per `play()`. |
|
|
104
|
+
| **Continuous** | `createSpring`, `createSmoothDamp`, `createLerp` | Chases a live target every frame until stopped. |
|
|
105
|
+
| **Pure function** | `createSmoothClamp` | Asymptotic saturation — returns `(input: number) => number`. |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Timed: `createAnimation`
|
|
110
|
+
|
|
111
|
+
### Single tween
|
|
112
|
+
|
|
113
|
+
Animates from one value to another over a duration with easing.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const anim = createAnimation({
|
|
117
|
+
from: 0, // number or () => number
|
|
118
|
+
to: 100, // number or () => number
|
|
119
|
+
durationMs: 1000, // number or () => number
|
|
120
|
+
ease: "outCubic", // EaseName or custom EaseFunction
|
|
121
|
+
onUpdate: (value, velocity) => {
|
|
122
|
+
/* assign value */
|
|
123
|
+
},
|
|
124
|
+
onProgress: (progress) => {}, // 0 → 1
|
|
125
|
+
onStarted: () => {},
|
|
126
|
+
onEnded: () => {},
|
|
127
|
+
ticker: customTicker, // optional ExternalTicker
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await anim.play(); // Promise resolves when animation completes
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Keyframes
|
|
134
|
+
|
|
135
|
+
Multiple segments, each with its own value, duration (`gap`), and easing.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
createAnimation({
|
|
139
|
+
keyframes: [{ value: 0 }, { value: 50, gap: 300, ease: "outCubic" }, { value: 100, gap: 400 }],
|
|
140
|
+
onUpdate: (v) => (sprite.x = v),
|
|
141
|
+
}).play();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The first keyframe provides the starting value; `gap` is the duration from the previous keyframe.
|
|
145
|
+
|
|
146
|
+
### Control handle
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
anim.play() → Promise<void> // Start or restart from beginning
|
|
150
|
+
anim.pause() → void // Freeze at current position
|
|
151
|
+
anim.resume() → void // Continue from paused position
|
|
152
|
+
anim.stop() → void // Stop; promise resolves at current value
|
|
153
|
+
anim.skipToEnd() → void // Jump to end value; promise resolves
|
|
154
|
+
anim.setProgress(p) → void // Jump to progress [0, 1]; pauses if playing
|
|
155
|
+
|
|
156
|
+
anim.value → number // Current interpolated value (readonly)
|
|
157
|
+
anim.velocity → number // Current velocity in units/sec (readonly)
|
|
158
|
+
anim.progress → number // Progress 0→1 (readonly)
|
|
159
|
+
anim.status → "playing" | "paused" | "stopped"
|
|
160
|
+
anim.durationMs → number // Total duration (readonly)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Repeat & yoyo
|
|
164
|
+
|
|
165
|
+
Since `DynamicValue` is re-evaluated each `play()` call, alternating the source toggles direction:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
let forward = true;
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < 6; i++) {
|
|
171
|
+
await createAnimation({
|
|
172
|
+
from: () => (forward ? 1 : 1.3),
|
|
173
|
+
to: () => (forward ? 1.3 : 1),
|
|
174
|
+
durationMs: 600,
|
|
175
|
+
ease: "inOutSine",
|
|
176
|
+
onUpdate: (v) => sprite.scale.set(v),
|
|
177
|
+
}).play();
|
|
178
|
+
forward = !forward;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Timed: `createTimeline`
|
|
185
|
+
|
|
186
|
+
Orchestrate multiple keyframe animations in parallel or sequence.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { createTimeline } from "anim-engine";
|
|
190
|
+
|
|
191
|
+
const timeline = createTimeline(
|
|
192
|
+
[
|
|
193
|
+
// Layer 1 — starts at time 0
|
|
194
|
+
{
|
|
195
|
+
at: 0,
|
|
196
|
+
animation: {
|
|
197
|
+
keyframes: [{ value: 0 }, { value: 1, gap: 500 }],
|
|
198
|
+
onEnded: () => console.log("layer 1 done"),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
// Layer 2 — starts at time 0 (parallel)
|
|
202
|
+
{
|
|
203
|
+
at: 0,
|
|
204
|
+
animation: {
|
|
205
|
+
keyframes: [{ value: -100 }, { value: 0, gap: 800, ease: "outBack" }],
|
|
206
|
+
onUpdate: (v) => (sprite.y = v),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
// Layer 3 — starts 200ms after layer 2 ends
|
|
210
|
+
{
|
|
211
|
+
gap: 200,
|
|
212
|
+
animation: {
|
|
213
|
+
keyframes: [{ value: 0 }, { value: 50, gap: 300 }],
|
|
214
|
+
onEnded: () => console.log("layer 3 done"),
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
{
|
|
219
|
+
// values[i] / velocities[i] correspond to layer i in definition order
|
|
220
|
+
onUpdate: (values, velocities) => {
|
|
221
|
+
sprite.x = values[0]; // layer 1 (fade in)
|
|
222
|
+
sprite.y = values[1]; // layer 2 (slide up)
|
|
223
|
+
sprite.rotation = values[2]; // layer 3 (spin)
|
|
224
|
+
},
|
|
225
|
+
onProgress: (p) => console.log(p),
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- Layer positions: `at: DynamicValue` (absolute ms) or `gap: number` (relative to previous layer end).
|
|
231
|
+
- **Each layer supports its own callbacks.** Pass `onStarted`, `onUpdate`, `onProgress`, and `onEnded` inside the layer's `animation` config to react to per-layer events independently.
|
|
232
|
+
- Timeline handle has the same shape as `Animation`: `play()`, `pause()`, `resume()`, `stop()`, `skipToEnd()`, `setProgress()`.
|
|
233
|
+
- `values` and `velocities` are arrays — one entry per layer in definition order.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Continuous: `createSpring`
|
|
238
|
+
|
|
239
|
+
Mass-spring-damper physics. Bouncy or elastic movement toward a live target.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const spring = createSpring({
|
|
243
|
+
to: () => mouseX, // Re-evaluated every frame
|
|
244
|
+
stiffness: 180, // default — higher = snappier
|
|
245
|
+
damping: 12, // default — higher = less oscillation
|
|
246
|
+
mass: 1, // default — higher = heavier feel
|
|
247
|
+
precision: 0.01, // default — settles within this tolerance
|
|
248
|
+
onUpdate: (value, velocity) => {
|
|
249
|
+
sprite.x = value;
|
|
250
|
+
},
|
|
251
|
+
onEnded: () => {},
|
|
252
|
+
ticker: customTicker,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Later:
|
|
256
|
+
spring.stop();
|
|
257
|
+
spring.setValue(50); // Immediately set position
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Uses Verlet integration for stable, energy-conserving simulation.
|
|
261
|
+
|
|
262
|
+
Starts active automatically. Call `stop()` to halt.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Continuous: `createSmoothDamp`
|
|
267
|
+
|
|
268
|
+
Natural deceleration toward a target (Unity-style `SmoothDamp`).
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createSmoothDamp } from "anim-engine";
|
|
272
|
+
|
|
273
|
+
const damp = createSmoothDamp({
|
|
274
|
+
to: () => targetY,
|
|
275
|
+
smoothTimeMs: 300, // Approximate time to reach target
|
|
276
|
+
maxSpeed: undefined, // Optional velocity cap
|
|
277
|
+
precision: 0.01,
|
|
278
|
+
onUpdate: (value, vel) => {
|
|
279
|
+
/* assign */
|
|
280
|
+
},
|
|
281
|
+
onEnded: () => {},
|
|
282
|
+
ticker: customTicker,
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Continuous: `createLerp`
|
|
289
|
+
|
|
290
|
+
Exponential approach. Always moves toward target, never overshoots.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const lerp = createLerp({
|
|
294
|
+
to: () => targetX,
|
|
295
|
+
smoothTimeMs: 300,
|
|
296
|
+
precision: 0.01,
|
|
297
|
+
onUpdate: (value, vel) => {
|
|
298
|
+
/* assign */
|
|
299
|
+
},
|
|
300
|
+
onEnded: () => {},
|
|
301
|
+
ticker: customTicker,
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Pure function: `createSmoothClamp`
|
|
308
|
+
|
|
309
|
+
Asymptotic saturation — caps a value with smooth deceleration. No control handle, no ticker.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import { createSmoothClamp } from "anim-engine";
|
|
313
|
+
|
|
314
|
+
const clamp = createSmoothClamp(45); // max velocity
|
|
315
|
+
|
|
316
|
+
clamp(1000); // ≈ 44.96 (asymptotic to 45)
|
|
317
|
+
clamp(5); // ≈ 4.99
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Useful for limiting velocity, torque, force, or any value that should approach a maximum smoothly.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Color
|
|
325
|
+
|
|
326
|
+
Perceptually uniform Oklab color interpolation.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { lerpRgba, hexToRgba } from "anim-engine";
|
|
330
|
+
|
|
331
|
+
const from = hexToRgba("#ff6b6b"); // → [1, 0.42, 0.42, 1] (normalized RGBA)
|
|
332
|
+
const to = hexToRgba("#4ecdc4");
|
|
333
|
+
|
|
334
|
+
createAnimation({
|
|
335
|
+
from: 0,
|
|
336
|
+
to: 1,
|
|
337
|
+
durationMs: 2000,
|
|
338
|
+
onUpdate: (t) => {
|
|
339
|
+
const [r, g, b, a] = lerpRgba(from, to, t);
|
|
340
|
+
element.style.color = `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`;
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Accepted hex formats: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Dynamic Values
|
|
350
|
+
|
|
351
|
+
`DynamicValue` (`number | () => number`) lets values be static or computed lazily.
|
|
352
|
+
|
|
353
|
+
Resolution timing depends on the primitive:
|
|
354
|
+
|
|
355
|
+
| Primitive | `from` / `to` / `gap` / `smoothTimeMs` resolved |
|
|
356
|
+
| -------------------------------------------------- | ----------------------------------------------- |
|
|
357
|
+
| `createAnimation` (tween) | Once per `play()` call, cached for duration |
|
|
358
|
+
| `createAnimation` (keyframes) | Once per `play()` call |
|
|
359
|
+
| `createTimeline` | Once per `play()` call |
|
|
360
|
+
| `createSpring` / `createSmoothDamp` / `createLerp` | **Every frame** (target is a live value) |
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
// Static value — resolved once
|
|
364
|
+
createAnimation({ from: 0, to: 100, durationMs: 1000 });
|
|
365
|
+
|
|
366
|
+
// Dynamic — function called at resolution time
|
|
367
|
+
createAnimation({
|
|
368
|
+
from: () => getCurrentX(),
|
|
369
|
+
durationMs: () => 500 / speedMultiplier,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Continuous — function called every frame
|
|
373
|
+
createSpring({ to: () => mouseX });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Easing
|
|
379
|
+
|
|
380
|
+
31 named presets plus custom cubic bezier.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
type EaseName =
|
|
384
|
+
| "linear"
|
|
385
|
+
| "inQuad"
|
|
386
|
+
| "outQuad"
|
|
387
|
+
| "inOutQuad"
|
|
388
|
+
| "inCubic"
|
|
389
|
+
| "outCubic"
|
|
390
|
+
| "inOutCubic"
|
|
391
|
+
| "inQuart"
|
|
392
|
+
| "outQuart"
|
|
393
|
+
| "inOutQuart"
|
|
394
|
+
| "inQuint"
|
|
395
|
+
| "outQuint"
|
|
396
|
+
| "inOutQuint"
|
|
397
|
+
| "inSine"
|
|
398
|
+
| "outSine"
|
|
399
|
+
| "inOutSine"
|
|
400
|
+
| "inExpo"
|
|
401
|
+
| "outExpo"
|
|
402
|
+
| "inOutExpo"
|
|
403
|
+
| "inCirc"
|
|
404
|
+
| "outCirc"
|
|
405
|
+
| "inOutCirc"
|
|
406
|
+
| "inBack"
|
|
407
|
+
| "outBack"
|
|
408
|
+
| "inOutBack"
|
|
409
|
+
| "inElastic"
|
|
410
|
+
| "outElastic"
|
|
411
|
+
| "inOutElastic"
|
|
412
|
+
| "inBounce"
|
|
413
|
+
| "outBounce"
|
|
414
|
+
| "inOutBounce";
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Custom bezier:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { cubicBezier } from "anim-engine";
|
|
421
|
+
|
|
422
|
+
const ease = cubicBezier(0.25, 0.1, 0.25, 1);
|
|
423
|
+
|
|
424
|
+
createAnimation({ from: 0, to: 100, durationMs: 1000, ease });
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Custom function:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
createAnimation({
|
|
431
|
+
from: 0,
|
|
432
|
+
to: 100,
|
|
433
|
+
durationMs: 1000,
|
|
434
|
+
ease: (t) => t * t, // custom quadratic
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Common Recipes
|
|
441
|
+
|
|
442
|
+
### Animating CSS transforms
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
const anim = createAnimation({
|
|
446
|
+
from: 0,
|
|
447
|
+
to: 100,
|
|
448
|
+
durationMs: 2000,
|
|
449
|
+
ease: "outElastic",
|
|
450
|
+
onUpdate: (v) => {
|
|
451
|
+
element.style.transform = `translateX(${v}px) rotate(${v * 0.1}deg)`;
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Animating canvas drawing
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
const ctx = canvas.getContext("2d");
|
|
460
|
+
let progress = 0;
|
|
461
|
+
|
|
462
|
+
createAnimation({
|
|
463
|
+
keyframes: [{ value: 0 }, { value: 1, gap: 1000, ease: "inOutSine" }],
|
|
464
|
+
onUpdate: (v) => {
|
|
465
|
+
progress = v;
|
|
466
|
+
},
|
|
467
|
+
}).play();
|
|
468
|
+
|
|
469
|
+
function draw() {
|
|
470
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
471
|
+
ctx.beginPath();
|
|
472
|
+
ctx.arc(100 + progress * 200, 100, 30, 0, Math.PI * 2);
|
|
473
|
+
ctx.fill();
|
|
474
|
+
requestAnimationFrame(draw);
|
|
475
|
+
}
|
|
476
|
+
draw();
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Spring-loaded UI element
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { createSpring, getTicker } from "anim-engine";
|
|
483
|
+
getTicker().start();
|
|
484
|
+
|
|
485
|
+
createSpring({
|
|
486
|
+
to: () => mouseX,
|
|
487
|
+
stiffness: 160,
|
|
488
|
+
damping: 14,
|
|
489
|
+
onUpdate: (x) => (cursor.style.left = `${x}px`),
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Fade in with smooth damp
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
createSmoothDamp({
|
|
497
|
+
to: () => (visible ? 1 : 0),
|
|
498
|
+
smoothTimeMs: 400,
|
|
499
|
+
onUpdate: (v) => (element.style.opacity = String(v)),
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Polling values in a game loop
|
|
504
|
+
|
|
505
|
+
Read-only `value` and `velocity` properties let you poll instead of using callbacks:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
const spring = createSpring({ to: () => enemy.targetX });
|
|
509
|
+
|
|
510
|
+
function gameLoop(dt: number) {
|
|
511
|
+
ticker.update(dt);
|
|
512
|
+
enemy.x = spring.value; // Read current position
|
|
513
|
+
enemy.vx = spring.velocity; // Read current velocity
|
|
514
|
+
}
|
|
515
|
+
```
|
|
@@ -59,15 +59,17 @@ var createTimeline = (layers, { onStarted, onUpdate, onEnded, onProgress = noOp
|
|
|
59
59
|
let totalDurationMs = state.totalDurationMs;
|
|
60
60
|
let valuesCache = [];
|
|
61
61
|
let velocitiesCache = [];
|
|
62
|
+
let valuesDirty = true;
|
|
62
63
|
const syncValues = () => {
|
|
64
|
+
if (!valuesDirty) return;
|
|
65
|
+
valuesDirty = false;
|
|
63
66
|
valuesCache.length = activeLayers.length;
|
|
64
67
|
velocitiesCache.length = activeLayers.length;
|
|
65
68
|
for (let i = 0; i < activeLayers.length; i++) {
|
|
66
|
-
valuesCache[i] = activeLayers[i].runner.
|
|
69
|
+
valuesCache[i] = activeLayers[i].runner.value;
|
|
67
70
|
velocitiesCache[i] = activeLayers[i].runner.velocity;
|
|
68
71
|
}
|
|
69
72
|
};
|
|
70
|
-
syncValues();
|
|
71
73
|
let timelineStatus = "stopped";
|
|
72
74
|
let elapsedMs = 0;
|
|
73
75
|
let resolvePromise;
|
|
@@ -94,8 +96,11 @@ var createTimeline = (layers, { onStarted, onUpdate, onEnded, onProgress = noOp
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
onUpdate
|
|
99
|
+
valuesDirty = true;
|
|
100
|
+
if (onUpdate) {
|
|
101
|
+
syncValues();
|
|
102
|
+
onUpdate(valuesCache, velocitiesCache);
|
|
103
|
+
}
|
|
99
104
|
onProgress(totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 1);
|
|
100
105
|
if (remainingLayers <= 0) finish();
|
|
101
106
|
};
|
|
@@ -173,9 +178,11 @@ var createTimeline = (layers, { onStarted, onUpdate, onEnded, onProgress = noOp
|
|
|
173
178
|
return totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 1;
|
|
174
179
|
},
|
|
175
180
|
get values() {
|
|
181
|
+
syncValues();
|
|
176
182
|
return valuesCache;
|
|
177
183
|
},
|
|
178
184
|
get velocities() {
|
|
185
|
+
syncValues();
|
|
179
186
|
return velocitiesCache;
|
|
180
187
|
},
|
|
181
188
|
get status() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anim-engine",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "JavaScript library for animating numbers",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
13
|
"README.md",
|
|
14
|
-
"LICENSE.txt"
|
|
14
|
+
"LICENSE.txt",
|
|
15
|
+
"SKILL.md"
|
|
15
16
|
],
|
|
16
17
|
"type": "module",
|
|
17
18
|
"sideEffects": false,
|