anim-engine 0.3.2 → 0.5.0

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.
Files changed (46) hide show
  1. package/README.md +283 -499
  2. package/dist/animation/create-animation.d.ts +12 -0
  3. package/dist/animation/create-animation.js +19 -23
  4. package/dist/animation/create-single-tween.d.ts +1 -2
  5. package/dist/animation/index.d.ts +1 -0
  6. package/dist/animation/runner.d.ts +1 -1
  7. package/dist/animation/runner.js +22 -22
  8. package/dist/animation/update.d.ts +1 -1
  9. package/dist/animation/update.js +4 -4
  10. package/dist/domain/animation.d.ts +118 -3
  11. package/dist/domain/color.d.ts +14 -0
  12. package/dist/domain/easing.d.ts +12 -0
  13. package/dist/domain/easing.js +9 -35
  14. package/dist/domain/index.d.ts +4 -4
  15. package/dist/domain/interpolation.d.ts +153 -5
  16. package/dist/domain/resolve-value.d.ts +13 -0
  17. package/dist/domain/resolve-value.js +8 -0
  18. package/dist/domain/ticker.d.ts +26 -14
  19. package/dist/domain/timeline.d.ts +47 -2
  20. package/dist/index.d.ts +10 -13
  21. package/dist/index.js +8 -7
  22. package/dist/lerp/create-lerp.d.ts +14 -9
  23. package/dist/lerp/create-lerp.js +21 -18
  24. package/dist/lerp/index.d.ts +1 -0
  25. package/dist/lerp-rgba/hex-to-rgba.d.ts +13 -0
  26. package/dist/lerp-rgba/hex-to-rgba.js +48 -0
  27. package/dist/lerp-rgba/index.d.ts +2 -0
  28. package/dist/lerp-rgba/lerp-rgba.d.ts +13 -0
  29. package/dist/{color/lerp-oklab.js → lerp-rgba/lerp-rgba.js} +3 -48
  30. package/dist/smooth-clamp/{smooth-clamp.js → create-smooth-clamp.js} +1 -1
  31. package/dist/smooth-clamp/index.d.ts +1 -0
  32. package/dist/smooth-damp/create-smooth-damp.d.ts +16 -10
  33. package/dist/smooth-damp/create-smooth-damp.js +34 -29
  34. package/dist/smooth-damp/index.d.ts +1 -0
  35. package/dist/spring/create-spring.d.ts +13 -11
  36. package/dist/spring/create-spring.js +17 -19
  37. package/dist/spring/index.d.ts +1 -0
  38. package/dist/ticker/get-ticker.d.ts +25 -0
  39. package/dist/{domain/ticker.js → ticker/get-ticker.js} +10 -2
  40. package/dist/ticker/index.d.ts +1 -0
  41. package/dist/timeline/create-timeline.d.ts +16 -6
  42. package/dist/timeline/create-timeline.js +41 -13
  43. package/dist/timeline/index.d.ts +1 -0
  44. package/package.json +1 -1
  45. package/dist/color/lerp-oklab.d.ts +0 -26
  46. /package/dist/smooth-clamp/{smooth-clamp.d.ts → create-smooth-clamp.d.ts} +0 -0
@@ -1,20 +1,43 @@
1
- import { getTicker } from "../domain/ticker.js";
2
1
  import { resolveValue } from "../domain/resolve-value.js";
2
+ import { getTicker } from "../ticker/get-ticker.js";
3
3
  import { smoothDampStep } from "./step.js";
4
4
  //#region src/smooth-damp/create-smooth-damp.ts
