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,336 @@
1
+ import { easingFunctions } from "../easing/easing.js";
2
+ import { getTicker } from "../ticker/get-ticker.js";
3
+ import { updateTween } from "./update.js";
4
+ //#region src/animation/create-animation.ts
5
+ var resolveEasing = (ease) => typeof ease === "function" ? ease : easingFunctions[ease];
6
+ var isKeyframeMode = (options) => {
7
+ return "keyframes" in options && Array.isArray(options.keyframes);
8
+ };
9
+ var createAnimation = (options) => {
10
+ if (isKeyframeMode(options)) return createKeyframeAnimation(options);
11
+ return createSingleTween(options);
12
+ };
13
+ var createSingleTween = (options) => {
14
+ const easeName = options.ease ?? "inOutSine";
15
+ const delayMs = options.delayMs ?? 0;
16
+ const { onStarted, onUpdate, onEnded, durationMs } = options;
17
+ let rawFrom = options.from;
18
+ let rawTo = options.to;
19
+ const state = {
20
+ progress: 0,
21
+ currentValue: 0,
22
+ velocity: 0
23
+ };
24
+ let status = "stopped";
25
+ let stopped = false;
26
+ let resolvePromise;
27
+ let delayRemainingMs = 0;
28
+ let pendingStart = false;
29
+ const ticker = getTicker();
30
+ const update = (deltaMs) => {
31
+ if (stopped) return;
32
+ if (pendingStart) {
33
+ delayRemainingMs -= deltaMs;
34
+ if (delayRemainingMs > 0) return;
35
+ pendingStart = false;
36
+ onStarted?.();
37
+ deltaMs = -delayRemainingMs;
38
+ }
39
+ const from = typeof rawFrom === "function" ? rawFrom() : rawFrom;
40
+ const to = typeof rawTo === "function" ? rawTo() : rawTo;
41
+ const completed = updateTween(state, deltaMs, durationMs, currentEase, from, to);
42
+ onUpdate?.(state.currentValue, state.velocity);
43
+ if (completed) handleCompletion();
44
+ };
45
+ let currentEase = resolveEasing(easeName);
46
+ const play = () => {
47
+ if (status === "dead") throw new Error("Cannot play a dead animation");
48
+ stopped = false;
49
+ state.progress = 0;
50
+ const initFrom = typeof rawFrom === "function" ? rawFrom() : rawFrom;
51
+ state.currentValue = initFrom;
52
+ state.velocity = 0;
53
+ if (delayMs > 0) {
54
+ delayRemainingMs = delayMs;
55
+ pendingStart = true;
56
+ } else {
57
+ pendingStart = false;
58
+ delayRemainingMs = 0;
59
+ }
60
+ const promise = new Promise((resolve) => {
61
+ resolvePromise = resolve;
62
+ });
63
+ status = "playing";
64
+ ticker.add(update);
65
+ if (delayRemainingMs <= 0) onStarted?.();
66
+ return promise;
67
+ };
68
+ const pause = () => {
69
+ if (status !== "playing") return;
70
+ status = "paused";
71
+ ticker.remove(update);
72
+ };
73
+ const resume = () => {
74
+ if (status !== "paused") return;
75
+ status = "playing";
76
+ ticker.add(update);
77
+ };
78
+ const stop = () => {
79
+ stopped = true;
80
+ status = "stopped";
81
+ ticker.remove(update);
82
+ resolvePromise?.(controls);
83
+ resolvePromise = void 0;
84
+ };
85
+ const skipToEnd = () => {
86
+ const skipTo = typeof rawTo === "function" ? rawTo() : rawTo;
87
+ state.currentValue = skipTo;
88
+ state.velocity = 0;
89
+ state.progress = 1;
90
+ onUpdate?.(state.currentValue, state.velocity);
91
+ if (status === "playing" || status === "paused") onEnded?.();
92
+ status = "stopped";
93
+ ticker.remove(update);
94
+ resolvePromise?.(controls);
95
+ resolvePromise = void 0;
96
+ };
97
+ const kill = () => {
98
+ status = "dead";
99
+ ticker.remove(update);
100
+ stopped = true;
101
+ resolvePromise = void 0;
102
+ };
103
+ function handleCompletion() {
104
+ onEnded?.();
105
+ status = "stopped";
106
+ ticker.remove(update);
107
+ resolvePromise?.(controls);
108
+ resolvePromise = void 0;
109
+ }
110
+ const controls = {
111
+ play,
112
+ pause,
113
+ resume,
114
+ stop,
115
+ skipToEnd,
116
+ kill,
117
+ setCurrentValue: (value) => {
118
+ state.currentValue = value;
119
+ state.velocity = 0;
120
+ },
121
+ get currentValue() {
122
+ return state.currentValue;
123
+ },
124
+ get velocity() {
125
+ return state.velocity;
126
+ },
127
+ get progress() {
128
+ return state.progress;
129
+ },
130
+ setProgress(value) {
131
+ if (status === "playing") pause();
132
+ const clamped = Math.max(0, Math.min(1, value));
133
+ state.progress = clamped;
134
+ const from = typeof rawFrom === "function" ? rawFrom() : rawFrom;
135
+ const to = typeof rawTo === "function" ? rawTo() : rawTo;
136
+ state.currentValue = from + (to - from) * currentEase(clamped);
137
+ state.velocity = 0;
138
+ onUpdate?.(state.currentValue, state.velocity);
139
+ },
140
+ get status() {
141
+ return status;
142
+ },
143
+ get durationMs() {
144
+ return durationMs;
145
+ }
146
+ };
147
+ return controls;
148
+ };
149
+ var createKeyframeAnimation = (options) => {
150
+ const keyframes = options.keyframes;
151
+ const onUpdate = options.onUpdate;
152
+ const onProgress = options.onProgress;
153
+ const onEnded = options.onEnded;
154
+ const sorted = [...keyframes].sort((a, b) => a.at - b.at);
155
+ const totalDurationMs = sorted[sorted.length - 1].at;
156
+ const invTotalDuration = 1 / totalDurationMs;
157
+ const resolveKeyframeValue = (kf) => {
158
+ return typeof kf.value === "function" ? kf.value() : kf.value;
159
+ };
160
+ const segments = [];
161
+ for (let i = 0; i < sorted.length - 1; i++) {
162
+ const current = sorted[i];
163
+ const next = sorted[i + 1];
164
+ const from = resolveKeyframeValue(current);
165
+ const to = resolveKeyframeValue(next);
166
+ segments.push({
167
+ from,
168
+ to,
169
+ range: to - from,
170
+ durationMs: next.at - current.at,
171
+ easeFn: resolveEasing(next.ease ?? "inOutSine")
172
+ });
173
+ }
174
+ const prefixSum = [0];
175
+ for (let i = 0; i < segments.length; i++) prefixSum.push(prefixSum[i] + segments[i].durationMs);
176
+ if (segments.length === 0) return createSingleTween({
177
+ from: resolveKeyframeValue(sorted[0]),
178
+ to: resolveKeyframeValue(sorted[0]),
179
+ durationMs: totalDurationMs,
180
+ ease: "linear",
181
+ onUpdate,
182
+ onEnded
183
+ });
184
+ const state = {
185
+ progress: 0,
186
+ currentValue: 0,
187
+ velocity: 0
188
+ };
189
+ let status = "stopped";
190
+ let stopped = false;
191
+ let resolvePromise;
192
+ let currentSegmentIndex = 0;
193
+ let previousValue = resolveKeyframeValue(sorted[0]);
194
+ const ticker = getTicker();
195
+ const update = (deltaMs) => {
196
+ if (stopped) return;
197
+ const segment = segments[currentSegmentIndex];
198
+ segmentElapsed += deltaMs;
199
+ segmentProgress += deltaMs / segment.durationMs;
200
+ if (segmentProgress >= 1) segmentProgress = 1;
201
+ const eased = segment.easeFn(segmentProgress);
202
+ previousValue = state.currentValue;
203
+ state.currentValue = segment.from + segment.range * eased;
204
+ if (segmentProgress >= 1) {
205
+ state.currentValue = segment.to;
206
+ state.velocity = 0;
207
+ } else state.velocity = (state.currentValue - previousValue) / (deltaMs / 1e3);
208
+ const elapsedTotal = prefixSum[currentSegmentIndex] + segmentElapsed;
209
+ state.progress = Math.min(elapsedTotal * invTotalDuration, 1);
210
+ onProgress?.(state.progress);
211
+ onUpdate?.(state.currentValue, state.velocity);
212
+ if (segmentProgress >= 1) if (currentSegmentIndex < segments.length - 1) {
213
+ currentSegmentIndex++;
214
+ segmentElapsed = 0;
215
+ segmentProgress = 0;
216
+ state.currentValue = segments[currentSegmentIndex].from;
217
+ previousValue = state.currentValue;
218
+ state.velocity = 0;
219
+ state.progress = Math.min(prefixSum[currentSegmentIndex] * invTotalDuration, 1);
220
+ onProgress?.(state.progress);
221
+ } else {
222
+ status = "stopped";
223
+ ticker.remove(update);
224
+ onEnded?.();
225
+ resolvePromise?.(controls);
226
+ resolvePromise = void 0;
227
+ }
228
+ };
229
+ let segmentProgress = 0;
230
+ let segmentElapsed = 0;
231
+ const play = () => {
232
+ if (status === "dead") throw new Error("Cannot play a dead animation");
233
+ stopped = false;
234
+ currentSegmentIndex = 0;
235
+ segmentElapsed = 0;
236
+ segmentProgress = 0;
237
+ state.progress = 0;
238
+ state.currentValue = segments[0].from;
239
+ previousValue = segments[0].from;
240
+ state.velocity = 0;
241
+ const promise = new Promise((resolve) => {
242
+ resolvePromise = resolve;
243
+ });
244
+ status = "playing";
245
+ ticker.add(update);
246
+ return promise;
247
+ };
248
+ const pause = () => {
249
+ if (status !== "playing") return;
250
+ status = "paused";
251
+ ticker.remove(update);
252
+ };
253
+ const resume = () => {
254
+ if (status !== "paused") return;
255
+ status = "playing";
256
+ ticker.add(update);
257
+ };
258
+ const stop = () => {
259
+ stopped = true;
260
+ status = "stopped";
261
+ ticker.remove(update);
262
+ resolvePromise?.(controls);
263
+ resolvePromise = void 0;
264
+ };
265
+ const skipToEnd = () => {
266
+ const last = segments[segments.length - 1];
267
+ state.currentValue = last.to;
268
+ state.velocity = 0;
269
+ state.progress = 1;
270
+ onUpdate?.(state.currentValue, state.velocity);
271
+ if (status === "playing" || status === "paused") onEnded?.();
272
+ status = "stopped";
273
+ ticker.remove(update);
274
+ resolvePromise?.(controls);
275
+ resolvePromise = void 0;
276
+ };
277
+ const kill = () => {
278
+ status = "dead";
279
+ ticker.remove(update);
280
+ stopped = true;
281
+ resolvePromise = void 0;
282
+ };
283
+ const controls = {
284
+ play,
285
+ pause,
286
+ resume,
287
+ stop,
288
+ skipToEnd,
289
+ kill,
290
+ setCurrentValue: (value) => {
291
+ state.currentValue = value;
292
+ state.velocity = 0;
293
+ },
294
+ get currentValue() {
295
+ return state.currentValue;
296
+ },
297
+ get velocity() {
298
+ return state.velocity;
299
+ },
300
+ get progress() {
301
+ return state.progress;
302
+ },
303
+ setProgress(value) {
304
+ if (status === "playing") pause();
305
+ const clamped = Math.max(0, Math.min(1, value));
306
+ state.progress = clamped;
307
+ const elapsed = clamped * totalDurationMs;
308
+ let segIdx = 0;
309
+ for (let i = 0; i < segments.length; i++) {
310
+ if (elapsed <= prefixSum[i + 1]) {
311
+ segIdx = i;
312
+ break;
313
+ }
314
+ segIdx = i;
315
+ }
316
+ const segment = segments[segIdx];
317
+ const segStart = prefixSum[segIdx];
318
+ const segDuration = segment.durationMs;
319
+ const segProgress = segDuration > 0 ? (elapsed - segStart) / segDuration : 1;
320
+ const eased = segment.easeFn(Math.max(0, Math.min(1, segProgress)));
321
+ state.currentValue = segment.from + segment.range * eased;
322
+ state.velocity = 0;
323
+ onUpdate?.(state.currentValue, state.velocity);
324
+ onProgress?.(clamped);
325
+ },
326
+ get status() {
327
+ return status;
328
+ },
329
+ get durationMs() {
330
+ return totalDurationMs;
331
+ }
332
+ };
333
+ return controls;
334
+ };
335
+ //#endregion
336
+ export { createAnimation };
@@ -0,0 +1,12 @@
1
+ import { EaseFunction } from '../shared/types';
2
+ export type TweenState = {
3
+ progress: number;
4
+ currentValue: number;
5
+ velocity: number;
6
+ };
7
+ /**
8
+ * Advance a tween by one frame.
9
+ * Mutates `state` in place — zero allocation.
10
+ * Returns `true` if the tween has completed (progress >= 1).
11
+ */
12
+ export declare const updateTween: (state: TweenState, deltaMs: number, durationMs: number, easeFn: EaseFunction, from: number, to: number) => boolean;
@@ -0,0 +1,22 @@
1
+ //#region src/animation/update.ts
2
+ /**
3
+ * Advance a tween by one frame.
4
+ * Mutates `state` in place — zero allocation.
5
+ * Returns `true` if the tween has completed (progress >= 1).
6
+ */
7
+ var updateTween = (state, deltaMs, durationMs, easeFn, from, to) => {
8
+ const thisFrameProgress = deltaMs / durationMs;
9
+ state.progress = Math.min(state.progress + thisFrameProgress, 1);
10
+ const previousValue = state.currentValue;
11
+ if (state.progress >= 1) {
12
+ state.currentValue = to;
13
+ state.velocity = 0;
14
+ return true;
15
+ }
16
+ const eased = easeFn(state.progress);
17
+ state.currentValue = from + (to - from) * eased;
18
+ state.velocity = (state.currentValue - previousValue) / (deltaMs / 1e3);
19
+ return false;
20
+ };
21
+ //#endregion
22
+ export { updateTween };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
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];
@@ -0,0 +1,182 @@
1
+ //#region src/color/lerp-oklab.ts
2
+ /**
3
+ * Pre-computed constants for Oklab conversion.
4
+ *
5
+ * sRGB → linear → LMS → LMS' (cube root) → Oklab
6
+ */
7
+ var rgbToLmsMatrix = [
8
+ .4122214708,
9
+ .5363325363,
10
+ .0514459929,
11
+ .2119034982,
12
+ .6806995451,
13
+ .1073969566,
14
+ .0883024619,
15
+ .2817188376,
16
+ .6299787005
17
+ ];
18
+ var lmsPrimeToLabMatrix = [
19
+ .2104542553,
20
+ .793617785,
21
+ -.0040720468,
22
+ 1.9779984951,
23
+ -2.428592205,
24
+ .4505937099,
25
+ .0259040371,
26
+ .7827717662,
27
+ -.808675766
28
+ ];
29
+ var labToLmsPrimeMatrix = [
30
+ 1,
31
+ .3963377922,
32
+ .2158037583,
33
+ 1,
34
+ -.1055613423,
35
+ -.0638541728,
36
+ 1,
37
+ -.0894841825,
38
+ -1.2914855377
39
+ ];
40
+ var lmsToLinearRgbMatrix = [
41
+ 4.0767416621,
42
+ -3.3077115913,
43
+ .2309699292,
44
+ -1.2684380046,
45
+ 2.6097574011,
46
+ -.3413193965,
47
+ -.0041960863,
48
+ -.7034186147,
49
+ 1.707614701
50
+ ];
51
+ /**
52
+ * Convert a normalized sRGB channel to linear.
53
+ */
54
+ var srgbToLinear = (channel) => {
55
+ if (channel <= .04045) return channel / 12.92;
56
+ return ((channel + .055) / 1.055) ** 2.4;
57
+ };
58
+ /**
59
+ * Convert a linear channel to sRGB.
60
+ */
61
+ var linearToSrgb = (channel) => {
62
+ if (channel <= .0031308) return channel * 12.92;
63
+ return 1.055 * channel ** (1 / 2.4) - .055;
64
+ };
65
+ /**
66
+ * Convert an RGB tuple to Oklab [L, a, b].
67
+ * All values in [0, 1] range (except a/b which can be negative).
68
+ */
69
+ var rgbToOklab = (r, g, b) => {
70
+ const rLin = srgbToLinear(r);
71
+ const gLin = srgbToLinear(g);
72
+ const bLin = srgbToLinear(b);
73
+ const lLms = rgbToLmsMatrix[0] * rLin + rgbToLmsMatrix[1] * gLin + rgbToLmsMatrix[2] * bLin;
74
+ const mLms = rgbToLmsMatrix[3] * rLin + rgbToLmsMatrix[4] * gLin + rgbToLmsMatrix[5] * bLin;
75
+ const sLms = rgbToLmsMatrix[6] * rLin + rgbToLmsMatrix[7] * gLin + rgbToLmsMatrix[8] * bLin;
76
+ const lPrime = Math.cbrt(lLms);
77
+ const mPrime = Math.cbrt(mLms);
78
+ const sPrime = Math.cbrt(sLms);
79
+ return [
80
+ lmsPrimeToLabMatrix[0] * lPrime + lmsPrimeToLabMatrix[1] * mPrime + lmsPrimeToLabMatrix[2] * sPrime,
81
+ lmsPrimeToLabMatrix[3] * lPrime + lmsPrimeToLabMatrix[4] * mPrime + lmsPrimeToLabMatrix[5] * sPrime,
82
+ lmsPrimeToLabMatrix[6] * lPrime + lmsPrimeToLabMatrix[7] * mPrime + lmsPrimeToLabMatrix[8] * sPrime
83
+ ];
84
+ };
85
+ /**
86
+ * Convert an Oklab [L, a, b] tuple back to sRGB [R, G, B].
87
+ * All values clamped to [0, 1].
88
+ */
89
+ var oklabToRgb = (L, aL, bL) => {
90
+ const lPrime = labToLmsPrimeMatrix[0] * L + labToLmsPrimeMatrix[1] * aL + labToLmsPrimeMatrix[2] * bL;
91
+ const mPrime = labToLmsPrimeMatrix[3] * L + labToLmsPrimeMatrix[4] * aL + labToLmsPrimeMatrix[5] * bL;
92
+ const sPrime = labToLmsPrimeMatrix[6] * L + labToLmsPrimeMatrix[7] * aL + labToLmsPrimeMatrix[8] * bL;
93
+ const lLms = lPrime * lPrime * lPrime;
94
+ const mLms = mPrime * mPrime * mPrime;
95
+ const sLms = sPrime * sPrime * sPrime;
96
+ const rLin = lmsToLinearRgbMatrix[0] * lLms + lmsToLinearRgbMatrix[1] * mLms + lmsToLinearRgbMatrix[2] * sLms;
97
+ const gLin = lmsToLinearRgbMatrix[3] * lLms + lmsToLinearRgbMatrix[4] * mLms + lmsToLinearRgbMatrix[5] * sLms;
98
+ const bLin = lmsToLinearRgbMatrix[6] * lLms + lmsToLinearRgbMatrix[7] * mLms + lmsToLinearRgbMatrix[8] * sLms;
99
+ return [
100
+ clamp(linearToSrgb(rLin)),
101
+ clamp(linearToSrgb(gLin)),
102
+ clamp(linearToSrgb(bLin))
103
+ ];
104
+ };
105
+ /**
106
+ * Clamp a value to [0, 1].
107
+ */
108
+ var clamp = (value) => {
109
+ if (value < 0) return 0;
110
+ if (value > 1) return 1;
111
+ return value;
112
+ };
113
+ /**
114
+ * Interpolate between two RGBA colors in Oklab space.
115
+ *
116
+ * The RGB channels are converted to Oklab, lerped perceptually uniformly,
117
+ * and converted back. The alpha channel is lerped linearly.
118
+ *
119
+ * @param from - Starting RGBA tuple [R, G, B, A], each 0–1.
120
+ * @param to - Ending RGBA tuple [R, G, B, A], each 0–1.
121
+ * @param progress - Interpolation factor (0 = from, 1 = to).
122
+ * @returns A new RGBA tuple [R, G, B, A], each 0–1.
123
+ */
124
+ var lerpOklab = (from, to, progress) => {
125
+ const clampedProgress = clamp(progress);
126
+ const [lFrom, aFrom, bFrom] = rgbToOklab(from[0], from[1], from[2]);
127
+ const [lTo, aTo, bTo] = rgbToOklab(to[0], to[1], to[2]);
128
+ const [r, g, b] = oklabToRgb(lFrom + (lTo - lFrom) * clampedProgress, aFrom + (aTo - aFrom) * clampedProgress, bFrom + (bTo - bFrom) * clampedProgress);
129
+ return [
130
+ r,
131
+ g,
132
+ b,
133
+ clamp(from[3] + (to[3] - from[3]) * clampedProgress)
134
+ ];
135
+ };
136
+ /**
137
+ * Parse a hex color string into an RGBA tuple.
138
+ *
139
+ * Accepts formats:
140
+ * - `#RGB`
141
+ * - `#RGBA`
142
+ * - `#RRGGBB`
143
+ * - `#RRGGBBAA`
144
+ *
145
+ * @param hex - Hex color string with optional leading `#`.
146
+ * @returns An RGBA tuple [R, G, B, A], each 0–1.
147
+ */
148
+ var hexToRgba = (hex) => {
149
+ const cleaned = hex.startsWith("#") ? hex.slice(1) : hex;
150
+ if (!/^[0-9a-fA-F]+$/.test(cleaned)) return [
151
+ 0,
152
+ 0,
153
+ 0,
154
+ 1
155
+ ];
156
+ if (cleaned.length === 3) return [
157
+ parseInt(cleaned[0] + cleaned[0], 16) / 255,
158
+ parseInt(cleaned[1] + cleaned[1], 16) / 255,
159
+ parseInt(cleaned[2] + cleaned[2], 16) / 255,
160
+ 1
161
+ ];
162
+ if (cleaned.length === 4) return [
163
+ parseInt(cleaned[0] + cleaned[0], 16) / 255,
164
+ parseInt(cleaned[1] + cleaned[1], 16) / 255,
165
+ parseInt(cleaned[2] + cleaned[2], 16) / 255,
166
+ parseInt(cleaned[3] + cleaned[3], 16) / 255
167
+ ];
168
+ if (cleaned.length >= 6) return [
169
+ parseInt(cleaned.slice(0, 2), 16) / 255,
170
+ parseInt(cleaned.slice(2, 4), 16) / 255,
171
+ parseInt(cleaned.slice(4, 6), 16) / 255,
172
+ cleaned.length >= 8 ? parseInt(cleaned.slice(6, 8), 16) / 255 : 1
173
+ ];
174
+ return [
175
+ 0,
176
+ 0,
177
+ 0,
178
+ 1
179
+ ];
180
+ };
181
+ //#endregion
182
+ export { hexToRgba, lerpOklab };
@@ -0,0 +1,16 @@
1
+ import { EaseName, EaseFunction } from '../shared/types';
2
+ /** All 31 named easing identifiers, ordered by type. */
3
+ export declare const EASE_NAMES: EaseName[];
4
+ export declare const easingFunctions: Record<EaseName, EaseFunction>;
5
+ /**
6
+ * Cubic bezier easing. Builds a pre-computed lookup table at construction time
7
+ * for O(log n) binary search at runtime — no Newton iteration, no allocation
8
+ * per frame.
9
+ *
10
+ * @param p1x - First control point x coordinate.
11
+ * @param p1y - First control point y coordinate.
12
+ * @param p2x - Second control point x coordinate.
13
+ * @param p2y - Second control point y coordinate.
14
+ * @returns An EaseFunction suitable for use with animate() or createTimeline().
15
+ */
16
+ export declare const cubicBezier: (p1x: number, p1y: number, p2x: number, p2y: number) => EaseFunction;