@vune-ui/animation 0.1.20

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 (68) hide show
  1. package/ARCHITECTURE.md +470 -0
  2. package/CHANGELOG.md +88 -0
  3. package/LICENSE +21 -0
  4. package/PERFORMANCE.md +151 -0
  5. package/README.md +630 -0
  6. package/dist/index.d.ts +474 -0
  7. package/dist/src/canvas/index.d.ts +15 -0
  8. package/dist/src/canvas/index.js +67 -0
  9. package/dist/src/constraints/index.d.ts +33 -0
  10. package/dist/src/constraints/index.js +346 -0
  11. package/dist/src/core/bezier.js +51 -0
  12. package/dist/src/core/composition.js +17 -0
  13. package/dist/src/core/controls.js +22 -0
  14. package/dist/src/core/default-engine.js +20 -0
  15. package/dist/src/core/easing.js +58 -0
  16. package/dist/src/core/engine.js +1031 -0
  17. package/dist/src/core/frame-budget.js +30 -0
  18. package/dist/src/core/index.d.ts +43 -0
  19. package/dist/src/core/index.js +17 -0
  20. package/dist/src/core/js-spring-batch.js +57 -0
  21. package/dist/src/core/kinetics.js +140 -0
  22. package/dist/src/core/math.js +20 -0
  23. package/dist/src/core/motion-value.js +53 -0
  24. package/dist/src/core/planner.js +72 -0
  25. package/dist/src/core/specs.js +70 -0
  26. package/dist/src/dom/index.d.ts +41 -0
  27. package/dist/src/dom/index.js +364 -0
  28. package/dist/src/gesture/index.d.ts +66 -0
  29. package/dist/src/gesture/index.js +376 -0
  30. package/dist/src/index.js +53 -0
  31. package/dist/src/interpolate/color.js +223 -0
  32. package/dist/src/interpolate/css.d.ts +13 -0
  33. package/dist/src/interpolate/css.js +34 -0
  34. package/dist/src/interpolate/index.d.ts +13 -0
  35. package/dist/src/interpolate/index.js +55 -0
  36. package/dist/src/interpolate/transform.js +247 -0
  37. package/dist/src/layout/index.d.ts +56 -0
  38. package/dist/src/layout/index.js +485 -0
  39. package/dist/src/material/index.d.ts +9 -0
  40. package/dist/src/material/index.js +70 -0
  41. package/dist/src/path/index.d.ts +37 -0
  42. package/dist/src/path/index.js +527 -0
  43. package/dist/src/render/frame-batcher.js +52 -0
  44. package/dist/src/scroll/index.d.ts +55 -0
  45. package/dist/src/scroll/index.js +233 -0
  46. package/dist/src/timeline/index.d.ts +147 -0
  47. package/dist/src/timeline/index.js +849 -0
  48. package/dist/src/transition/index.d.ts +88 -0
  49. package/dist/src/transition/index.js +369 -0
  50. package/dist/src/wasm/index.d.ts +29 -0
  51. package/dist/src/wasm/index.js +8 -0
  52. package/dist/src/wasm/loader.js +55 -0
  53. package/dist/src/wasm/shared-wasm-spring-batch.js +52 -0
  54. package/dist/src/wasm/wasm-spring-batch.js +52 -0
  55. package/dist/src/webgl/index.d.ts +22 -0
  56. package/dist/src/webgl/index.js +94 -0
  57. package/dist/src/webgpu/index.d.ts +35 -0
  58. package/dist/src/webgpu/index.js +73 -0
  59. package/dist/src/webgpu/spring-batch.js +218 -0
  60. package/dist/src/worker/index.d.ts +17 -0
  61. package/dist/src/worker/index.js +1 -0
  62. package/dist/src/worker/shared-spring-worker.js +218 -0
  63. package/dist/src/worker/shared-worker.js +75 -0
  64. package/dist/wasm/kernel-scalar.wasm +0 -0
  65. package/dist/wasm/kernel-shared-scalar.wasm +0 -0
  66. package/dist/wasm/kernel-shared-simd.wasm +0 -0
  67. package/dist/wasm/kernel-simd.wasm +0 -0
  68. package/package.json +113 -0
