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.
- package/LICENSE.txt +21 -0
- package/README.md +639 -0
- package/dist/animation/create-animation.d.ts +24 -0
- package/dist/animation/create-animation.js +336 -0
- package/dist/animation/update.d.ts +12 -0
- package/dist/animation/update.js +22 -0
- package/dist/benchmarks/easing.bench.d.ts +1 -0
- package/dist/benchmarks/vs-gsap.bench.d.ts +1 -0
- package/dist/color/lerp-oklab.d.ts +26 -0
- package/dist/color/lerp-oklab.js +182 -0
- package/dist/easing/easing.d.ts +16 -0
- package/dist/easing/easing.js +154 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/lerp/create-lerp.d.ts +9 -0
- package/dist/lerp/create-lerp.js +65 -0
- package/dist/lerp/step.d.ts +14 -0
- package/dist/lerp/step.js +14 -0
- package/dist/shared/internal.d.ts +4 -0
- package/dist/shared/types.d.ts +29 -0
- package/dist/smooth-clamp/smooth-clamp.d.ts +13 -0
- package/dist/smooth-clamp/smooth-clamp.js +22 -0
- package/dist/smooth-damp/create-smooth-damp.d.ts +10 -0
- package/dist/smooth-damp/create-smooth-damp.js +63 -0
- package/dist/smooth-damp/step.d.ts +13 -0
- package/dist/smooth-damp/step.js +26 -0
- package/dist/spring/create-spring.d.ts +11 -0
- package/dist/spring/create-spring.js +67 -0
- package/dist/spring/verlet.d.ts +13 -0
- package/dist/spring/verlet.js +9 -0
- package/dist/ticker/get-ticker.d.ts +5 -0
- package/dist/ticker/get-ticker.js +10 -0
- package/dist/ticker/ticker.d.ts +22 -0
- package/dist/ticker/ticker.js +74 -0
- package/dist/timeline/create-timeline.d.ts +25 -0
- package/dist/timeline/create-timeline.js +139 -0
- 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;
|