5
- var createSmoothDamp = (options) => {
6
- const precision = options.precision ?? .01;
7
- const onUpdate = options.onUpdate;
8
- const onEnded = options.onEnded;
5
+ /**
6
+ * Creates a smooth damped interpolation that progressively moves a value
7
+ * toward a target with velocity that decreases as it approaches, producing
8
+ * a natural deceleration effect.
9
+ *
10
+ * Unlike a simple lerp, smooth damp respects an optional max speed and
11
+ * maintains velocity continuity, making it suitable for camera-relative
12
+ * movement, UI animations, and game object tracking.
13
+ *
14
+ * The target is re-evaluated every frame, allowing it to change dynamically.
15
+ *
16
+ * @param options - Configuration options for the smooth damp interpolation.
17
+ * @returns An {@link Interpolation} instance for controlling the smooth damp.
18
+ */
19
+ var createSmoothDamp = ({ to, smoothTimeMs: rawSmoothTimeMs, maxSpeed: rawMaxSpeed, precision = .01, onUpdate, onEnded, ticker = getTicker() }) => {
9
20
  const state = {
10
21
  current: 0,
11
22
  velocity: 0
12
23
  };
13
24
  let active = true;
14
- const ticker = getTicker();
15
- state.current = options.to();
25
+ state.current = to();
26
+ const update = (deltaMs) => {
27
+ if (!active) return;
28
+ const target = to();
29
+ const smoothTimeMs = resolveValue(rawSmoothTimeMs);
30
+ const maxSpeed = rawMaxSpeed !== void 0 ? resolveValue(rawMaxSpeed) : Infinity;
31
+ smoothDampStep(state, target, smoothTimeMs, maxSpeed, deltaMs);
32
+ onUpdate?.(state.current, state.velocity);
33
+ if (onEnded && Math.abs(state.current - target) < precision && Math.abs(state.velocity) < precision) {
34
+ state.current = target;
35
+ state.velocity = 0;
36
+ onEnded();
37
+ }
38
+ };
16
39
  ticker.add(update);
17
- const start = () => {
40
+ const resume = () => {
18
41
  if (active) return;
19
42
  active = true;
20
43
  ticker.add(update);
@@ -23,32 +46,14 @@ var createSmoothDamp = (options) => {
23
46
  active = false;
24
47
  ticker.remove(update);
25
48
  };
26
- const kill = () => {
27
- active = false;
28
- ticker.remove(update);
29
- };
30
- function update(deltaMs) {
31
- if (!active) return;
32
- const target = options.to();
33
- const smoothTimeMs = resolveValue(options.smoothTimeMs);
34
- const maxSpeed = options.maxSpeed !== void 0 ? resolveValue(options.maxSpeed) : Infinity;
35
- smoothDampStep(state, target, smoothTimeMs, maxSpeed, deltaMs);
36
- onUpdate?.(state.current, state.velocity);
37
- if (onEnded && Math.abs(state.current - target) < precision && Math.abs(state.velocity) < precision) {
38
- state.current = target;
39
- state.velocity = 0;
40
- onEnded();
41
- }
42
- }
43
49
  return {
44
- start,
50
+ resume,
45
51
  stop,
46
- kill,
47
- setCurrentValue: (value) => {
52
+ setValue: (value) => {
48
53
  state.current = value;
49
54
  state.velocity = 0;
50
55
  },
51
- get currentValue() {
56
+ get value() {
52
57
  return state.current;
53
58
  },
54
59
  get velocity() {
@@ -0,0 +1 @@
1
+ export { createSmoothDamp } from './create-smooth-damp';
@@ -1,11 +1,13 @@
1
- import { Interpolation, DynamicValue } from '../domain';
2
- export type SpringOptions = {
3
- to: () => number;
4
- stiffness?: DynamicValue;
5
- damping?: DynamicValue;
6
- mass?: DynamicValue;
7
- precision?: number;
8
- onUpdate?: (value: number, velocity: number) => void;
9
- onEnded?: () => void;
10
- };
11
- export declare const createSpring: (options: SpringOptions) => Interpolation;
1
+ import { Interpolation, SpringOptions } from '../domain';
2
+ /**
3
+ * Creates a spring-based interpolation that simulates mass-spring-damper
4
+ * physics, producing bouncy or elastic movement toward the target value.
5
+ *
6
+ * Uses Verlet integration for stable, energy-conserving simulation.
7
+ * Higher stiffness produces faster, snappier motion; higher damping
8
+ * reduces oscillation. The target is re-evaluated every frame.
9
+ *
10
+ * @param options - Configuration options for the spring interpolation.
11
+ * @returns An {@link Interpolation} instance for controlling the spring.
12
+ */
13
+ export declare const createSpring: ({ precision, onUpdate, onEnded, to: rawTo, stiffness: rawStiffness, damping: rawDamping, mass: rawMass, ticker, }: SpringOptions) => Interpolation;
@@ -1,23 +1,26 @@
1
- import { getTicker } from "../domain/ticker.js";
2
1
  import { resolveValue } from "../domain/resolve-value.js";
2
+ import { getTicker } from "../ticker/get-ticker.js";
3
3
  import { verletStep } from "./verlet.js";
4
4
  //#region src/spring/create-spring.ts
5
- var createSpring = (options) => {
6
- const precision = options.precision ?? .01;
7
- const onUpdate = options.onUpdate;
8
- const onEnded = options.onEnded;
9
- const rawTo = options.to;
10
- const rawStiffness = options.stiffness ?? 180;
11
- const rawDamping = options.damping ?? 12;
12
- const rawMass = options.mass ?? 1;
5
+ /**
6
+ * Creates a spring-based interpolation that simulates mass-spring-damper
7
+ * physics, producing bouncy or elastic movement toward the target value.
8
+ *
9
+ * Uses Verlet integration for stable, energy-conserving simulation.
10
+ * Higher stiffness produces faster, snappier motion; higher damping
11
+ * reduces oscillation. The target is re-evaluated every frame.
12
+ *
13
+ * @param options - Configuration options for the spring interpolation.
14
+ * @returns An {@link Interpolation} instance for controlling the spring.
15
+ */
16
+ var createSpring = ({ precision = .01, onUpdate, onEnded, to: rawTo, stiffness: rawStiffness = 180, damping: rawDamping = 12, mass: rawMass = 1, ticker = getTicker() }) => {
13
17
  const state = {
14
18
  current: rawTo(),
15
19
  velocity: 0
16
20
  };
17
21
  let active = true;
18
- const ticker = getTicker();
19
22
  ticker.add(update);
20
- const start = () => {
23
+ const resume = () => {
21
24
  if (active) return;
22
25
  active = true;
23
26
  ticker.add(update);
@@ -26,10 +29,6 @@ var createSpring = (options) => {
26
29
  active = false;
27
30
  ticker.remove(update);
28
31
  };
29
- const kill = () => {
30
- active = false;
31
- ticker.remove(update);
32
- };
33
32
  function update(deltaMs) {
34
33
  if (!active) return;
35
34
  const target = rawTo();
@@ -45,14 +44,13 @@ var createSpring = (options) => {
45
44
  }
46
45
  }
47
46
  return {
48
- start,
47
+ resume,
49
48
  stop,
50
- kill,
51
- setCurrentValue: (value) => {
49
+ setValue: (value) => {
52
50
  state.current = value;
53
51
  state.velocity = 0;
54
52
  },
55
- get currentValue() {
53
+ get value() {
56
54
  return state.current;
57
55
  },
58
56
  get velocity() {
@@ -0,0 +1 @@
1
+ export { createSpring } from './create-spring';
@@ -0,0 +1,25 @@
1
+ import { Ticker } from '../domain';
2
+ /**
3
+ * Returns the application-wide default ticker singleton.
4
+ *
5
+ * The ticker is created lazily on first access and reused for all
6
+ * subsequent calls. It drives all animations and interpolations that
7
+ * don't specify an external ticker.
8
+ *
9
+ * @returns The default {@link Ticker} instance.
10
+ */
11
+ export declare const getTicker: () => Ticker;
12
+ /**
13
+ * Create a ticker that drives active animations.
14
+ *
15
+ * Does NOT auto-start. The user must explicitly call either:
16
+ * - `start()` — begins a `requestAnimationFrame` loop
17
+ * - `update(deltaMs)` — drive manually from a game loop
18
+ *
19
+ * `add()` and `remove()` register/unregister animations without
20
+ * side effects on the rAF loop.
21
+ *
22
+ * Uses a flat array with undefined-tombstone removal for safe concurrent
23
+ * modification during iteration. Compacted after each frame.
24
+ */
25
+ export declare const createTicker: () => Ticker;
@@ -1,6 +1,14 @@
1
- //#region src/domain/ticker.ts
1
+ //#region src/ticker/get-ticker.ts
2
2
  var singleton;
3
- /** Returns the default ticker singleton. Created lazily on first access. */
3
+ /**
4
+ * Returns the application-wide default ticker singleton.
5
+ *
6
+ * The ticker is created lazily on first access and reused for all
7
+ * subsequent calls. It drives all animations and interpolations that
8
+ * don't specify an external ticker.
9
+ *
10
+ * @returns The default {@link Ticker} instance.
11
+ */
4
12
  var getTicker = () => {
5
13
  if (!singleton) singleton = createTicker();
6
14
  return singleton;
@@ -0,0 +1 @@
1
+ export { getTicker } from './get-ticker';
@@ -1,6 +1,16 @@
1
- import { TimelineLayer, Timeline } from '../domain';
2
- export declare const createTimeline: (layers: TimelineLayer[], options?: {
3
- onStarted?: () => void;
4
- onProgress?: (progress: number) => void;
5
- onEnded?: () => void;
6
- }) => Timeline;
1
+ import { TimelineLayer, Timeline, TimelineCallbacks, ExternalTicker } from '../domain';
2
+ /**
3
+ * Creates a timeline animation that sequences multiple keyframe animation
4
+ * layers, each starting at a specified time or gap after the previous layer.
5
+ *
6
+ * The timeline aggregates all layers into a single controllable animation
7
+ * with its own play/pause/resume/stop semantics and returns the combined
8
+ * values and velocities of all layers on each update.
9
+ *
10
+ * @param layers - An array of {@link TimelineLayer} configs describing each
11
+ * animation in the sequence and when it should play.
12
+ * @param callbacks - Optional callback hooks and configuration.
13
+ * @param ticker - An optional external ticker. Defaults to the global ticker.
14
+ * @returns A {@link Timeline} instance for controlling the sequenced animation.
15
+ */
16
+ export declare const createTimeline: (layers: TimelineLayer[], { onStarted, onUpdate, onEnded, onProgress }?: TimelineCallbacks, ticker?: ExternalTicker) => Timeline;
@@ -1,9 +1,9 @@
1
1
  import { resolveEasing } from "../domain/easing.js";
2
- import { getTicker } from "../domain/ticker.js";
3
2
  import { resolveValue } from "../domain/resolve-value.js";
3
+ import { getTicker } from "../ticker/get-ticker.js";
4
4
  import { createKeyframeRunner } from "../animation/runner.js";
5
5
  //#region src/timeline/create-timeline.ts
6
- var noop = () => {};
6
+ var noOp = () => {};
7
7
  var buildFromConfigs = (rawLayers) => {
8
8
  const activeLayers = [];
9
9
  let previousEndAt = 0;
@@ -38,18 +38,40 @@ var buildFromConfigs = (rawLayers) => {
38
38
  totalDurationMs: Math.max(0, ...activeLayers.map((l) => l.endAt))
39
39
  };
40
40
  };
41
- var createTimeline = (layers, options) => {
42
- const { onStarted, onEnded } = options ?? {};
43
- const onProgress = options?.onProgress ?? noop;
41
+ /**
42
+ * Creates a timeline animation that sequences multiple keyframe animation
43
+ * layers, each starting at a specified time or gap after the previous layer.
44
+ *
45
+ * The timeline aggregates all layers into a single controllable animation
46
+ * with its own play/pause/resume/stop semantics and returns the combined
47
+ * values and velocities of all layers on each update.
48
+ *
49
+ * @param layers - An array of {@link TimelineLayer} configs describing each
50
+ * animation in the sequence and when it should play.
51
+ * @param callbacks - Optional callback hooks and configuration.
52
+ * @param ticker - An optional external ticker. Defaults to the global ticker.
53
+ * @returns A {@link Timeline} instance for controlling the sequenced animation.
54
+ */
55
+ var createTimeline = (layers, { onStarted, onUpdate, onEnded, onProgress = noOp } = {}, ticker = getTicker()) => {
44
56
  const rawLayers = layers;
45
57
  let state = buildFromConfigs(rawLayers);
46
58
  let activeLayers = state.activeLayers;
47
59
  let totalDurationMs = state.totalDurationMs;
60
+ let valuesCache = [];
61
+ let velocitiesCache = [];
62
+ const syncValues = () => {
63
+ valuesCache.length = activeLayers.length;
64
+ velocitiesCache.length = activeLayers.length;
65
+ for (let i = 0; i < activeLayers.length; i++) {
66
+ valuesCache[i] = activeLayers[i].runner.velocity;
67
+ velocitiesCache[i] = activeLayers[i].runner.velocity;
68
+ }
69
+ };
70
+ syncValues();
48
71
  let timelineStatus = "stopped";
49
72
  let elapsedMs = 0;
50
73
  let resolvePromise;
51
74
  let remainingLayers = activeLayers.length;
52
- const ticker = getTicker();
53
75
  const finish = () => {
54
76
  timelineStatus = "stopped";
55
77
  ticker.remove(update);
@@ -72,15 +94,19 @@ var createTimeline = (layers, options) => {
72
94
  }
73
95
  }
74
96
  }
97
+ syncValues();
98
+ onUpdate?.(valuesCache, velocitiesCache);
75
99
  onProgress(totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 1);
76
100
  if (remainingLayers <= 0) finish();
77
101
  };
78
102
  const play = () => {
79
- if (timelineStatus === "dead") throw new Error("Cannot play a dead timeline");
80
103
  state = buildFromConfigs(rawLayers);
81
104
  activeLayers = state.activeLayers;
82
105
  totalDurationMs = state.totalDurationMs;
83
106
  remainingLayers = activeLayers.length;
107
+ valuesCache = [];
108
+ velocitiesCache = [];
109
+ syncValues();
84
110
  elapsedMs = 0;
85
111
  const promise = new Promise((resolve) => {
86
112
  resolvePromise = resolve;
@@ -112,17 +138,13 @@ var createTimeline = (layers, options) => {
112
138
  layer.runner.evaluate(1);
113
139
  layer.runner.onEnded?.();
114
140
  }
141
+ syncValues();
115
142
  timelineStatus = "stopped";
116
143
  ticker.remove(update);
117
144
  onEnded?.();
118
145
  resolvePromise?.();
119
146
  resolvePromise = void 0;
120
147
  };
121
- const kill = () => {
122
- timelineStatus = "dead";
123
- ticker.remove(update);
124
- resolvePromise = void 0;
125
- };
126
148
  const setProgress = (value) => {
127
149
  if (timelineStatus === "playing") pause();
128
150
  elapsedMs = Math.max(0, Math.min(1, value)) * totalDurationMs;
@@ -137,6 +159,7 @@ var createTimeline = (layers, options) => {
137
159
  layer.runner.evaluate(localProgress);
138
160
  layer.ended = localProgress >= 1;
139
161
  }
162
+ syncValues();
140
163
  remainingLayers = activeLayers.filter((l) => !l.ended).length;
141
164
  };
142
165
  return {
@@ -145,11 +168,16 @@ var createTimeline = (layers, options) => {
145
168
  resume,
146
169
  stop,
147
170
  skipToEnd,
148
- kill,
149
171
  setProgress,
150
172
  get progress() {
151
173
  return totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 1;
152
174
  },
175
+ get values() {
176
+ return valuesCache;
177
+ },
178
+ get velocities() {
179
+ return velocitiesCache;
180
+ },
153
181
  get status() {
154
182
  return timelineStatus;
155
183
  },
@@ -0,0 +1 @@
1
+ export { createTimeline } from './create-timeline';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anim-engine",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "JavaScript library for animating numbers",
6
6
  "license": "MIT",
@@ -1,26 +0,0 @@
1
- export type RgbaTuple = readonly [number, number, number, number];
2
- /**
3
- * Interpolate between two RGBA colors in Oklab space.
4
- *
5
- * The RGB channels are converted to Oklab, lerped perceptually uniformly,
6
- * and converted back. The alpha channel is lerped linearly.
7
- *
8
- * @param from - Starting RGBA tuple [R, G, B, A], each 0–1.
9
- * @param to - Ending RGBA tuple [R, G, B, A], each 0–1.
10
- * @param progress - Interpolation factor (0 = from, 1 = to).
11
- * @returns A new RGBA tuple [R, G, B, A], each 0–1.
12
- */
13
- export declare const lerpOklab: (from: RgbaTuple, to: RgbaTuple, progress: number) => [number, number, number, number];
14
- /**
15
- * Parse a hex color string into an RGBA tuple.
16
- *
17
- * Accepts formats:
18
- * - `#RGB`
19
- * - `#RGBA`
20
- * - `#RRGGBB`
21
- * - `#RRGGBBAA`
22
- *
23
- * @param hex - Hex color string with optional leading `#`.
24
- * @returns An RGBA tuple [R, G, B, A], each 0–1.
25
- */
26
- export declare const hexToRgba: (hex: string) => [number, number, number, number];