@@ -0,0 +1,346 @@
1
+ const OP_AFFINE = 1;
2
+ const OP_CLAMP = 2;
3
+ const OP_SUM = 3;
4
+ const OP_MIX = 4;
5
+ const OP_CUSTOM = 5;
6
+
7
+ function finite(value, fallback = 0) {
8
+ return Number.isFinite(value) ? Number(value) : fallback;
9
+ }
10
+
11
+ function isMotionValue(value) {
12
+ return value && typeof value.get === 'function' && (typeof value._commit === 'function' || typeof value.set === 'function');
13
+ }
14
+
15
+ function nodeIndex(graph, node) {
16
+ if (node instanceof ConstraintNode) {
17
+ if (node.graph !== graph) throw new TypeError('Constraint node belongs to another graph.');
18
+ return node.index;
19
+ }
20
+ if (typeof node === 'number') return graph.constant(node).index;
21
+ throw new TypeError('Expected a ConstraintNode or finite constant.');
22
+ }
23
+
24
+ export class ConstraintNode {
25
+ constructor(graph, index, name = '') {
26
+ this.graph = graph;
27
+ this.index = index;
28
+ this.name = name;
29
+ }
30
+
31
+ get() { return this.graph.values[this.index]; }
32
+ getVelocity() { return this.graph.velocities[this.index]; }
33
+ set(value, velocity = 0) { this.graph.set(this, value, velocity); return this; }
34
+ }
35
+
36
+ export class ConstraintGraph {
37
+ constructor({ engine = null } = {}) {
38
+ this.values = [];
39
+ this.velocities = [];
40
+ this.bindings = [];
41
+ this.nodes = [];
42
+ this.operations = [];
43
+ this.writerByNode = new Map();
44
+ this.compiled = null;
45
+ this.dirty = true;
46
+ this.evaluating = false;
47
+ this.engine = null;
48
+ this.registered = false;
49
+ this.unsubscribers = [];
50
+ this.disposed = false;
51
+ if (engine) this.attach(engine);
52
+ }
53
+
54
+ node(value = 0, { name = '' } = {}) {
55
+ if (this.disposed) throw new Error('ConstraintGraph is disposed.');
56
+ if (this.compiled) throw new Error('ConstraintGraph topology is locked after compile/evaluate. Build all nodes first.');
57
+ const binding = isMotionValue(value) ? value : null;
58
+ const initial = binding ? finite(binding.get()) : finite(value);
59
+ const velocity = binding && typeof binding.getVelocity === 'function' ? finite(binding.getVelocity()) : 0;
60
+ const index = this.values.length;
61
+ this.values.push(initial);
62
+ this.velocities.push(velocity);
63
+ this.bindings.push(binding);
64
+ const node = new ConstraintNode(this, index, name);
65
+ this.nodes.push(node);
66
+ if (binding) this.#subscribeBinding(binding);
67
+ this.#invalidateCompile();
68
+ return node;
69
+ }
70
+
71
+ constant(value, options) {
72
+ if (!Number.isFinite(value)) throw new TypeError('Constraint constant must be finite.');
73
+ return this.node(Number(value), options);
74
+ }
75
+
76
+ #subscribeBinding(binding) {
77
+ const subscribe = binding.subscribeValue ?? binding.subscribe;
78
+ if (typeof subscribe !== 'function') return;
79
+ const unsubscribe = subscribe.call(binding, () => {
80
+ if (!this.evaluating) this.invalidate();
81
+ }, { emitCurrent: false });
82
+ this.unsubscribers.push(unsubscribe);
83
+ }
84
+
85
+ #invalidateCompile() {
86
+ this.compiled = null;
87
+ this.dirty = true;
88
+ this.invalidate();
89
+ }
90
+
91
+ #addOperation(type, target, sources, params = [], custom = null) {
92
+ if (this.compiled) throw new Error('ConstraintGraph topology is locked after compile/evaluate. Build all constraints first.');
93
+ const targetIndex = nodeIndex(this, target);
94
+ if (this.writerByNode.has(targetIndex)) {
95
+ throw new Error(`Constraint node '${this.nodes[targetIndex]?.name || targetIndex}' already has a writer.`);
96
+ }
97
+ const sourceIndices = sources.map((source) => nodeIndex(this, source));
98
+ const operation = { type, target: targetIndex, sources: sourceIndices, params, custom };
99
+ const opIndex = this.operations.length;
100
+ this.operations.push(operation);
101
+ this.writerByNode.set(targetIndex, opIndex);
102
+ this.#invalidateCompile();
103
+ return this;
104
+ }
105
+
106
+ affine(target, source, { scale = 1, offset = 0 } = {}) {
107
+ return this.#addOperation(OP_AFFINE, target, [source], [finite(scale, 1), finite(offset)]);
108
+ }
109
+
110
+ follow(target, source, options) { return this.affine(target, source, options); }
111
+
112
+ clamp(target, source, { min = -Infinity, max = Infinity } = {}) {
113
+ if (min > max) throw new RangeError('Constraint clamp min cannot exceed max.');
114
+ return this.#addOperation(OP_CLAMP, target, [source], [Number(min), Number(max)]);
115
+ }
116
+
117
+ sum(target, a, b, { scaleA = 1, scaleB = 1, offset = 0 } = {}) {
118
+ return this.#addOperation(OP_SUM, target, [a, b], [finite(scaleA, 1), finite(scaleB, 1), finite(offset)]);
119
+ }
120
+
121
+ mix(target, a, b, progress) {
122
+ return this.#addOperation(OP_MIX, target, [a, b, progress]);
123
+ }
124
+
125
+ map(target, inputs, compute) {
126
+ if (!Array.isArray(inputs) || inputs.length === 0) throw new TypeError('Constraint map() requires at least one input.');
127
+ if (typeof compute !== 'function') throw new TypeError('Constraint map() requires a compute callback.');
128
+ return this.#addOperation(OP_CUSTOM, target, inputs, [], compute);
129
+ }
130
+
131
+ compile() {
132
+ const count = this.operations.length;
133
+ const indegree = new Int32Array(count);
134
+ const outgoing = Array.from({ length: count }, () => []);
135
+ for (let opIndex = 0; opIndex < count; opIndex += 1) {
136
+ const operation = this.operations[opIndex];
137
+ for (const source of operation.sources) {
138
+ const writer = this.writerByNode.get(source);
139
+ if (writer == null) continue;
140
+ indegree[opIndex] += 1;
141
+ outgoing[writer].push(opIndex);
142
+ }
143
+ }
144
+
145
+ const queue = new Int32Array(Math.max(1, count));
146
+ let head = 0;
147
+ let tail = 0;
148
+ for (let i = 0; i < count; i += 1) if (indegree[i] === 0) queue[tail++] = i;
149
+ const order = new Int32Array(count);
150
+ let ordered = 0;
151
+ while (head < tail) {
152
+ const opIndex = queue[head++];
153
+ order[ordered++] = opIndex;
154
+ for (const dependent of outgoing[opIndex]) {
155
+ indegree[dependent] -= 1;
156
+ if (indegree[dependent] === 0) queue[tail++] = dependent;
157
+ }
158
+ }
159
+ if (ordered !== count) throw new Error('Constraint graph contains a dependency cycle.');
160
+
161
+ const types = new Uint8Array(count);
162
+ const targets = new Int32Array(count);
163
+ const sourceA = new Int32Array(count); sourceA.fill(-1);
164
+ const sourceB = new Int32Array(count); sourceB.fill(-1);
165
+ const sourceC = new Int32Array(count); sourceC.fill(-1);
166
+ const p0 = new Float64Array(count);
167
+ const p1 = new Float64Array(count);
168
+ const p2 = new Float64Array(count);
169
+ const customs = new Array(count).fill(null);
170
+
171
+ for (let slot = 0; slot < count; slot += 1) {
172
+ const operation = this.operations[order[slot]];
173
+ types[slot] = operation.type;
174
+ targets[slot] = operation.target;
175
+ sourceA[slot] = operation.sources[0] ?? -1;
176
+ sourceB[slot] = operation.sources[1] ?? -1;
177
+ sourceC[slot] = operation.sources[2] ?? -1;
178
+ p0[slot] = operation.params[0] ?? 0;
179
+ p1[slot] = operation.params[1] ?? 0;
180
+ p2[slot] = operation.params[2] ?? 0;
181
+ if (operation.type === OP_CUSTOM) {
182
+ customs[slot] = {
183
+ compute: operation.custom,
184
+ sources: Int32Array.from(operation.sources),
185
+ values: new Float64Array(operation.sources.length),
186
+ velocities: new Float64Array(operation.sources.length),
187
+ };
188
+ }
189
+ }
190
+
191
+ this.values = Float64Array.from(this.values);
192
+ this.velocities = Float64Array.from(this.velocities);
193
+ const boundIndices = Int32Array.from(this.bindings.flatMap((binding, index) => binding ? [index] : []));
194
+ const outputTargets = Int32Array.from(this.writerByNode.keys());
195
+ this.compiled = { types, targets, sourceA, sourceB, sourceC, p0, p1, p2, customs, count, boundIndices, outputTargets };
196
+ return this;
197
+ }
198
+
199
+ set(node, value, velocity = 0) {
200
+ const index = nodeIndex(this, node);
201
+ const next = finite(value, this.values[index]);
202
+ const nextVelocity = finite(velocity);
203
+ this.values[index] = next;
204
+ this.velocities[index] = nextVelocity;
205
+ const binding = this.bindings[index];
206
+ if (binding) {
207
+ this.evaluating = true;
208
+ try {
209
+ if (typeof binding._commit === 'function') binding._commit(next, nextVelocity);
210
+ else binding.set(next, nextVelocity);
211
+ } finally { this.evaluating = false; }
212
+ }
213
+ this.invalidate();
214
+ return this;
215
+ }
216
+
217
+ invalidate() {
218
+ if (this.disposed) return;
219
+ this.dirty = true;
220
+ if (this.engine && !this.registered) {
221
+ this.registered = true;
222
+ this.engine.addDriver(this);
223
+ }
224
+ }
225
+
226
+ evaluate() {
227
+ if (this.disposed) return false;
228
+ if (!this.compiled) this.compile();
229
+ const values = this.values;
230
+ const velocities = this.velocities;
231
+
232
+ const c = this.compiled;
233
+ for (let k = 0; k < c.boundIndices.length; k += 1) {
234
+ const i = c.boundIndices[k];
235
+ const binding = this.bindings[i];
236
+ values[i] = finite(binding.get(), values[i]);
237
+ velocities[i] = typeof binding.getVelocity === 'function' ? finite(binding.getVelocity()) : velocities[i];
238
+ }
239
+
240
+ this.evaluating = true;
241
+ try {
242
+ for (let i = 0; i < c.count; i += 1) {
243
+ const target = c.targets[i];
244
+ const aIndex = c.sourceA[i];
245
+ const bIndex = c.sourceB[i];
246
+ const cIndex = c.sourceC[i];
247
+ const a = aIndex >= 0 ? values[aIndex] : 0;
248
+ const va = aIndex >= 0 ? velocities[aIndex] : 0;
249
+ switch (c.types[i]) {
250
+ case OP_AFFINE: {
251
+ values[target] = a * c.p0[i] + c.p1[i];
252
+ velocities[target] = va * c.p0[i];
253
+ break;
254
+ }
255
+ case OP_CLAMP: {
256
+ const min = c.p0[i];
257
+ const max = c.p1[i];
258
+ const next = Math.min(max, Math.max(min, a));
259
+ values[target] = next;
260
+ velocities[target] = next === a ? va : 0;
261
+ break;
262
+ }
263
+ case OP_SUM: {
264
+ const b = values[bIndex];
265
+ values[target] = a * c.p0[i] + b * c.p1[i] + c.p2[i];
266
+ velocities[target] = va * c.p0[i] + velocities[bIndex] * c.p1[i];
267
+ break;
268
+ }
269
+ case OP_MIX: {
270
+ const b = values[bIndex];
271
+ const t = values[cIndex];
272
+ const vb = velocities[bIndex];
273
+ const vt = velocities[cIndex];
274
+ values[target] = a + (b - a) * t;
275
+ velocities[target] = va * (1 - t) + vb * t + (b - a) * vt;
276
+ break;
277
+ }
278
+ case OP_CUSTOM: {
279
+ const custom = c.customs[i];
280
+ for (let j = 0; j < custom.sources.length; j += 1) {
281
+ const source = custom.sources[j];
282
+ custom.values[j] = values[source];
283
+ custom.velocities[j] = velocities[source];
284
+ }
285
+ const result = custom.compute(custom.values, custom.velocities, this);
286
+ if (result && typeof result === 'object') {
287
+ values[target] = finite(result.value, values[target]);
288
+ velocities[target] = finite(result.velocity);
289
+ } else {
290
+ values[target] = finite(result, values[target]);
291
+ velocities[target] = 0;
292
+ }
293
+ break;
294
+ }
295
+ default: throw new Error(`Unknown constraint operation ${c.types[i]}.`);
296
+ }
297
+ }
298
+
299
+ for (let k = 0; k < c.outputTargets.length; k += 1) {
300
+ const target = c.outputTargets[k];
301
+ const binding = this.bindings[target];
302
+ if (!binding) continue;
303
+ const next = values[target];
304
+ const velocity = velocities[target];
305
+ if (typeof binding._commit === 'function') binding._commit(next, velocity);
306
+ else binding.set(next, velocity);
307
+ }
308
+ } finally {
309
+ this.evaluating = false;
310
+ this.dirty = false;
311
+ }
312
+ return true;
313
+ }
314
+
315
+ step() {
316
+ this.registered = false;
317
+ if (this.dirty) this.evaluate();
318
+ return false;
319
+ }
320
+
321
+ attach(engine) {
322
+ if (!engine?.addDriver) throw new TypeError('ConstraintGraph.attach() requires a MotionEngine-like engine.');
323
+ this.engine = engine;
324
+ this.invalidate();
325
+ return this;
326
+ }
327
+
328
+ detach() {
329
+ if (this.engine && this.registered) this.engine.removeDriver(this);
330
+ this.registered = false;
331
+ this.engine = null;
332
+ return this;
333
+ }
334
+
335
+ dispose() {
336
+ if (this.disposed) return;
337
+ this.detach();
338
+ for (const unsubscribe of this.unsubscribers) unsubscribe?.();
339
+ this.unsubscribers.length = 0;
340
+ this.disposed = true;
341
+ }
342
+ }
343
+
344
+ export function createConstraintGraph(options) {
345
+ return new ConstraintGraph(options);
346
+ }
@@ -0,0 +1,51 @@
1
+ import { clamp } from './math.js';
2
+
3
+ function sampleCurve(a1, a2, t) {
4
+ const inv = 1 - t;
5
+ return 3 * inv * inv * t * a1 + 3 * inv * t * t * a2 + t * t * t;
6
+ }
7
+
8
+ function sampleDerivative(a1, a2, t) {
9
+ return 3 * (1 - t) * (1 - t) * a1
10
+ + 6 * (1 - t) * t * (a2 - a1)
11
+ + 3 * t * t * (1 - a2);
12
+ }
13
+
14
+ function solveCurveT(curve, progress) {
15
+ const x = clamp(progress, 0, 1);
16
+ if (curve.x1 === curve.y1 && curve.x2 === curve.y2) return { x, t: x, linear: true };
17
+
18
+ let t = x;
19
+ for (let i = 0; i < 5; i += 1) {
20
+ const estimate = sampleCurve(curve.x1, curve.x2, t) - x;
21
+ const derivative = sampleDerivative(curve.x1, curve.x2, t);
22
+ if (Math.abs(derivative) < 1e-7) break;
23
+ t = clamp(t - estimate / derivative, 0, 1);
24
+ }
25
+
26
+ let low = 0;
27
+ let high = 1;
28
+ for (let i = 0; i < 8; i += 1) {
29
+ const estimate = sampleCurve(curve.x1, curve.x2, t);
30
+ if (Math.abs(estimate - x) < 1e-6) break;
31
+ if (estimate < x) low = t;
32
+ else high = t;
33
+ t = (low + high) * 0.5;
34
+ }
35
+
36
+ return { x, t, linear: false };
37
+ }
38
+
39
+ export function evaluateBezier(curve, progress) {
40
+ const solved = solveCurveT(curve, progress);
41
+ if (solved.linear) return solved.x;
42
+ return sampleCurve(curve.y1, curve.y2, solved.t);
43
+ }
44
+
45
+ export function evaluateBezierDerivative(curve, progress) {
46
+ const solved = solveCurveT(curve, progress);
47
+ if (solved.linear) return 1;
48
+ const dx = sampleDerivative(curve.x1, curve.x2, solved.t);
49
+ if (Math.abs(dx) < 1e-7) return 0;
50
+ return sampleDerivative(curve.y1, curve.y2, solved.t) / dx;
51
+ }
@@ -0,0 +1,17 @@
1
+ export const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
2
+
3
+ export async function parallel(...factories) {
4
+ return Promise.all(factories.map((factory) => {
5
+ const result = typeof factory === 'function' ? factory() : factory;
6
+ return result?.finished ?? result;
7
+ }));
8
+ }
9
+
10
+ export async function sequence(...factories) {
11
+ const results = [];
12
+ for (const factory of factories) {
13
+ const result = typeof factory === 'function' ? factory() : factory;
14
+ results.push(await (result?.finished ?? result));
15
+ }
16
+ return results;
17
+ }
@@ -0,0 +1,22 @@
1
+ export class AnimationControls {
2
+ constructor(cancel, finish, promise) {
3
+ this.cancel = cancel;
4
+ this.finish = finish;
5
+ this.finished = promise;
6
+ }
7
+ }
8
+
9
+ export function deferredControls() {
10
+ let resolver;
11
+ let settled = false;
12
+ const finished = new Promise((resolve) => { resolver = resolve; });
13
+ return {
14
+ finished,
15
+ settle(result) {
16
+ if (settled) return;
17
+ settled = true;
18
+ resolver(result);
19
+ },
20
+ get settled() { return settled; },
21
+ };
22
+ }
@@ -0,0 +1,20 @@
1
+ import { MotionEngine } from './engine.js';
2
+ import { decay as decaySpec, inertia as inertiaSpec } from './kinetics.js';
3
+
4
+ export const defaultEngine = new MotionEngine();
5
+
6
+ export function animate(value, to, spec) {
7
+ return defaultEngine.animate(value, to, spec);
8
+ }
9
+
10
+ export function animateVelocity(value, spec) {
11
+ return defaultEngine.animateVelocity(value, spec);
12
+ }
13
+
14
+ export function animateDecay(value, options = {}) {
15
+ return defaultEngine.animateVelocity(value, decaySpec(options));
16
+ }
17
+
18
+ export function animateInertia(value, options = {}) {
19
+ return defaultEngine.animateVelocity(value, inertiaSpec(options));
20
+ }
@@ -0,0 +1,58 @@
1
+ import { evaluateBezier } from './bezier.js';
2
+
3
+ const EASING_LUT_SIZE = 256;
4
+ const easingLutCache = new WeakMap();
5
+ const linearCompiledEasing = Object.freeze({ kind: 'linear' });
6
+
7
+ function clamp01(value) {
8
+ return Math.min(1, Math.max(0, value));
9
+ }
10
+
11
+ /**
12
+ * Compile a timing curve once into a compact lookup plan. Bezier objects are
13
+ * immutable in the public API, so a WeakMap safely shares the LUT across every
14
+ * animation/timeline that uses the same curve object.
15
+ */
16
+ export function compileEasing(easing) {
17
+ if (typeof easing === 'function') return { kind: 'function', easing };
18
+ if (!easing || easing.kind !== 'bezier' || (easing.x1 === easing.y1 && easing.x2 === easing.y2)) return linearCompiledEasing;
19
+ const cached = easingLutCache.get(easing);
20
+ if (cached) return cached;
21
+ const values = new Float64Array(EASING_LUT_SIZE + 1);
22
+ for (let i = 0; i <= EASING_LUT_SIZE; i += 1) values[i] = evaluateBezier(easing, i / EASING_LUT_SIZE);
23
+ const compiled = Object.freeze({ kind: 'lut', values });
24
+ easingLutCache.set(easing, compiled);
25
+ return compiled;
26
+ }
27
+
28
+ export function evaluateCompiledEasing(compiled, progress) {
29
+ const p = clamp01(progress);
30
+ if (compiled.kind === 'linear') return p;
31
+ if (compiled.kind === 'function') {
32
+ const value = compiled.easing(p);
33
+ return Number.isFinite(value) ? value : p;
34
+ }
35
+ const scaled = p * EASING_LUT_SIZE;
36
+ const index = Math.min(EASING_LUT_SIZE - 1, Math.floor(scaled));
37
+ const fraction = scaled - index;
38
+ const values = compiled.values;
39
+ return values[index] + (values[index + 1] - values[index]) * fraction;
40
+ }
41
+
42
+ export function derivativeCompiledEasing(compiled, progress) {
43
+ const p = clamp01(progress);
44
+ if (compiled.kind === 'linear') return 1;
45
+ if (compiled.kind === 'function') {
46
+ const epsilon = 1e-4;
47
+ const lo = Math.max(0, p - epsilon);
48
+ const hi = Math.min(1, p + epsilon);
49
+ if (hi - lo <= Number.EPSILON) return 0;
50
+ const a = compiled.easing(lo);
51
+ const b = compiled.easing(hi);
52
+ return Number.isFinite(a) && Number.isFinite(b) ? (b - a) / (hi - lo) : 0;
53
+ }
54
+ const scaled = p * EASING_LUT_SIZE;
55
+ const index = Math.min(EASING_LUT_SIZE - 1, Math.floor(scaled));
56
+ const values = compiled.values;
57
+ return (values[index + 1] - values[index]) * EASING_LUT_SIZE;
58
+ }