anim-engine 0.4.0 → 0.5.1
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 +300 -498
- package/SKILL.md +515 -0
- package/dist/animation/create-animation.d.ts +12 -0
- package/dist/animation/create-animation.js +18 -22
- package/dist/animation/create-single-tween.d.ts +1 -2
- package/dist/animation/runner.d.ts +1 -1
- package/dist/animation/runner.js +22 -22
- package/dist/animation/update.d.ts +1 -1
- package/dist/animation/update.js +4 -4
- package/dist/domain/animation.d.ts +118 -3
- package/dist/domain/color.d.ts +12 -0
- package/dist/domain/easing.d.ts +12 -0
- package/dist/domain/easing.js +8 -0
- package/dist/domain/index.d.ts +2 -2
- package/dist/domain/interpolation.d.ts +128 -5
- package/dist/domain/resolve-value.d.ts +13 -0
- package/dist/domain/resolve-value.js +8 -0
- package/dist/domain/ticker.d.ts +28 -0
- package/dist/domain/timeline.d.ts +47 -2
- package/dist/index.d.ts +1 -1
- package/dist/lerp/create-lerp.d.ts +13 -1
- package/dist/lerp/create-lerp.js +20 -17
- package/dist/smooth-damp/create-smooth-damp.d.ts +15 -1
- package/dist/smooth-damp/create-smooth-damp.js +33 -28
- package/dist/spring/create-spring.d.ts +12 -1
- package/dist/spring/create-spring.js +16 -18
- package/dist/ticker/get-ticker.d.ts +9 -1
- package/dist/ticker/get-ticker.js +9 -1
- package/dist/timeline/create-timeline.d.ts +16 -6
- package/dist/timeline/create-timeline.js +46 -10
- package/package.json +3 -2
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: 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
|
+
```
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import { AnimationOptions, Animation } from '../domain';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an animation instance that can be played, paused, stopped,
|
|
4
|
+
* and queried for its current state.
|
|
5
|
+
*
|
|
6
|
+
* Accepts either a single tween configuration (from → to over a duration)
|
|
7
|
+
* or a keyframe animation with multiple keyframes, each with optional
|
|
8
|
+
* easing and gaps.
|
|
9
|
+
*
|
|
10
|
+
* @param options - The animation configuration, either {@link SingleTweenOptions}
|
|
11
|
+
* or {@link KeyframeAnimationOptions}.
|
|
12
|
+
* @returns An {@link Animation} instance for controlling the animation.
|
|
13
|
+
*/
|
|
2
14
|
export declare const createAnimation: (options: AnimationOptions) => Animation;
|
|
@@ -6,16 +6,27 @@ import { createKeyframeRunner, createTweenRunner } from "./runner.js";
|
|
|
6
6
|
var isKeyframeMode = (options) => {
|
|
7
7
|
return "keyframes" in options && Array.isArray(options.keyframes);
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates an animation instance that can be played, paused, stopped,
|
|
11
|
+
* and queried for its current state.
|
|
12
|
+
*
|
|
13
|
+
* Accepts either a single tween configuration (from → to over a duration)
|
|
14
|
+
* or a keyframe animation with multiple keyframes, each with optional
|
|
15
|
+
* easing and gaps.
|
|
16
|
+
*
|
|
17
|
+
* @param options - The animation configuration, either {@link SingleTweenOptions}
|
|
18
|
+
* or {@link KeyframeAnimationOptions}.
|
|
19
|
+
* @returns An {@link Animation} instance for controlling the animation.
|
|
20
|
+
*/
|
|
9
21
|
var createAnimation = (options) => {
|
|
10
22
|
if (isKeyframeMode(options)) return createKeyframeAnimation(options);
|
|
11
23
|
return createSingleTween(options);
|
|
12
24
|
};
|
|
13
|
-
var createSingleTween = ({
|
|
25
|
+
var createSingleTween = ({ from: rawFrom, to: rawTo, durationMs: rawDurationMs, ease: easeName = "inOutSine", onStarted, onUpdate, onProgress, onEnded, ticker = getTicker() }) => {
|
|
14
26
|
let cachedDurationMs = resolveValue(rawDurationMs);
|
|
15
27
|
let status = "stopped";
|
|
16
28
|
const hasDynamicProperty = typeof rawFrom === "function" || typeof rawTo === "function" || typeof rawDurationMs === "function";
|
|
17
29
|
let resolvePromise;
|
|
18
|
-
const ticker = getTicker();
|
|
19
30
|
const handleEnded = () => {
|
|
20
31
|
status = "stopped";
|
|
21
32
|
ticker.remove(runner);
|
|
@@ -41,7 +52,6 @@ var createSingleTween = ({ onStarted, onUpdate, onProgress, onEnded, from: rawFr
|
|
|
41
52
|
};
|
|
42
53
|
runner = buildRunner();
|
|
43
54
|
const play = () => {
|
|
44
|
-
if (status === "dead") throw new Error("Cannot play a dead animation");
|
|
45
55
|
if (hasDynamicProperty) runner = buildRunner();
|
|
46
56
|
else runner.reset();
|
|
47
57
|
const promise = new Promise((resolve) => {
|
|
@@ -76,11 +86,6 @@ var createSingleTween = ({ onStarted, onUpdate, onProgress, onEnded, from: rawFr
|
|
|
76
86
|
resolvePromise?.();
|
|
77
87
|
resolvePromise = void 0;
|
|
78
88
|
};
|
|
79
|
-
const kill = () => {
|
|
80
|
-
status = "dead";
|
|
81
|
-
ticker.remove(runner);
|
|
82
|
-
resolvePromise = void 0;
|
|
83
|
-
};
|
|
84
89
|
const setProgress = (value) => {
|
|
85
90
|
if (status === "playing") pause();
|
|
86
91
|
runner.evaluate(Math.max(0, Math.min(1, value)));
|
|
@@ -91,9 +96,8 @@ var createSingleTween = ({ onStarted, onUpdate, onProgress, onEnded, from: rawFr
|
|
|
91
96
|
resume,
|
|
92
97
|
stop,
|
|
93
98
|
skipToEnd,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return runner.currentValue;
|
|
99
|
+
get value() {
|
|
100
|
+
return runner.value;
|
|
97
101
|
},
|
|
98
102
|
get velocity() {
|
|
99
103
|
return runner.velocity;
|
|
@@ -110,7 +114,7 @@ var createSingleTween = ({ onStarted, onUpdate, onProgress, onEnded, from: rawFr
|
|
|
110
114
|
}
|
|
111
115
|
};
|
|
112
116
|
};
|
|
113
|
-
var createKeyframeAnimation = ({ keyframes: rawKeyframes, onStarted, onUpdate, onProgress, onEnded }) => {
|
|
117
|
+
var createKeyframeAnimation = ({ keyframes: rawKeyframes, onStarted, onUpdate, onProgress, onEnded, ticker = getTicker() }) => {
|
|
114
118
|
const resolveKeyframeGaps = () => {
|
|
115
119
|
let total = 0;
|
|
116
120
|
for (let i = 1; i < rawKeyframes.length; i++) total += resolveValue(rawKeyframes[i].gap ?? 0);
|
|
@@ -120,7 +124,6 @@ var createKeyframeAnimation = ({ keyframes: rawKeyframes, onStarted, onUpdate, o
|
|
|
120
124
|
let status = "stopped";
|
|
121
125
|
let resolvePromise;
|
|
122
126
|
const hasDynamicProperty = rawKeyframes.some((kf) => typeof kf.value === "function" || typeof kf.gap === "function");
|
|
123
|
-
const ticker = getTicker();
|
|
124
127
|
const handleEnded = () => {
|
|
125
128
|
status = "stopped";
|
|
126
129
|
ticker.remove(runner);
|
|
@@ -144,7 +147,6 @@ var createKeyframeAnimation = ({ keyframes: rawKeyframes, onStarted, onUpdate, o
|
|
|
144
147
|
};
|
|
145
148
|
runner = buildRunner();
|
|
146
149
|
const play = () => {
|
|
147
|
-
if (status === "dead") throw new Error("Cannot play a dead animation");
|
|
148
150
|
if (hasDynamicProperty) {
|
|
149
151
|
runner = buildRunner();
|
|
150
152
|
cachedDurationMs = resolveKeyframeGaps();
|
|
@@ -181,11 +183,6 @@ var createKeyframeAnimation = ({ keyframes: rawKeyframes, onStarted, onUpdate, o
|
|
|
181
183
|
resolvePromise?.();
|
|
182
184
|
resolvePromise = void 0;
|
|
183
185
|
};
|
|
184
|
-
const kill = () => {
|
|
185
|
-
status = "dead";
|
|
186
|
-
ticker.remove(runner);
|
|
187
|
-
resolvePromise = void 0;
|
|
188
|
-
};
|
|
189
186
|
const setProgress = (value) => {
|
|
190
187
|
if (status === "playing") pause();
|
|
191
188
|
runner.evaluate(Math.max(0, Math.min(1, value)));
|
|
@@ -196,9 +193,8 @@ var createKeyframeAnimation = ({ keyframes: rawKeyframes, onStarted, onUpdate, o
|
|
|
196
193
|
resume,
|
|
197
194
|
stop,
|
|
198
195
|
skipToEnd,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return runner.currentValue;
|
|
196
|
+
get value() {
|
|
197
|
+
return runner.value;
|
|
202
198
|
},
|
|
203
199
|
get velocity() {
|
|
204
200
|
return runner.velocity;
|