anim-engine 0.1.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.
Files changed (37) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +639 -0
  3. package/dist/animation/create-animation.d.ts +24 -0
  4. package/dist/animation/create-animation.js +336 -0
  5. package/dist/animation/update.d.ts +12 -0
  6. package/dist/animation/update.js +22 -0
  7. package/dist/benchmarks/easing.bench.d.ts +1 -0
  8. package/dist/benchmarks/vs-gsap.bench.d.ts +1 -0
  9. package/dist/color/lerp-oklab.d.ts +26 -0
  10. package/dist/color/lerp-oklab.js +182 -0
  11. package/dist/easing/easing.d.ts +16 -0
  12. package/dist/easing/easing.js +154 -0
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.js +10 -0
  15. package/dist/lerp/create-lerp.d.ts +9 -0
  16. package/dist/lerp/create-lerp.js +65 -0
  17. package/dist/lerp/step.d.ts +14 -0
  18. package/dist/lerp/step.js +14 -0
  19. package/dist/shared/internal.d.ts +4 -0
  20. package/dist/shared/types.d.ts +29 -0
  21. package/dist/smooth-clamp/smooth-clamp.d.ts +13 -0
  22. package/dist/smooth-clamp/smooth-clamp.js +22 -0
  23. package/dist/smooth-damp/create-smooth-damp.d.ts +10 -0
  24. package/dist/smooth-damp/create-smooth-damp.js +63 -0
  25. package/dist/smooth-damp/step.d.ts +13 -0
  26. package/dist/smooth-damp/step.js +26 -0
  27. package/dist/spring/create-spring.d.ts +11 -0
  28. package/dist/spring/create-spring.js +67 -0
  29. package/dist/spring/verlet.d.ts +13 -0
  30. package/dist/spring/verlet.js +9 -0
  31. package/dist/ticker/get-ticker.d.ts +5 -0
  32. package/dist/ticker/get-ticker.js +10 -0
  33. package/dist/ticker/ticker.d.ts +22 -0
  34. package/dist/ticker/ticker.js +74 -0
  35. package/dist/timeline/create-timeline.d.ts +25 -0
  36. package/dist/timeline/create-timeline.js +139 -0
  37. package/package.json +47 -0
@@ -0,0 +1,154 @@
1
+ //#region src/easing/easing.ts
2
+ var pow = Math.pow;
3
+ var sqrt = Math.sqrt;
4
+ var sin = Math.sin;
5
+ var cos = Math.cos;
6
+ var PI = Math.PI;
7
+ var c1 = 1.70158;
8
+ var c2 = c1 * 1.525;
9
+ var c3 = 2.70158;
10
+ var c4 = 2 * PI / 3;
11
+ var c5 = 2 * PI / 4.5;
12
+ var bounceOut = (x) => {
13
+ const n1 = 7.5625;
14
+ const d1 = 2.75;
15
+ if (x < 1 / d1) return n1 * x * x;
16
+ else if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + .75;
17
+ else if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + .9375;
18
+ else return n1 * (x -= 2.625 / d1) * x + .984375;
19
+ };
20
+ /** All 31 named easing identifiers, ordered by type. */
21
+ var EASE_NAMES = [
22
+ "linear",
23
+ "inQuad",
24
+ "outQuad",
25
+ "inOutQuad",
26
+ "inCubic",
27
+ "outCubic",
28
+ "inOutCubic",
29
+ "inQuart",
30
+ "outQuart",
31
+ "inOutQuart",
32
+ "inQuint",
33
+ "outQuint",
34
+ "inOutQuint",
35
+ "inSine",
36
+ "outSine",
37
+ "inOutSine",
38
+ "inExpo",
39
+ "outExpo",
40
+ "inOutExpo",
41
+ "inCirc",
42
+ "outCirc",
43
+ "inOutCirc",
44
+ "inBack",
45
+ "outBack",
46
+ "inOutBack",
47
+ "inElastic",
48
+ "outElastic",
49
+ "inOutElastic",
50
+ "inBounce",
51
+ "outBounce",
52
+ "inOutBounce"
53
+ ];
54
+ var easingFunctions = {
55
+ linear: (x) => x,
56
+ inQuad: (x) => x * x,
57
+ outQuad: (x) => 1 - (1 - x) * (1 - x),
58
+ inOutQuad: (x) => x < .5 ? 2 * x * x : 1 - pow(-2 * x + 2, 2) / 2,
59
+ inCubic: (x) => x * x * x,
60
+ outCubic: (x) => {
61
+ const t = 1 - x;
62
+ return 1 - t * t * t;
63
+ },
64
+ inOutCubic: (x) => {
65
+ if (x < .5) return 4 * x * x * x;
66
+ const t = -2 * x + 2;
67
+ return 1 - t * t * t / 2;
68
+ },
69
+ inQuart: (x) => x * x * x * x,
70
+ outQuart: (x) => {
71
+ const t = 1 - x;
72
+ const t2 = t * t;
73
+ return 1 - t2 * t2;
74
+ },
75
+ inOutQuart: (x) => {
76
+ if (x < .5) return 8 * x * x * x * x;
77
+ const t = -2 * x + 2;
78
+ return 1 - t * t * t * t / 2;
79
+ },
80
+ inQuint: (x) => x * x * x * x * x,
81
+ outQuint: (x) => {
82
+ const t = 1 - x;
83
+ const t2 = t * t;
84
+ return 1 - t2 * t2 * t;
85
+ },
86
+ inOutQuint: (x) => {
87
+ if (x < .5) return 16 * x * x * x * x * x;
88
+ const t = -2 * x + 2;
89
+ return 1 - t * t * t * t * t / 2;
90
+ },
91
+ inSine: (x) => 1 - cos(x * PI / 2),
92
+ outSine: (x) => sin(x * PI / 2),
93
+ inOutSine: (x) => -(cos(PI * x) - 1) / 2,
94
+ inExpo: (x) => x === 0 ? 0 : pow(2, 10 * x - 10),
95
+ outExpo: (x) => x === 1 ? 1 : 1 - pow(2, -10 * x),
96
+ inOutExpo: (x) => x === 0 ? 0 : x === 1 ? 1 : x < .5 ? pow(2, 20 * x - 10) / 2 : (2 - pow(2, -20 * x + 10)) / 2,
97
+ inCirc: (x) => 1 - sqrt(1 - x * x),
98
+ outCirc: (x) => sqrt(1 - (x - 1) * (x - 1)),
99
+ inOutCirc: (x) => x < .5 ? (1 - sqrt(1 - 4 * x * x)) / 2 : (sqrt(1 - 4 * (x - 1) * (x - 1)) + 1) / 2,
100
+ inBack: (x) => c3 * x * x * x - c1 * x * x,
101
+ outBack: (x) => {
102
+ const t = x - 1;
103
+ return 1 + c3 * t * t * t + c1 * t * t;
104
+ },
105
+ inOutBack: (x) => x < .5 ? 4 * x * x * (3.5949095 * 2 * x - c2) / 2 : (4 * (x - 1) * (x - 1) * (3.5949095 * (x * 2 - 2) + c2) + 2) / 2,
106
+ inElastic: (x) => x === 0 ? 0 : x === 1 ? 1 : -pow(2, 10 * x - 10) * sin((x * 10 - 10.75) * c4),
107
+ outElastic: (x) => x === 0 ? 0 : x === 1 ? 1 : pow(2, -10 * x) * sin((x * 10 - .75) * c4) + 1,
108
+ inOutElastic: (x) => x === 0 ? 0 : x === 1 ? 1 : x < .5 ? -(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * c5)) / 2 : pow(2, -20 * x + 10) * sin((20 * x - 11.125) * c5) / 2 + 1,
109
+ inBounce: (x) => 1 - bounceOut(1 - x),
110
+ outBounce: bounceOut,
111
+ inOutBounce: (x) => x < .5 ? (1 - bounceOut(1 - 2 * x)) / 2 : (1 + bounceOut(2 * x - 1)) / 2
112
+ };
113
+ /**
114
+ * Cubic bezier easing. Builds a pre-computed lookup table at construction time
115
+ * for O(log n) binary search at runtime — no Newton iteration, no allocation
116
+ * per frame.
117
+ *
118
+ * @param p1x - First control point x coordinate.
119
+ * @param p1y - First control point y coordinate.
120
+ * @param p2x - Second control point x coordinate.
121
+ * @param p2y - Second control point y coordinate.
122
+ * @returns An EaseFunction suitable for use with animate() or createTimeline().
123
+ */
124
+ var cubicBezier = (p1x, p1y, p2x, p2y) => {
125
+ const sampleSize = 64;
126
+ const samplesX = new Float64Array(sampleSize);
127
+ const samplesY = new Float64Array(sampleSize);
128
+ for (let index = 0; index < sampleSize; index++) {
129
+ const t = index / (sampleSize - 1);
130
+ const tInv = 1 - t;
131
+ const tInvSq = tInv * tInv;
132
+ const tSq = t * t;
133
+ samplesX[index] = 3 * tInvSq * t * p1x + 3 * tInv * tSq * p2x + tSq * t;
134
+ samplesY[index] = 3 * tInvSq * t * p1y + 3 * tInv * tSq * p2y + tSq * t;
135
+ }
136
+ return (x) => {
137
+ if (x <= 0) return 0;
138
+ if (x >= 1) return 1;
139
+ let low = 0;
140
+ let high = sampleSize - 1;
141
+ while (high - low > 1) {
142
+ const mid = low + high >>> 1;
143
+ if (samplesX[mid] <= x) low = mid;
144
+ else high = mid;
145
+ }
146
+ const xLow = samplesX[low];
147
+ const xHigh = samplesX[high];
148
+ const fraction = (x - xLow) / (xHigh - xLow);
149
+ const yLow = samplesY[low];
150
+ return yLow + fraction * (samplesY[high] - yLow);
151
+ };
152
+ };
153
+ //#endregion
154
+ export { EASE_NAMES, cubicBezier, easingFunctions };
@@ -0,0 +1,17 @@
1
+ export { createAnimation } from './animation/create-animation';
2
+ export { createTimeline } from './timeline/create-timeline';
3
+ export { createSpring } from './spring/create-spring';
4
+ export { createSmoothDamp } from './smooth-damp/create-smooth-damp';
5
+ export { createLerp } from './lerp/create-lerp';
6
+ export { createSmoothClamp } from './smooth-clamp/smooth-clamp';
7
+ export { lerpOklab, hexToRgba } from './color/lerp-oklab';
8
+ export { getTicker } from './ticker/get-ticker';
9
+ export { EASE_NAMES, cubicBezier } from './easing/easing';
10
+ export type { Keyframe, AnimationOptions } from './animation/create-animation';
11
+ export type { Animation, Interpolation, EaseName, AnimationStatus, InterpolationStatus, DynamicValue, } from './shared/types';
12
+ export type { SpringOptions } from './spring/create-spring';
13
+ export type { SmoothDampOptions } from './smooth-damp/create-smooth-damp';
14
+ export type { LerpOptions } from './lerp/create-lerp';
15
+ export type { TimelineLayer, Timeline } from './timeline/create-timeline';
16
+ export type { RgbaTuple } from './color/lerp-oklab';
17
+ export type { TickerControls } from './ticker/ticker';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { EASE_NAMES, cubicBezier } from "./easing/easing.js";
2
+ import { getTicker } from "./ticker/get-ticker.js";
3
+ import { createAnimation } from "./animation/create-animation.js";
4
+ import { createTimeline } from "./timeline/create-timeline.js";
5
+ import { createSpring } from "./spring/create-spring.js";
6
+ import { createSmoothDamp } from "./smooth-damp/create-smooth-damp.js";
7
+ import { createLerp } from "./lerp/create-lerp.js";
8
+ import { createSmoothClamp } from "./smooth-clamp/smooth-clamp.js";
9
+ import { hexToRgba, lerpOklab } from "./color/lerp-oklab.js";
10
+ export { EASE_NAMES, createAnimation, createLerp, createSmoothClamp, createSmoothDamp, createSpring, createTimeline, cubicBezier, getTicker, hexToRgba, lerpOklab };
@@ -0,0 +1,9 @@
1
+ import { Interpolation, DynamicValue } from '../shared/types';
2
+ export type LerpOptions = {
3
+ to: () => number;
4
+ smoothTimeMs: DynamicValue;
5
+ precision?: number;
6
+ onUpdate?: (value: number, velocity: number) => void;
7
+ onEnded?: () => void;
8
+ };
9
+ export declare const createLerp: (options: LerpOptions) => Interpolation;
@@ -0,0 +1,65 @@
1
+ import { getTicker } from "../ticker/get-ticker.js";
2
+ import { lerpStep } from "./step.js";
3
+ //#region src/lerp/create-lerp.ts
4
+ var createLerp = (options) => {
5
+ const precision = options.precision ?? .01;
6
+ const onUpdate = options.onUpdate;
7
+ const onEnded = options.onEnded;
8
+ const state = { current: 0 };
9
+ let previousValue = 0;
10
+ let currentVelocity = 0;
11
+ let active = true;
12
+ const ticker = getTicker();
13
+ const resolveValue = (v) => typeof v === "function" ? v() : v;
14
+ state.current = options.to();
15
+ previousValue = state.current;
16
+ ticker.add(update);
17
+ const start = () => {
18
+ if (active) return;
19
+ active = true;
20
+ ticker.add(update);
21
+ };
22
+ const stop = () => {
23
+ active = false;
24
+ ticker.remove(update);
25
+ };
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
+ lerpStep(state, target, smoothTimeMs, deltaMs);
35
+ currentVelocity = (state.current - previousValue) / (deltaMs / 1e3);
36
+ previousValue = state.current;
37
+ onUpdate?.(state.current, currentVelocity);
38
+ if (onEnded && Math.abs(state.current - target) < precision && Math.abs(currentVelocity) < precision) {
39
+ state.current = target;
40
+ currentVelocity = 0;
41
+ onEnded();
42
+ }
43
+ }
44
+ return {
45
+ start,
46
+ stop,
47
+ kill,
48
+ setCurrentValue: (value) => {
49
+ state.current = value;
50
+ previousValue = value;
51
+ currentVelocity = 0;
52
+ },
53
+ get currentValue() {
54
+ return state.current;
55
+ },
56
+ get velocity() {
57
+ return currentVelocity;
58
+ },
59
+ get status() {
60
+ return active ? "active" : "inactive";
61
+ }
62
+ };
63
+ };
64
+ //#endregion
65
+ export { createLerp };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Lerp step function — first-order exponential approach.
3
+ * Mutates `state` in place — zero allocation.
4
+ */
5
+ export type LerpState = {
6
+ current: number;
7
+ };
8
+ /**
9
+ * One frame of lerp approach.
10
+ * Frame-rate independent. Approaches target asymptotically:
11
+ * current += (target - current) * rate * deltaTime
12
+ * where rate = 3 / (smoothTimeMs / 1000) ≈ reaches 95% in smoothTimeMs
13
+ */
14
+ export declare const lerpStep: (state: LerpState, target: number, smoothTimeMs: number, deltaMs: number) => void;
@@ -0,0 +1,14 @@
1
+ //#region src/lerp/step.ts
2
+ /**
3
+ * One frame of lerp approach.
4
+ * Frame-rate independent. Approaches target asymptotically:
5
+ * current += (target - current) * rate * deltaTime
6
+ * where rate = 3 / (smoothTimeMs / 1000) ≈ reaches 95% in smoothTimeMs
7
+ */
8
+ var lerpStep = (state, target, smoothTimeMs, deltaMs) => {
9
+ const deltaTime = deltaMs / 1e3;
10
+ const rate = 3 / Math.max(1e-4, smoothTimeMs / 1e3);
11
+ state.current += (target - state.current) * rate * deltaTime;
12
+ };
13
+ //#endregion
14
+ export { lerpStep };
@@ -0,0 +1,4 @@
1
+ /** An object that can be driven by the ticker. */
2
+ export type Updateable = {
3
+ update(deltaMs: number): void;
4
+ };
@@ -0,0 +1,29 @@
1
+ export type EaseName = "linear" | "inQuad" | "outQuad" | "inOutQuad" | "inCubic" | "outCubic" | "inOutCubic" | "inQuart" | "outQuart" | "inOutQuart" | "inQuint" | "outQuint" | "inOutQuint" | "inSine" | "outSine" | "inOutSine" | "inExpo" | "outExpo" | "inOutExpo" | "inCirc" | "outCirc" | "inOutCirc" | "inBack" | "outBack" | "inOutBack" | "inElastic" | "outElastic" | "inOutElastic" | "inBounce" | "outBounce" | "inOutBounce";
2
+ export type EaseFunction = (t: number) => number;
3
+ export type AnimationStatus = "playing" | "paused" | "stopped" | "dead";
4
+ export type InterpolationStatus = "active" | "inactive" | "dead";
5
+ export type DynamicValue = number | (() => number);
6
+ export type Animation = {
7
+ play: () => Promise<Animation>;
8
+ pause: () => void;
9
+ resume: () => void;
10
+ stop: () => void;
11
+ skipToEnd: () => void;
12
+ kill: () => void;
13
+ setCurrentValue: (value: number) => void;
14
+ currentValue: number;
15
+ velocity: number;
16
+ progress: number;
17
+ setProgress: (value: number) => void;
18
+ status: AnimationStatus;
19
+ durationMs: number;
20
+ };
21
+ export type Interpolation = {
22
+ start: () => void;
23
+ stop: () => void;
24
+ kill: () => void;
25
+ setCurrentValue: (value: number) => void;
26
+ currentValue: number;
27
+ velocity: number;
28
+ status: InterpolationStatus;
29
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Creates a smooth clamp function that maps input toward a threshold
3
+ * without ever exceeding it. Uses `x / (1 + |x|)` for a natural-feeling
4
+ * curve — near-linear at low inputs, asymptotically approaches the
5
+ * threshold at high inputs.
6
+ *
7
+ * Compared to a hard clamp, this gives a gradual rolloff that feels
8
+ * more organic. Useful for capping velocity, torque, or force.
9
+ *
10
+ * @param threshold - The asymptotic maximum output (never exceeded).
11
+ * @returns A function that applies smooth clamping to an input value.
12
+ */
13
+ export declare const createSmoothClamp: (threshold: number) => ((input: number) => number);
@@ -0,0 +1,22 @@
1
+ //#region src/smooth-clamp/smooth-clamp.ts
2
+ /**
3
+ * Creates a smooth clamp function that maps input toward a threshold
4
+ * without ever exceeding it. Uses `x / (1 + |x|)` for a natural-feeling
5
+ * curve — near-linear at low inputs, asymptotically approaches the
6
+ * threshold at high inputs.
7
+ *
8
+ * Compared to a hard clamp, this gives a gradual rolloff that feels
9
+ * more organic. Useful for capping velocity, torque, or force.
10
+ *
11
+ * @param threshold - The asymptotic maximum output (never exceeded).
12
+ * @returns A function that applies smooth clamping to an input value.
13
+ */
14
+ var createSmoothClamp = (threshold) => {
15
+ return (input) => {
16
+ if (!isFinite(input)) return input >= 0 ? threshold : -threshold;
17
+ const normalized = input / threshold;
18
+ return threshold * (normalized / (1 + Math.abs(normalized)));
19
+ };
20
+ };
21
+ //#endregion
22
+ export { createSmoothClamp };
@@ -0,0 +1,10 @@
1
+ import { Interpolation, DynamicValue } from '../shared/types';
2
+ export type SmoothDampOptions = {
3
+ to: () => number;
4
+ smoothTimeMs: DynamicValue;
5
+ maxSpeed?: DynamicValue;
6
+ precision?: number;
7
+ onUpdate?: (value: number, velocity: number) => void;
8
+ onEnded?: () => void;
9
+ };
10
+ export declare const createSmoothDamp: (options: SmoothDampOptions) => Interpolation;
@@ -0,0 +1,63 @@
1
+ import { getTicker } from "../ticker/get-ticker.js";
2
+ import { smoothDampStep } from "./step.js";
3
+ //#region src/smooth-damp/create-smooth-damp.ts
4
+ var createSmoothDamp = (options) => {
5
+ const precision = options.precision ?? .01;
6
+ const onUpdate = options.onUpdate;
7
+ const onEnded = options.onEnded;
8
+ const state = {
9
+ current: 0,
10
+ velocity: 0
11
+ };
12
+ let active = true;
13
+ const ticker = getTicker();
14
+ const resolveValue = (v) => typeof v === "function" ? v() : v;
15
+ state.current = options.to();
16
+ ticker.add(update);
17
+ const start = () => {
18
+ if (active) return;
19
+ active = true;
20
+ ticker.add(update);
21
+ };
22
+ const stop = () => {
23
+ active = false;
24
+ ticker.remove(update);
25
+ };
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
+ return {
44
+ start,
45
+ stop,
46
+ kill,
47
+ setCurrentValue: (value) => {
48
+ state.current = value;
49
+ state.velocity = 0;
50
+ },
51
+ get currentValue() {
52
+ return state.current;
53
+ },
54
+ get velocity() {
55
+ return state.velocity;
56
+ },
57
+ get status() {
58
+ return active ? "active" : "inactive";
59
+ }
60
+ };
61
+ };
62
+ //#endregion
63
+ export { createSmoothDamp };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Unity-style Smooth Damp step function.
3
+ * Mutates `state` in place — zero allocation.
4
+ */
5
+ export type SmoothDampState = {
6
+ current: number;
7
+ velocity: number;
8
+ };
9
+ /**
10
+ * One frame of Smooth Damp using Unity's formula.
11
+ * Frame-rate independent, critically damped approach toward target.
12
+ */
13
+ export declare const smoothDampStep: (state: SmoothDampState, target: number, smoothTimeMs: number, maxSpeed: number, deltaMs: number) => void;
@@ -0,0 +1,26 @@
1
+ //#region src/smooth-damp/step.ts
2
+ /**
3
+ * One frame of Smooth Damp using Unity's formula.
4
+ * Frame-rate independent, critically damped approach toward target.
5
+ */
6
+ var smoothDampStep = (state, target, smoothTimeMs, maxSpeed, deltaMs) => {
7
+ const deltaTime = deltaMs / 1e3;
8
+ const st = Math.max(1e-4, smoothTimeMs / 1e3);
9
+ const omega = 2 / st;
10
+ const x = omega * deltaTime;
11
+ const exp = 1 / (1 + x + .48 * x * x + .235 * x * x * x);
12
+ const change = state.current - target;
13
+ const originalTo = target;
14
+ const maxChange = maxSpeed * st;
15
+ const clampedChange = Math.max(-maxChange, Math.min(change, maxChange));
16
+ target = state.current - clampedChange;
17
+ const temp = (state.velocity + omega * clampedChange) * deltaTime;
18
+ state.velocity = (state.velocity - omega * temp) * exp;
19
+ state.current = target + (clampedChange + temp) * exp;
20
+ if (originalTo - state.current > 0 === state.current > originalTo) {
21
+ state.current = originalTo;
22
+ state.velocity = (originalTo - state.current) / deltaTime;
23
+ }
24
+ };
25
+ //#endregion
26
+ export { smoothDampStep };
@@ -0,0 +1,11 @@
1
+ import { Interpolation, DynamicValue } from '../shared/types';
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;
@@ -0,0 +1,67 @@
1
+ import { getTicker } from "../ticker/get-ticker.js";
2
+ import { verletStep } from "./verlet.js";
3
+ //#region src/spring/create-spring.ts
4
+ var createSpring = (options) => {
5
+ const precision = options.precision ?? .01;
6
+ const onUpdate = options.onUpdate;
7
+ const onEnded = options.onEnded;
8
+ const resolveValue = (v) => typeof v === "function" ? v() : v;
9
+ const rawTo = options.to;
10
+ const rawStiffness = options.stiffness ?? 180;
11
+ const rawDamping = options.damping ?? 12;
12
+ const rawMass = options.mass ?? 1;
13
+ const state = {
14
+ current: rawTo(),
15
+ velocity: 0
16
+ };
17
+ let active = true;
18
+ const ticker = getTicker();
19
+ ticker.add(update);
20
+ const start = () => {
21
+ if (active) return;
22
+ active = true;
23
+ ticker.add(update);
24
+ };
25
+ const stop = () => {
26
+ active = false;
27
+ ticker.remove(update);
28
+ };
29
+ const kill = () => {
30
+ active = false;
31
+ ticker.remove(update);
32
+ };
33
+ function update(deltaMs) {
34
+ if (!active) return;
35
+ const target = rawTo();
36
+ const stiffness = resolveValue(rawStiffness);
37
+ const damping = resolveValue(rawDamping);
38
+ const mass = resolveValue(rawMass);
39
+ verletStep(state, target, stiffness, damping, mass, deltaMs);
40
+ onUpdate?.(state.current, state.velocity);
41
+ if (onEnded && Math.abs(state.current - target) < precision && Math.abs(state.velocity) < precision) {
42
+ state.current = target;
43
+ state.velocity = 0;
44
+ onEnded();
45
+ }
46
+ }
47
+ return {
48
+ start,
49
+ stop,
50
+ kill,
51
+ setCurrentValue: (value) => {
52
+ state.current = value;
53
+ state.velocity = 0;
54
+ },
55
+ get currentValue() {
56
+ return state.current;
57
+ },
58
+ get velocity() {
59
+ return state.velocity;
60
+ },
61
+ get status() {
62
+ return active ? "active" : "inactive";
63
+ }
64
+ };
65
+ };
66
+ //#endregion
67
+ export { createSpring };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Verlet integration step for spring physics.
3
+ * Mutates `state` in place — zero allocation.
4
+ */
5
+ export type SpringState = {
6
+ current: number;
7
+ velocity: number;
8
+ };
9
+ export declare const verletStep: (state: SpringState, target: number, stiffness: number, damping: number, mass: number, deltaMs: number) => void;
10
+ /**
11
+ * Check if the spring is near rest (within precision of target, velocity near 0).
12
+ */
13
+ export declare const isNearRest: (current: number, velocity: number, target: number, precision: number) => boolean;
@@ -0,0 +1,9 @@
1
+ //#region src/spring/verlet.ts
2
+ var verletStep = (state, target, stiffness, damping, mass, deltaMs) => {
3
+ const dt = deltaMs / 1e3;
4
+ const acceleration = (-stiffness * (state.current - target) + -damping * state.velocity) / mass;
5
+ state.velocity += acceleration * dt;
6
+ state.current += state.velocity * dt;
7
+ };
8
+ //#endregion
9
+ export { verletStep };
@@ -0,0 +1,5 @@
1
+ import { TickerControls } from './ticker';
2
+ /** Returns the default ticker singleton. Created lazily on first access. */
3
+ export declare const getTicker: () => TickerControls;
4
+ /** Creates a new independent ticker instance (for testing or multi-loop setups). */
5
+ export { createTicker } from './ticker';
@@ -0,0 +1,10 @@
1
+ import { createTicker } from "./ticker.js";
2
+ //#region src/ticker/get-ticker.ts
3
+ var singleton;
4
+ /** Returns the default ticker singleton. Created lazily on first access. */
5
+ var getTicker = () => {
6
+ if (!singleton) singleton = createTicker();
7
+ return singleton;
8
+ };
9
+ //#endregion
10
+ export { getTicker };
@@ -0,0 +1,22 @@
1
+ export type TickHandler = (deltaMs: number) => void;
2
+ export type TickerControls = {
3
+ start: () => void;
4
+ stop: () => void;
5
+ update: (deltaMs: number) => void;
6
+ add: (handler: TickHandler) => void;
7
+ remove: (handler: TickHandler) => void;
8
+ };
9
+ /**
10
+ * Create a ticker that drives active animations.
11
+ *
12
+ * Does NOT auto-start. The user must explicitly call either:
13
+ * - `start()` — begins a `requestAnimationFrame` loop
14
+ * - `update(deltaMs)` — drive manually from a game loop
15
+ *
16
+ * `add()` and `remove()` register/unregister animations without
17
+ * side effects on the rAF loop.
18
+ *
19
+ * Uses a flat array with undefined-tombstone removal for safe concurrent
20
+ * modification during iteration. Compacted after each frame.
21
+ */
22
+ export declare const createTicker: () => TickerControls;