@glissade/core 0.39.0-pre.1 → 0.40.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/track.js ADDED
@@ -0,0 +1,1194 @@
1
+ //#region src/easing.ts
2
+ /**
3
+ * Time-mirror of an ease, so a segment played BACKWARD eases identically to the
4
+ * forward segment (used by `retime`'s reverse/pingpong). The built-in eases pair
5
+ * exactly — `easeInX ↔ easeOutX`, and `easeInOutX`/`linear` self-mirror — and a
6
+ * `cubicBezier` mirrors by point reflection (`[x1,y1,x2,y2] → [1−x2,1−y2,1−x1,1−y1]`,
7
+ * the analytic reverse of a bézier ease). Springs are causal and a custom-named
8
+ * ease has no known inverse, so both throw fail-loud rather than mis-animate.
9
+ */
10
+ function mirrorEase(spec) {
11
+ if (spec === void 0) return void 0;
12
+ if (typeof spec === "string") {
13
+ if (spec === "linear") return "linear";
14
+ if (spec.startsWith("easeInOut")) return spec;
15
+ if (spec.startsWith("easeIn")) return `easeOut${spec.slice(6)}`;
16
+ if (spec.startsWith("easeOut")) return `easeIn${spec.slice(7)}`;
17
+ throw new Error(`retime: cannot time-mirror the custom ease '${spec}' for reverse/pingpong — reverse only the built-in eases or a cubicBezier, or retime with { speed } / { shift }`);
18
+ }
19
+ if (spec.kind === "cubicBezier") {
20
+ const [x1, y1, x2, y2] = spec.pts;
21
+ return {
22
+ kind: "cubicBezier",
23
+ pts: [
24
+ 1 - x2,
25
+ 1 - y2,
26
+ 1 - x1,
27
+ 1 - y1
28
+ ]
29
+ };
30
+ }
31
+ throw new Error("retime: cannot reverse/pingpong a spring ease (springs are causal) — retime the spring segment with { speed } / { shift }, or author the reverse explicitly");
32
+ }
33
+ const c1 = 1.70158;
34
+ const c2 = c1 * 1.525;
35
+ const c3 = 2.70158;
36
+ const c4 = 2 * Math.PI / 3;
37
+ const c5 = 2 * Math.PI / 4.5;
38
+ function bounceOut(t) {
39
+ const n1 = 7.5625;
40
+ const d1 = 2.75;
41
+ if (t < 1 / d1) return n1 * t * t;
42
+ if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + .75;
43
+ if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + .9375;
44
+ return n1 * (t -= 2.625 / d1) * t + .984375;
45
+ }
46
+ const easings = {
47
+ linear: (t) => t,
48
+ easeInQuad: (t) => t * t,
49
+ easeOutQuad: (t) => 1 - (1 - t) * (1 - t),
50
+ easeInOutQuad: (t) => t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2,
51
+ easeInCubic: (t) => t ** 3,
52
+ easeOutCubic: (t) => 1 - (1 - t) ** 3,
53
+ easeInOutCubic: (t) => t < .5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2,
54
+ easeInQuart: (t) => t ** 4,
55
+ easeOutQuart: (t) => 1 - (1 - t) ** 4,
56
+ easeInOutQuart: (t) => t < .5 ? 8 * t ** 4 : 1 - (-2 * t + 2) ** 4 / 2,
57
+ easeInQuint: (t) => t ** 5,
58
+ easeOutQuint: (t) => 1 - (1 - t) ** 5,
59
+ easeInOutQuint: (t) => t < .5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2,
60
+ easeInSine: (t) => 1 - Math.cos(t * Math.PI / 2),
61
+ easeOutSine: (t) => Math.sin(t * Math.PI / 2),
62
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
63
+ easeInExpo: (t) => t === 0 ? 0 : 2 ** (10 * t - 10),
64
+ easeOutExpo: (t) => t === 1 ? 1 : 1 - 2 ** (-10 * t),
65
+ easeInOutExpo: (t) => t === 0 ? 0 : t === 1 ? 1 : t < .5 ? 2 ** (20 * t - 10) / 2 : (2 - 2 ** (-20 * t + 10)) / 2,
66
+ easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
67
+ easeOutCirc: (t) => Math.sqrt(1 - (t - 1) * (t - 1)),
68
+ easeInOutCirc: (t) => t < .5 ? (1 - Math.sqrt(1 - (2 * t) ** 2)) / 2 : (Math.sqrt(1 - (-2 * t + 2) ** 2) + 1) / 2,
69
+ easeInBack: (t) => c3 * t ** 3 - c1 * t * t,
70
+ easeOutBack: (t) => 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2,
71
+ easeInOutBack: (t) => t < .5 ? (2 * t) ** 2 * (3.5949095 * 2 * t - c2) / 2 : ((2 * t - 2) ** 2 * (3.5949095 * (t * 2 - 2) + c2) + 2) / 2,
72
+ easeInElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : -(2 ** (10 * t - 10)) * Math.sin((t * 10 - 10.75) * c4),
73
+ easeOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - .75) * c4) + 1,
74
+ easeInOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : t < .5 ? -(2 ** (20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 : 2 ** (-20 * t + 10) * Math.sin((20 * t - 11.125) * c5) / 2 + 1,
75
+ easeInBounce: (t) => 1 - bounceOut(1 - t),
76
+ easeOutBounce: bounceOut,
77
+ easeInOutBounce: (t) => t < .5 ? (1 - bounceOut(1 - 2 * t)) / 2 : (1 + bounceOut(2 * t - 1)) / 2
78
+ };
79
+ function bounceOutD(t) {
80
+ const n1 = 7.5625;
81
+ const d1 = 2.75;
82
+ if (t < 1 / d1) return 2 * n1 * t;
83
+ if (t < 2 / d1) return 2 * n1 * (t - 1.5 / d1);
84
+ if (t < 2.5 / d1) return 2 * n1 * (t - 2.25 / d1);
85
+ return 2 * n1 * (t - 2.625 / d1);
86
+ }
87
+ const LN2 = Math.LN2;
88
+ /**
89
+ * Analytic derivatives d(u) of every named ease (§B.6) — closed-form, used
90
+ * for reading velocity off in-flight curves at interruption time. Property-
91
+ * tested against central differences at interior points.
92
+ */
93
+ const easingDerivatives = {
94
+ linear: () => 1,
95
+ easeInQuad: (t) => 2 * t,
96
+ easeOutQuad: (t) => 2 * (1 - t),
97
+ easeInOutQuad: (t) => t < .5 ? 4 * t : 4 * (1 - t),
98
+ easeInCubic: (t) => 3 * t ** 2,
99
+ easeOutCubic: (t) => 3 * (1 - t) ** 2,
100
+ easeInOutCubic: (t) => t < .5 ? 12 * t ** 2 : 12 * (1 - t) ** 2,
101
+ easeInQuart: (t) => 4 * t ** 3,
102
+ easeOutQuart: (t) => 4 * (1 - t) ** 3,
103
+ easeInOutQuart: (t) => t < .5 ? 32 * t ** 3 : 32 * (1 - t) ** 3,
104
+ easeInQuint: (t) => 5 * t ** 4,
105
+ easeOutQuint: (t) => 5 * (1 - t) ** 4,
106
+ easeInOutQuint: (t) => t < .5 ? 80 * t ** 4 : 80 * (1 - t) ** 4,
107
+ easeInSine: (t) => Math.PI / 2 * Math.sin(t * Math.PI / 2),
108
+ easeOutSine: (t) => Math.PI / 2 * Math.cos(t * Math.PI / 2),
109
+ easeInOutSine: (t) => Math.PI / 2 * Math.sin(Math.PI * t),
110
+ easeInExpo: (t) => t === 0 ? 0 : 10 * LN2 * 2 ** (10 * t - 10),
111
+ easeOutExpo: (t) => t === 1 ? 0 : 10 * LN2 * 2 ** (-10 * t),
112
+ easeInOutExpo: (t) => t === 0 || t === 1 ? 0 : t < .5 ? 10 * LN2 * 2 ** (20 * t - 10) : 10 * LN2 * 2 ** (-20 * t + 10),
113
+ easeInCirc: (t) => t / Math.sqrt(1 - t * t),
114
+ easeOutCirc: (t) => (1 - t) / Math.sqrt(1 - (t - 1) * (t - 1)),
115
+ easeInOutCirc: (t) => t < .5 ? 2 * t / Math.sqrt(1 - 4 * t * t) : (2 - 2 * t) / Math.sqrt(1 - (-2 * t + 2) ** 2),
116
+ easeInBack: (t) => 3 * c3 * t ** 2 - 2 * c1 * t,
117
+ easeOutBack: (t) => 3 * c3 * (t - 1) ** 2 + 2 * c1 * (t - 1),
118
+ easeInOutBack: (t) => t < .5 ? 12 * 3.5949095 * t ** 2 - 4 * c2 * t : 3 * 3.5949095 * (2 * t - 2) ** 2 + 2 * c2 * (2 * t - 2),
119
+ easeInElastic: (t) => {
120
+ if (t === 0 || t === 1) return 0;
121
+ const theta = (t * 10 - 10.75) * c4;
122
+ const amp = 2 ** (10 * t - 10);
123
+ return -(10 * LN2 * amp * Math.sin(theta) + amp * 10 * c4 * Math.cos(theta));
124
+ },
125
+ easeOutElastic: (t) => {
126
+ if (t === 0 || t === 1) return 0;
127
+ const phi = (t * 10 - .75) * c4;
128
+ const amp = 2 ** (-10 * t);
129
+ return -10 * LN2 * amp * Math.sin(phi) + amp * 10 * c4 * Math.cos(phi);
130
+ },
131
+ easeInOutElastic: (t) => {
132
+ if (t === 0 || t === 1) return 0;
133
+ const psi = (20 * t - 11.125) * c5;
134
+ if (t < .5) {
135
+ const amp = 2 ** (20 * t - 10);
136
+ return -(20 * LN2 * amp * Math.sin(psi) + amp * 20 * c5 * Math.cos(psi)) / 2;
137
+ }
138
+ const amp = 2 ** (-20 * t + 10);
139
+ return (-20 * LN2 * amp * Math.sin(psi) + amp * 20 * c5 * Math.cos(psi)) / 2;
140
+ },
141
+ easeInBounce: (t) => bounceOutD(1 - t),
142
+ easeOutBounce: bounceOutD,
143
+ easeInOutBounce: (t) => t < .5 ? bounceOutD(1 - 2 * t) : bounceOutD(2 * t - 1)
144
+ };
145
+ /** Default property-tween ease (Motion Canvas precedent). */
146
+ const DEFAULT_EASE = "easeInOutCubic";
147
+ function bezierKernel(p1x, p1y, p2x, p2y) {
148
+ const ax = 3 * p1x - 3 * p2x + 1;
149
+ const bx = 3 * p2x - 6 * p1x;
150
+ const cx = 3 * p1x;
151
+ const ay = 3 * p1y - 3 * p2y + 1;
152
+ const by = 3 * p2y - 6 * p1y;
153
+ const cy = 3 * p1y;
154
+ const sampleX = (u) => ((ax * u + bx) * u + cx) * u;
155
+ const sampleY = (u) => ((ay * u + by) * u + cy) * u;
156
+ const sampleDX = (u) => (3 * ax * u + 2 * bx) * u + cx;
157
+ const sampleDY = (u) => (3 * ay * u + 2 * by) * u + cy;
158
+ const solveU = (x) => {
159
+ let u = x;
160
+ for (let i = 0; i < 8; i++) {
161
+ const err = sampleX(u) - x;
162
+ if (Math.abs(err) < 1e-7) return u;
163
+ const d = sampleDX(u);
164
+ if (Math.abs(d) < 1e-6) break;
165
+ u -= err / d;
166
+ }
167
+ let lo = 0;
168
+ let hi = 1;
169
+ u = x;
170
+ while (hi - lo > 1e-7) {
171
+ if (sampleX(u) < x) lo = u;
172
+ else hi = u;
173
+ u = (lo + hi) / 2;
174
+ }
175
+ return u;
176
+ };
177
+ return {
178
+ sampleY,
179
+ sampleDX,
180
+ sampleDY,
181
+ solveU
182
+ };
183
+ }
184
+ /**
185
+ * CSS-style cubic bézier where x is time and y is progress. Newton's method
186
+ * with a bisection fallback for the flat-derivative regions.
187
+ */
188
+ function cubicBezier(p1x, p1y, p2x, p2y) {
189
+ const k = bezierKernel(p1x, p1y, p2x, p2y);
190
+ return (t) => {
191
+ if (t <= 0) return 0;
192
+ if (t >= 1) return 1;
193
+ return k.sampleY(k.solveU(t));
194
+ };
195
+ }
196
+ /** Analytic dy/dx of a cubic bézier ease: y'(s)/x'(s) at the solved parameter (§B.6). */
197
+ function cubicBezierDerivative(p1x, p1y, p2x, p2y) {
198
+ const k = bezierKernel(p1x, p1y, p2x, p2y);
199
+ return (t) => {
200
+ const u = k.solveU(Math.min(1, Math.max(0, t)));
201
+ const dx = k.sampleDX(u);
202
+ if (Math.abs(dx) < 1e-9) return 0;
203
+ return k.sampleDY(u) / dx;
204
+ };
205
+ }
206
+ var UnknownEasingError = class extends Error {
207
+ constructor(name) {
208
+ super(`unknown easing '${name}'; register it via easings or use cubicBezier/spring`);
209
+ this.name = "UnknownEasingError";
210
+ }
211
+ };
212
+ function namedEasing(name) {
213
+ const fn = easings[name];
214
+ if (!fn) throw new UnknownEasingError(name);
215
+ return fn;
216
+ }
217
+ //#endregion
218
+ //#region src/spring.ts
219
+ const DEFAULT_SETTLE_TOLERANCE = .005;
220
+ function params(cfg) {
221
+ const mass = cfg.mass ?? 1;
222
+ if (!(cfg.stiffness > 0) || !(cfg.damping > 0) || !(mass > 0)) throw new RangeError("spring stiffness, damping, and mass must all be > 0");
223
+ return {
224
+ w0: Math.sqrt(cfg.stiffness / mass),
225
+ zeta: cfg.damping / (2 * Math.sqrt(cfg.stiffness * mass))
226
+ };
227
+ }
228
+ /** Raw closed-form spring velocity at time t — d/dt of rawValue's three branches. */
229
+ function rawDerivative(cfg, t) {
230
+ if (t <= 0) return 0;
231
+ const { w0, zeta } = params(cfg);
232
+ if (Math.abs(zeta - 1) < 1e-9) return w0 * w0 * t * Math.exp(-w0 * t);
233
+ if (zeta < 1) {
234
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
235
+ return Math.exp(-zeta * w0 * t) * (w0 * w0 / wd) * Math.sin(wd * t);
236
+ }
237
+ const s = Math.sqrt(zeta * zeta - 1);
238
+ const r1 = -w0 * (zeta - s);
239
+ const r2 = -w0 * (zeta + s);
240
+ return r1 * r2 * (Math.exp(r1 * t) - Math.exp(r2 * t)) / (r1 - r2);
241
+ }
242
+ /** Raw closed-form spring position at time t (seconds). Approaches 1, may overshoot. */
243
+ function rawValue(cfg, t) {
244
+ if (t <= 0) return 0;
245
+ const { w0, zeta } = params(cfg);
246
+ if (Math.abs(zeta - 1) < 1e-9) return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
247
+ if (zeta < 1) {
248
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
249
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + zeta * w0 / wd * Math.sin(wd * t));
250
+ }
251
+ const s = Math.sqrt(zeta * zeta - 1);
252
+ const r1 = -w0 * (zeta - s);
253
+ const r2 = -w0 * (zeta + s);
254
+ return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
255
+ }
256
+ /**
257
+ * Settle duration: the earliest time after which |x - 1| stays within
258
+ * settleTolerance. Closed-form via the decay envelope for the underdamped
259
+ * case; bisection on the monotone tail otherwise. Deterministic.
260
+ */
261
+ function duration(cfg, opts) {
262
+ const tol = opts?.settleTolerance ?? DEFAULT_SETTLE_TOLERANCE;
263
+ const { w0, zeta } = params(cfg);
264
+ if (zeta < 1) {
265
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
266
+ const amp = Math.sqrt(1 + (zeta * w0 / wd) ** 2);
267
+ return Math.log(amp / tol) / (zeta * w0);
268
+ }
269
+ let hi = 1 / w0;
270
+ while (1 - rawValue(cfg, hi) > tol) hi *= 2;
271
+ let lo = 0;
272
+ for (let i = 0; i < 64 && hi - lo > 1e-9; i++) {
273
+ const mid = (lo + hi) / 2;
274
+ if (1 - rawValue(cfg, mid) > tol) lo = mid;
275
+ else hi = mid;
276
+ }
277
+ return hi;
278
+ }
279
+ /**
280
+ * Spring progress at local time t, affinely rescaled so value(duration) = 1
281
+ * exactly — the raw form only approaches 1, and an unscaled curve would snap
282
+ * at the key (§2.7 "endpoint continuity"). May exceed 1 (overshoot).
283
+ */
284
+ function value(cfg, t, opts) {
285
+ const d = duration(cfg, opts);
286
+ return rawValue(cfg, Math.min(t, d)) / rawValue(cfg, d);
287
+ }
288
+ function retarget(cfg, x0, v0) {
289
+ const { w0, zeta } = params(cfg);
290
+ let value0;
291
+ let velocity0;
292
+ let envelope;
293
+ if (Math.abs(zeta - 1) < 1e-9) {
294
+ const b = v0 + w0 * x0;
295
+ value0 = (tau) => tau <= 0 ? x0 : Math.exp(-w0 * tau) * (x0 + b * tau);
296
+ velocity0 = (tau) => tau <= 0 ? v0 : Math.exp(-w0 * tau) * (v0 - w0 * b * tau);
297
+ envelope = (tau) => Math.exp(-w0 * tau) * (Math.abs(x0) + Math.abs(b) * tau);
298
+ } else if (zeta < 1) {
299
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
300
+ const c2v = (v0 + zeta * w0 * x0) / wd;
301
+ const amp = Math.hypot(x0, c2v);
302
+ value0 = (tau) => tau <= 0 ? x0 : Math.exp(-zeta * w0 * tau) * (x0 * Math.cos(wd * tau) + c2v * Math.sin(wd * tau));
303
+ velocity0 = (tau) => tau <= 0 ? v0 : Math.exp(-zeta * w0 * tau) * (v0 * Math.cos(wd * tau) - (w0 * w0 * x0 + zeta * w0 * v0) / wd * Math.sin(wd * tau));
304
+ envelope = (tau) => Math.exp(-zeta * w0 * tau) * amp;
305
+ } else {
306
+ const s = Math.sqrt(zeta * zeta - 1);
307
+ const rp = w0 * (-zeta + s);
308
+ const rm = w0 * (-zeta - s);
309
+ const cp = (v0 - rm * x0) / (rp - rm);
310
+ const cm = (rp * x0 - v0) / (rp - rm);
311
+ value0 = (tau) => tau <= 0 ? x0 : cp * Math.exp(rp * tau) + cm * Math.exp(rm * tau);
312
+ velocity0 = (tau) => tau <= 0 ? v0 : cp * rp * Math.exp(rp * tau) + cm * rm * Math.exp(rm * tau);
313
+ envelope = (tau) => Math.abs(cp) * Math.exp(rp * tau) + Math.abs(cm) * Math.exp(rm * tau);
314
+ }
315
+ const settleTime = (tol) => {
316
+ const eps = tol ?? Math.abs(x0) * .005 + 1e-6;
317
+ if (envelope(0) <= eps && Math.abs(v0) < 1e-12) return 0;
318
+ let hi = 1 / w0;
319
+ let guard = 0;
320
+ while (envelope(hi) > eps && guard++ < 64) hi *= 2;
321
+ let lo = 0;
322
+ for (let i = 0; i < 64 && hi - lo > 1e-9; i++) {
323
+ const mid = (lo + hi) / 2;
324
+ if (Math.max(envelope(mid), envelope(mid * 1.05 + 1e-6)) > eps) lo = mid;
325
+ else hi = mid;
326
+ }
327
+ return hi;
328
+ };
329
+ return {
330
+ value: value0,
331
+ velocity: velocity0,
332
+ settleTime
333
+ };
334
+ }
335
+ const spring = Object.assign((cfg) => {
336
+ params(cfg);
337
+ return {
338
+ kind: "spring",
339
+ stiffness: cfg.stiffness,
340
+ damping: cfg.damping,
341
+ mass: cfg.mass ?? 1
342
+ };
343
+ }, {
344
+ duration,
345
+ value,
346
+ retarget
347
+ });
348
+ /**
349
+ * Named spring feels (react-spring conventions, mass 1) — a vocabulary instead
350
+ * of hand-tuned stiffness/damping. Use `spring(springPresets.wobbly)` or
351
+ * `springTo(t, a, b, springPresets.gentle)`.
352
+ */
353
+ const springPresets = {
354
+ default: {
355
+ stiffness: 170,
356
+ damping: 26
357
+ },
358
+ gentle: {
359
+ stiffness: 120,
360
+ damping: 14
361
+ },
362
+ wobbly: {
363
+ stiffness: 180,
364
+ damping: 12
365
+ },
366
+ stiff: {
367
+ stiffness: 210,
368
+ damping: 20
369
+ },
370
+ slow: {
371
+ stiffness: 280,
372
+ damping: 60
373
+ },
374
+ molasses: {
375
+ stiffness: 280,
376
+ damping: 120
377
+ }
378
+ };
379
+ /**
380
+ * The spring as a normalized easing over a segment whose length must equal
381
+ * spring.duration(cfg) (validated at the document layer, §2.7).
382
+ */
383
+ function springEasing(cfg) {
384
+ const d = duration(cfg);
385
+ return (p) => value(cfg, p * d);
386
+ }
387
+ /**
388
+ * Analytic d/dp of springEasing (§B.6): oscillator derivative × the affine
389
+ * rescale factor × duration (chain rule p → t = p·D). Flat past p=1,
390
+ * matching value()'s clamp (right-derivative convention).
391
+ */
392
+ function springEasingDerivative(cfg) {
393
+ const d = duration(cfg);
394
+ const scale = d / rawValue(cfg, d);
395
+ return (p) => p >= 1 ? 0 : rawDerivative(cfg, p * d) * scale;
396
+ }
397
+ //#endregion
398
+ //#region src/devWarning.ts
399
+ let devWarn = (msg) => {
400
+ globalThis.console?.warn(`[glissade] ${msg}`);
401
+ };
402
+ function setDevWarning(fn) {
403
+ devWarn = fn;
404
+ }
405
+ /** Internal: emit through the configurable channel. */
406
+ function emitDevWarning(message) {
407
+ devWarn(message);
408
+ }
409
+ //#endregion
410
+ //#region src/color.ts
411
+ const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
412
+ const RGB_RE = /^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*(?:[,/]\s*([\d.]+%?)\s*)?\)$/i;
413
+ var ColorParseError = class extends Error {
414
+ constructor(input) {
415
+ super(`cannot parse color '${input}' (supported: #rgb[a], #rrggbb[aa], rgb()/rgba())`);
416
+ this.name = "ColorParseError";
417
+ }
418
+ };
419
+ function parseColor(input) {
420
+ const hex = HEX_RE.exec(input);
421
+ if (hex) {
422
+ let h = hex[1];
423
+ if (h.length <= 4) h = [...h].map((c) => c + c).join("");
424
+ const n = parseInt(h, 16);
425
+ if (h.length === 6) return {
426
+ r: n >> 16,
427
+ g: n >> 8 & 255,
428
+ b: n & 255,
429
+ a: 1
430
+ };
431
+ return {
432
+ r: n >>> 24 & 255,
433
+ g: n >>> 16 & 255,
434
+ b: n >>> 8 & 255,
435
+ a: (n & 255) / 255
436
+ };
437
+ }
438
+ const rgb = RGB_RE.exec(input);
439
+ if (rgb) {
440
+ const alphaRaw = rgb[4];
441
+ const a = alphaRaw === void 0 ? 1 : alphaRaw.endsWith("%") ? parseFloat(alphaRaw) / 100 : parseFloat(alphaRaw);
442
+ return {
443
+ r: parseFloat(rgb[1]),
444
+ g: parseFloat(rgb[2]),
445
+ b: parseFloat(rgb[3]),
446
+ a
447
+ };
448
+ }
449
+ throw new ColorParseError(input);
450
+ }
451
+ function formatColor(c) {
452
+ const clampByte = (v) => Math.min(255, Math.max(0, Math.round(v)));
453
+ const hex = (v) => clampByte(v).toString(16).padStart(2, "0");
454
+ const base = `#${hex(c.r)}${hex(c.g)}${hex(c.b)}`;
455
+ const a = Math.min(1, Math.max(0, c.a));
456
+ return a >= 1 ? base : `${base}${hex(a * 255)}`;
457
+ }
458
+ function srgbToLinear(c) {
459
+ const v = c / 255;
460
+ return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
461
+ }
462
+ function linearToSrgb(v) {
463
+ return (v <= .0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - .055) * 255;
464
+ }
465
+ function rgbaToOklab(c) {
466
+ const r = srgbToLinear(c.r);
467
+ const g = srgbToLinear(c.g);
468
+ const b = srgbToLinear(c.b);
469
+ const l = Math.cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * b);
470
+ const m = Math.cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * b);
471
+ const s = Math.cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * b);
472
+ return {
473
+ L: .2104542553 * l + .793617785 * m - .0040720468 * s,
474
+ a: 1.9779984951 * l - 2.428592205 * m + .4505937099 * s,
475
+ b: .0259040371 * l + .7827717662 * m - .808675766 * s,
476
+ alpha: c.a
477
+ };
478
+ }
479
+ function oklabToRgba(c) {
480
+ const l = (c.L + .3963377774 * c.a + .2158037573 * c.b) ** 3;
481
+ const m = (c.L - .1055613458 * c.a - .0638541728 * c.b) ** 3;
482
+ const s = (c.L - .0894841775 * c.a - 1.291485548 * c.b) ** 3;
483
+ return {
484
+ r: linearToSrgb(4.0767416621 * l - 3.3077115913 * m + .2309699292 * s),
485
+ g: linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - .3413193965 * s),
486
+ b: linearToSrgb(-.0041960863 * l - .7034186147 * m + 1.707614701 * s),
487
+ a: c.alpha
488
+ };
489
+ }
490
+ /** Interpolate two CSS color strings in OKLab; t may extrapolate. */
491
+ function lerpColor(from, to, t) {
492
+ const a = rgbaToOklab(parseColor(from));
493
+ const b = rgbaToOklab(parseColor(to));
494
+ const mix = (x, y) => x + (y - x) * t;
495
+ return formatColor(oklabToRgba({
496
+ L: mix(a.L, b.L),
497
+ a: mix(a.a, b.a),
498
+ b: mix(a.b, b.b),
499
+ alpha: mix(a.alpha, b.alpha)
500
+ }));
501
+ }
502
+ //#endregion
503
+ //#region src/valueTypes.ts
504
+ /**
505
+ * Value-type registry with pluggable per-type interpolation (DESIGN.md §2.2).
506
+ * `extrapolates` declares whether a type's lerp accepts easedT outside [0,1]
507
+ * (spring overshoot); non-extrapolating types clamp.
508
+ */
509
+ const registry = /* @__PURE__ */ new Map();
510
+ function registerValueType(vt) {
511
+ registry.set(vt.id, vt);
512
+ }
513
+ var UnknownValueTypeError = class extends Error {
514
+ constructor(id) {
515
+ super(`unknown value type '${id}'; register it via registerValueType()`);
516
+ this.name = "UnknownValueTypeError";
517
+ }
518
+ };
519
+ function getValueType(id) {
520
+ const vt = registry.get(id);
521
+ if (!vt) throw new UnknownValueTypeError(id);
522
+ return vt;
523
+ }
524
+ /**
525
+ * The ids of every currently-registered value type, in registration order
526
+ * (the built-ins first, then any custom `registerValueType` additions). The
527
+ * introspection seam `describe()` reads to list `valueTypes` from the REAL
528
+ * registry — so the manifest can't drift from what `evaluate()` can actually
529
+ * interpolate. Pure read; never touched by evaluate().
530
+ */
531
+ function listValueTypes() {
532
+ return [...registry.keys()];
533
+ }
534
+ /**
535
+ * Resolve a value-type id to its REPRESENTATION (the bind guard's compatibility
536
+ * key, §2.2): a type's `repr` (the built-in it's representationally compatible
537
+ * with), else the id itself. SINGLE-HOP — `repr` is never chained, so a custom
538
+ * type's repr must be a base type. An UNREGISTERED id resolves to itself (the
539
+ * guard's job is repr-compat, not existence — `getValueType` enforces that).
540
+ */
541
+ function reprOf(id) {
542
+ return registry.get(id)?.repr ?? id;
543
+ }
544
+ const numberType = {
545
+ id: "number",
546
+ lerp: (a, b, t) => a + (b - a) * t,
547
+ extrapolates: true,
548
+ equals: Object.is,
549
+ add: (a, b) => a + b,
550
+ sub: (a, b) => a - b,
551
+ scale: (a, k) => a * k,
552
+ defaultHandoff: "spring"
553
+ };
554
+ const vec2Equals = (a, b) => a[0] === b[0] && a[1] === b[1];
555
+ const vec2Type = {
556
+ id: "vec2",
557
+ lerp: (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
558
+ extrapolates: true,
559
+ equals: vec2Equals,
560
+ add: (a, b) => [a[0] + b[0], a[1] + b[1]],
561
+ sub: (a, b) => [a[0] - b[0], a[1] - b[1]],
562
+ scale: (a, k) => [a[0] * k, a[1] * k],
563
+ defaultHandoff: "spring"
564
+ };
565
+ /** vec2 swept along a circular arc: polar lerp of radius + shortest-path angle (§2.2). */
566
+ const vec2ArcType = {
567
+ id: "vec2-arc",
568
+ repr: "vec2",
569
+ lerp: (a, b, t) => {
570
+ const ra = Math.hypot(a[0], a[1]);
571
+ const rb = Math.hypot(b[0], b[1]);
572
+ const angA = Math.atan2(a[1], a[0]);
573
+ let dAng = Math.atan2(b[1], b[0]) - angA;
574
+ while (dAng > Math.PI) dAng -= 2 * Math.PI;
575
+ while (dAng < -Math.PI) dAng += 2 * Math.PI;
576
+ const r = ra + (rb - ra) * t;
577
+ const ang = angA + dAng * t;
578
+ return [r * Math.cos(ang), r * Math.sin(ang)];
579
+ },
580
+ extrapolates: true,
581
+ equals: vec2Equals,
582
+ defaultHandoff: "blend-from-frozen"
583
+ };
584
+ const colorType = {
585
+ id: "color",
586
+ lerp: lerpColor,
587
+ extrapolates: true,
588
+ equals: (a, b) => a === b,
589
+ defaultHandoff: "blend-from-frozen"
590
+ };
591
+ /** Discrete types: hold-only by construction (§2.2); lerp snaps at t=1. */
592
+ function discrete(id) {
593
+ return {
594
+ id,
595
+ lerp: (a, b, t) => t >= 1 ? b : a,
596
+ extrapolates: false,
597
+ equals: (a, b) => Object.is(a, b),
598
+ defaultHandoff: "cut"
599
+ };
600
+ }
601
+ const stringType = discrete("string");
602
+ const booleanType = discrete("boolean");
603
+ const lerpV = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
604
+ /** Topology must match for a morph (contour count, closed flags, vertex counts). */
605
+ function pathTopologyMatches(a, b) {
606
+ if (a.length !== b.length) return false;
607
+ for (let i = 0; i < a.length; i++) {
608
+ const ca = a[i];
609
+ const cb = b[i];
610
+ if (ca.closed !== cb.closed || ca.v.length !== cb.v.length) return false;
611
+ }
612
+ return true;
613
+ }
614
+ let warnedPathTopology = false;
615
+ /**
616
+ * Path morphing (§2.2): pairwise lerp of anchors and tangents — exactly how
617
+ * lottie-web morphs, so imported animations are pixel-faithful. Mismatched
618
+ * topology snaps (hold a, then b at t ≥ 1) with a one-time dev warning; the
619
+ * de Casteljau normalization fallback for arbitrary native morphs is tracked
620
+ * future work. Lerp-only: offsets are not well-defined under mismatched
621
+ * topology, so no add/sub/scale — handoffs blend from the frozen value.
622
+ */
623
+ const pathType = {
624
+ id: "path",
625
+ lerp: (a, b, t) => {
626
+ if (!pathTopologyMatches(a, b)) {
627
+ if (!warnedPathTopology) {
628
+ warnedPathTopology = true;
629
+ emitDevWarning("path lerp with mismatched topology (contour/vertex counts or closed flags differ): snapping instead of morphing — supply matched vertex counts (§2.2)");
630
+ }
631
+ return t >= 1 ? b : a;
632
+ }
633
+ return a.map((ca, i) => {
634
+ const cb = b[i];
635
+ return {
636
+ closed: ca.closed,
637
+ v: ca.v.map((p, j) => lerpV(p, cb.v[j], t)),
638
+ in: ca.in.map((p, j) => lerpV(p, cb.in[j], t)),
639
+ out: ca.out.map((p, j) => lerpV(p, cb.out[j], t))
640
+ };
641
+ });
642
+ },
643
+ extrapolates: false,
644
+ equals: (a, b) => {
645
+ if (a === b) return true;
646
+ if (!pathTopologyMatches(a, b)) return false;
647
+ const eq = (x, y) => x[0] === y[0] && x[1] === y[1];
648
+ return a.every((ca, i) => {
649
+ const cb = b[i];
650
+ return ca.v.every((p, j) => eq(p, cb.v[j])) && ca.in.every((p, j) => eq(p, cb.in[j])) && ca.out.every((p, j) => eq(p, cb.out[j]));
651
+ });
652
+ },
653
+ defaultHandoff: "blend-from-frozen"
654
+ };
655
+ const fontAxisKeys = (a) => Object.keys(a).sort();
656
+ const fontAxesKeysMatch = (a, b) => {
657
+ const ka = fontAxisKeys(a);
658
+ const kb = fontAxisKeys(b);
659
+ return ka.length === kb.length && ka.every((k, i) => k === kb[i]);
660
+ };
661
+ let warnedFontAxes = false;
662
+ /**
663
+ * Variable-font axis map (§3.6) — `{ wght: 700, opsz: 14 }`, the ANIMATABLE
664
+ * counterpart to the static `fontVariationSettings` string. Lerps PER AXIS. Both
665
+ * keyframes must declare the SAME axis tags; a mismatched axis set SNAPS (holds
666
+ * `a`, then `b` at t≥1) + a one-time dev-warning — the same topology rule as
667
+ * path/paint, which prevents the silent-NaN class. No add/sub/scale (a structured
668
+ * map carries no linear offset); handoffs blend from the frozen value.
669
+ */
670
+ const fontAxesType = {
671
+ id: "fontAxes",
672
+ lerp: (a, b, t) => {
673
+ if (!fontAxesKeysMatch(a, b)) {
674
+ if (!warnedFontAxes) {
675
+ warnedFontAxes = true;
676
+ emitDevWarning("fontAxes lerp with mismatched axis tags (the two keyframes declare different axes): snapping instead of interpolating — give both keyframes the SAME axis set (§3.6)");
677
+ }
678
+ return t >= 1 ? b : a;
679
+ }
680
+ const out = {};
681
+ for (const k of Object.keys(a)) out[k] = a[k] + (b[k] - a[k]) * t;
682
+ return out;
683
+ },
684
+ extrapolates: false,
685
+ equals: (a, b) => {
686
+ if (a === b) return true;
687
+ if (!fontAxesKeysMatch(a, b)) return false;
688
+ return Object.keys(a).every((k) => a[k] === b[k]);
689
+ },
690
+ defaultHandoff: "blend-from-frozen"
691
+ };
692
+ const lerpN = (a, b, t) => a + (b - a) * t;
693
+ const lerpPt = (a, b, t) => [lerpN(a[0], b[0], t), lerpN(a[1], b[1], t)];
694
+ /** Same hue at alpha 0 — a transparent stand-in so an appearing/disappearing
695
+ * `bg` (mesh baseline) fades through alpha instead of popping at the boundary.
696
+ * `parseColor` THROWS on a non-hex/non-canonical CSS color (hsl/named/oklch);
697
+ * since this runs inside `lerp` during evaluate() it must NEVER throw — fall
698
+ * back to holding the PRESENT color so the bg snaps in/out instead of crashing
699
+ * the whole frame. */
700
+ function transparentOf(color) {
701
+ try {
702
+ return formatColor({
703
+ ...parseColor(color),
704
+ a: 0
705
+ });
706
+ } catch {
707
+ return color;
708
+ }
709
+ }
710
+ /** `lerpColor` that NEVER throws inside `lerp` (evaluate()): on an unparseable
711
+ * color (hsl/named/oklch — `parseColor` only knows hex/rgb) it SNAPS (holds `a`,
712
+ * then `b` at t≥1) instead of crashing the frame. Byte-identical to `lerpColor`
713
+ * for parseable colors, so it does not move any golden. */
714
+ function safeLerpColor(a, b, t) {
715
+ try {
716
+ return lerpColor(a, b, t);
717
+ } catch {
718
+ return t >= 1 ? b : a;
719
+ }
720
+ }
721
+ /** Lift a solid color to a uniform gradient matching `shape` (every stop = color),
722
+ * so a color↔gradient tween fades smoothly instead of snapping. */
723
+ function liftColor(color, shape) {
724
+ const stops = shape.stops.map((s) => ({
725
+ offset: s.offset,
726
+ color
727
+ }));
728
+ const interp = shape.interpolation ? { interpolation: shape.interpolation } : {};
729
+ return shape.kind === "radial" ? {
730
+ kind: "radial",
731
+ stops,
732
+ ...shape.center ? { center: shape.center } : {},
733
+ ...shape.radius !== void 0 ? { radius: shape.radius } : {},
734
+ ...interp
735
+ } : {
736
+ kind: "linear",
737
+ stops,
738
+ ...shape.from ? { from: shape.from } : {},
739
+ ...shape.to ? { to: shape.to } : {},
740
+ ...interp
741
+ };
742
+ }
743
+ let warnedPaintShape = false;
744
+ function paintSnap(t, a, b, message = "paint lerp across mismatched gradient shapes (different kind or stop count): snapping instead of interpolating — match the kind + stop count on both keyframes (§2.2)") {
745
+ if (!warnedPaintShape) {
746
+ warnedPaintShape = true;
747
+ emitDevWarning(message);
748
+ }
749
+ return t >= 1 ? b : a;
750
+ }
751
+ const stopsEqual = (a, b) => a.length === b.length && a.every((s, i) => s.offset === b[i].offset && s.color === b[i].color);
752
+ /**
753
+ * Gradient paint morphing (§2.2): a solid color lifts to a uniform gradient to
754
+ * meet the other side, then matched-kind/matched-stop-count gradients lerp their
755
+ * stops (offset + oklab color) and geometry pairwise; a mismatched kind or stop
756
+ * count snaps (hold a, then b at t ≥ 1) with a one-time dev warning. Lerp-only —
757
+ * no offsets under mismatch — so handoffs blend from the frozen value.
758
+ */
759
+ const paintType = {
760
+ id: "paint",
761
+ lerp: (a, b, t) => {
762
+ if (a.kind === "color" && b.kind === "color") return {
763
+ kind: "color",
764
+ color: lerpColor(a.color, b.color, t)
765
+ };
766
+ if (a.kind === "mesh" && b.kind === "mesh") {
767
+ if (a.points.length !== b.points.length) return paintSnap(t, a, b);
768
+ if (a.interpolation !== b.interpolation) return paintSnap(t, a, b, `paint lerp across mismatched mesh interpolation modes ('${a.interpolation ?? "smooth"}' → '${b.interpolation ?? "smooth"}'): snapping instead of interpolating — the blend kernel is discrete, so it switches once at the boundary; match \`interpolation\` on both keyframes to morph the points (§3 Paint)`);
769
+ const points = a.points.map((pa, i) => {
770
+ const pb = b.points[i];
771
+ return {
772
+ pos: lerpPt(pa.pos, pb.pos, t),
773
+ color: lerpColor(pa.color, pb.color, t)
774
+ };
775
+ });
776
+ const bg = a.bg !== void 0 || b.bg !== void 0 ? { bg: safeLerpColor(a.bg ?? transparentOf(b.bg), b.bg ?? transparentOf(a.bg), t) } : {};
777
+ return {
778
+ kind: "mesh",
779
+ points,
780
+ ...a.interpolation ? { interpolation: a.interpolation } : {},
781
+ ...bg
782
+ };
783
+ }
784
+ if (a.kind === "mesh" || b.kind === "mesh") return paintSnap(t, a, b);
785
+ let A = a;
786
+ let B = b;
787
+ if (A.kind === "color" && B.kind !== "color") A = liftColor(A.color, B);
788
+ else if (B.kind === "color" && A.kind !== "color") B = liftColor(B.color, A);
789
+ if (A.kind === "color" || B.kind === "color" || A.kind !== B.kind || A.stops.length !== B.stops.length) return paintSnap(t, a, b);
790
+ const stops = A.stops.map((sa, i) => ({
791
+ offset: lerpN(sa.offset, B.stops[i].offset, t),
792
+ color: lerpColor(sa.color, B.stops[i].color, t)
793
+ }));
794
+ const interp = A.interpolation ? { interpolation: A.interpolation } : {};
795
+ if (A.kind === "radial" && B.kind === "radial") return {
796
+ kind: "radial",
797
+ stops,
798
+ ...A.center && B.center ? { center: lerpPt(A.center, B.center, t) } : A.center ? { center: A.center } : {},
799
+ ...A.radius !== void 0 && B.radius !== void 0 ? { radius: lerpN(A.radius, B.radius, t) } : A.radius !== void 0 ? { radius: A.radius } : {},
800
+ ...interp
801
+ };
802
+ if (A.kind === "linear" && B.kind === "linear") return {
803
+ kind: "linear",
804
+ stops,
805
+ ...A.from && B.from ? { from: lerpPt(A.from, B.from, t) } : A.from ? { from: A.from } : {},
806
+ ...A.to && B.to ? { to: lerpPt(A.to, B.to, t) } : A.to ? { to: A.to } : {},
807
+ ...interp
808
+ };
809
+ return paintSnap(t, a, b);
810
+ },
811
+ extrapolates: false,
812
+ equals: (a, b) => {
813
+ if (a === b) return true;
814
+ if (a.kind !== b.kind) return false;
815
+ if (a.kind === "color") return b.kind === "color" && a.color === b.color;
816
+ if (a.kind === "mesh") {
817
+ if (b.kind !== "mesh") return false;
818
+ if (a.interpolation !== b.interpolation || a.bg !== b.bg) return false;
819
+ if (a.points.length !== b.points.length) return false;
820
+ return a.points.every((p, i) => {
821
+ const q = b.points[i];
822
+ return p.color === q.color && p.pos[0] === q.pos[0] && p.pos[1] === q.pos[1];
823
+ });
824
+ }
825
+ if (b.kind === "color" || b.kind === "mesh") return false;
826
+ if (!stopsEqual(a.stops, b.stops)) return false;
827
+ if (a.interpolation !== b.interpolation) return false;
828
+ if (a.kind === "radial" && b.kind === "radial") return JSON.stringify(a.center) === JSON.stringify(b.center) && a.radius === b.radius;
829
+ if (a.kind === "linear" && b.kind === "linear") return JSON.stringify(a.from) === JSON.stringify(b.from) && JSON.stringify(a.to) === JSON.stringify(b.to);
830
+ return false;
831
+ },
832
+ defaultHandoff: "blend-from-frozen"
833
+ };
834
+ var ValueTypeInferenceError = class extends Error {
835
+ constructor(value) {
836
+ super(`cannot infer a value type for ${JSON.stringify(value)}; register a custom type`);
837
+ this.name = "ValueTypeInferenceError";
838
+ }
839
+ };
840
+ const isContour = (c) => typeof c === "object" && c !== null && typeof c.closed === "boolean" && Array.isArray(c.v) && Array.isArray(c.in) && Array.isArray(c.out);
841
+ const isPaint = (v) => typeof v === "object" && v !== null && (v.kind === "color" || v.kind === "linear" || v.kind === "radial" || v.kind === "mesh");
842
+ /** Infer a registered type id from a sample value (builder + bake authoring surfaces). */
843
+ function inferValueType(value) {
844
+ if (typeof value === "number") return "number";
845
+ if (typeof value === "boolean") return "boolean";
846
+ if (isPaint(value)) return "paint";
847
+ if (Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === "number")) return "vec2";
848
+ if (Array.isArray(value) && value.length > 0 && value.every(isContour)) return "path";
849
+ if (typeof value === "string") try {
850
+ parseColor(value);
851
+ return "color";
852
+ } catch {
853
+ return "string";
854
+ }
855
+ throw new ValueTypeInferenceError(value);
856
+ }
857
+ registerValueType(numberType);
858
+ registerValueType(vec2Type);
859
+ registerValueType(vec2ArcType);
860
+ registerValueType(colorType);
861
+ registerValueType(stringType);
862
+ registerValueType(booleanType);
863
+ registerValueType(pathType);
864
+ registerValueType(paintType);
865
+ registerValueType(fontAxesType);
866
+ //#endregion
867
+ //#region src/track.ts
868
+ /**
869
+ * Track & keyframe model (DESIGN.md §2.2) and sampling (§2.4): binary search
870
+ * per track with a last-segment cursor — sanctioned memoization, semantics
871
+ * identical to a cold search (property-tested).
872
+ */
873
+ let exprCompiler = null;
874
+ /** Register the expr compiler (called on `@glissade/core/expr` import). */
875
+ function setExprCompiler(fn) {
876
+ exprCompiler = fn;
877
+ }
878
+ const NO_EXPR = "expr tracks need `import '@glissade/core/expr'`";
879
+ var TrackValidationError = class extends Error {
880
+ constructor(target, message) {
881
+ super(`track '${target}': ${message}`);
882
+ this.name = "TrackValidationError";
883
+ }
884
+ };
885
+ const TARGET_SHAPE = /^[^/]+\/.+$/;
886
+ const TARGET_MSG = "target must be '<nodeId>/<prop.path>' (e.g. 'circle/opacity')";
887
+ function validateTrack(track) {
888
+ const vt = getValueType(track.type);
889
+ if (!TARGET_SHAPE.test(track.target)) throw new TrackValidationError(track.target, TARGET_MSG);
890
+ if (track.expr !== void 0) {
891
+ if (track.type !== "number") throw new TrackValidationError(track.target, `an expr track must be type 'number' (got '${track.type}')`);
892
+ if (exprCompiler) try {
893
+ exprCompiler(track.expr);
894
+ } catch (e) {
895
+ throw new TrackValidationError(track.target, `invalid expr: ${e instanceof Error ? e.message : String(e)}`);
896
+ }
897
+ return;
898
+ }
899
+ if (track.keys.length === 0) throw new TrackValidationError(track.target, "must have at least one key");
900
+ if (vt.defaultHandoff === "cut") {
901
+ for (const k of track.keys) if (k.interp !== "hold") k.interp = "hold";
902
+ }
903
+ const repr = vt.repr ?? vt.id;
904
+ if (repr === "number" || repr === "vec2") {
905
+ for (const k of track.keys) if (!(repr === "number" ? typeof k.value === "number" && Number.isFinite(k.value) : Array.isArray(k.value) && k.value.length === 2 && k.value.every((n) => typeof n === "number" && Number.isFinite(n)))) {
906
+ const got = typeof k.value === "function" ? "a function (a signal accessor? call it — e.g. node.height(), not node.height)" : typeof k.value === "object" ? JSON.stringify(k.value) : String(k.value);
907
+ throw new TrackValidationError(track.target, `${repr} keyframe at t=${k.t} must be ${repr === "number" ? "a finite number" : "a [x, y] of finite numbers"}, got ${got}`);
908
+ }
909
+ }
910
+ for (let i = 1; i < track.keys.length; i++) {
911
+ const prev = track.keys[i - 1];
912
+ const cur = track.keys[i];
913
+ if (cur.t <= prev.t) throw new TrackValidationError(track.target, `keys must be strictly increasing in t (key[${i}] at t=${cur.t} after t=${prev.t})`);
914
+ }
915
+ }
916
+ function key(t, value, easeOrOpts) {
917
+ if (easeOrOpts === void 0) return {
918
+ t,
919
+ value
920
+ };
921
+ if (typeof easeOrOpts === "string" || "kind" in easeOrOpts) return {
922
+ t,
923
+ value,
924
+ ease: easeOrOpts
925
+ };
926
+ const opts = easeOrOpts;
927
+ const k = {
928
+ t,
929
+ value
930
+ };
931
+ if (opts.ease !== void 0) k.ease = opts.ease;
932
+ if (opts.interp !== void 0) k.interp = opts.interp;
933
+ if (opts.id !== void 0) k.id = opts.id;
934
+ if (opts.derived !== void 0) k.derived = opts.derived;
935
+ return k;
936
+ }
937
+ /**
938
+ * The settle-ON-the-beat helper: a spring key must sit at prev.t +
939
+ * spring.duration(cfg) (§2.7), so beat-anchored authoring otherwise means
940
+ * hand-computing the launch time. springTo returns the [launch, settle] key
941
+ * pair with the arithmetic done — spread it into a raw track():
942
+ * track('x/width', 'number', [...springTo(beats.start('drop'), 0, 320, cfg)])
943
+ */
944
+ function springTo(endT, from, to, cfg) {
945
+ const d = spring.duration(cfg);
946
+ if (endT - d < 0) throw new TrackValidationError("springTo", `this spring needs ${d.toFixed(3)}s to settle — endT must be ≥ its duration (got ${endT})`);
947
+ return [key(endT - d, from), key(endT, to, spring(cfg))];
948
+ }
949
+ /**
950
+ * Cascade a set of tracks by shifting each one's key times — the classic
951
+ * stagger for animating a list of nodes with a delay between them. `delay` is
952
+ * the per-index gap in seconds, or a function of the index for non-linear
953
+ * cascades. Pure: returns new tracks, leaving the inputs untouched.
954
+ *
955
+ * stagger(items.map((it, i) => track(`${it.id}/opacity`, 'number',
956
+ * [key(0, 0), key(0.3, 1, 'easeOutCubic')])), 0.08)
957
+ */
958
+ function stagger(tracks, delay) {
959
+ const at = typeof delay === "function" ? delay : (i) => i * delay;
960
+ return tracks.map((tr, i) => {
961
+ const d = at(i);
962
+ return {
963
+ ...tr,
964
+ keys: tr.keys.map((k) => ({
965
+ ...k,
966
+ t: k.t + d
967
+ }))
968
+ };
969
+ });
970
+ }
971
+ /** Minimal reversed copy of a key schedule: mirror times about [t0,tn], reverse
972
+ * the value order, and time-mirror each segment's ease so it plays identically
973
+ * backward. Throws on `hold` segments (asymmetric — can't reverse cleanly). */
974
+ function reversedKeys(keys) {
975
+ const n = keys.length;
976
+ if (n < 2) return keys.map((k) => ({
977
+ t: k.t,
978
+ value: k.value
979
+ }));
980
+ const t0 = keys[0].t;
981
+ const tn = keys[n - 1].t;
982
+ const out = [];
983
+ for (let j = 0; j < n; j++) {
984
+ const src = keys[n - 1 - j];
985
+ const k = {
986
+ t: t0 + tn - src.t,
987
+ value: src.value
988
+ };
989
+ if (j >= 1) {
990
+ const oldArrival = keys[n - j];
991
+ if (oldArrival.interp === "hold") throw new TrackValidationError("retime", "cannot reverse/pingpong a track with a hold segment (a hold is asymmetric in time) — retime it with { speed } / { shift }, or author the reversed schedule explicitly");
992
+ const m = mirrorEase(oldArrival.ease);
993
+ if (m !== void 0) k.ease = m;
994
+ }
995
+ out.push(k);
996
+ }
997
+ return out;
998
+ }
999
+ /**
1000
+ * Retime a set of tracks by remapping their key TIMES — slow-mo/fast (`speed`),
1001
+ * delay/advance (`shift`), `reverse`, or `pingpong` — as a pure build-time
1002
+ * transform. Because it rewrites the schedule into an ordinary `Track[]` (no
1003
+ * runtime clock warp, no cross-frame state), evaluate() stays a pure function of
1004
+ * time and the result is golden-stable and O(log keys) scrubbable like any doc.
1005
+ * Reverse/pingpong time-mirror each segment's ease exactly for the built-in
1006
+ * eases and cubicBezier; springs and hold segments fail loud (they're causal /
1007
+ * asymmetric). Returns NEW tracks; the inputs are untouched.
1008
+ *
1009
+ * retime(move, { speed: 0.5 }) // half speed
1010
+ * retime(move, { reverse: true }) // play it backward
1011
+ * retime(move, { pingpong: true }) // there and back
1012
+ */
1013
+ function retime(tracks, spec) {
1014
+ const speed = spec.speed ?? 1;
1015
+ if (!(speed > 0) || !Number.isFinite(speed)) throw new TrackValidationError("retime", `speed must be a finite number > 0 (got ${speed})`);
1016
+ if (spec.reverse && spec.pingpong) throw new TrackValidationError("retime", "pass reverse OR pingpong, not both");
1017
+ const shift = spec.shift ?? 0;
1018
+ return tracks.map((tr) => {
1019
+ let keys = tr.keys.map((k) => ({
1020
+ ...k,
1021
+ t: k.t / speed
1022
+ }));
1023
+ if (spec.reverse) keys = reversedKeys(keys);
1024
+ if (spec.pingpong && keys.length >= 2) {
1025
+ const t0 = keys[0].t;
1026
+ const tn = keys[keys.length - 1].t;
1027
+ const back = reversedKeys(keys).map((k) => ({
1028
+ ...k,
1029
+ t: k.t + (tn - t0)
1030
+ }));
1031
+ keys = [...keys, ...back.slice(1)];
1032
+ }
1033
+ if (shift !== 0) keys = keys.map((k) => ({
1034
+ ...k,
1035
+ t: k.t + shift
1036
+ }));
1037
+ return {
1038
+ ...tr,
1039
+ keys
1040
+ };
1041
+ });
1042
+ }
1043
+ function track(target, type, keys, opts) {
1044
+ const tr = {
1045
+ target,
1046
+ type,
1047
+ keys
1048
+ };
1049
+ if (opts?.editable !== void 0) tr.editable = opts.editable;
1050
+ validateTrack(tr);
1051
+ return tr;
1052
+ }
1053
+ /**
1054
+ * Expr (0.40): build a raw formula-driven numeric track — `{ target, type:
1055
+ * 'number', keys: [], expr }`. The public `exprTrack()` (on `@glissade/core/expr`,
1056
+ * which also registers the evaluator) wraps this + validates. Kept here (base) so
1057
+ * `tl.expr` can emit a track without dragging the evaluator onto the embed.
1058
+ */
1059
+ function makeExprTrack(target, formula) {
1060
+ const tr = {
1061
+ target,
1062
+ type: "number",
1063
+ keys: [],
1064
+ expr: formula
1065
+ };
1066
+ validateTrack(tr);
1067
+ return tr;
1068
+ }
1069
+ function resolveEase(spec) {
1070
+ if (spec === void 0) return namedEasing("linear");
1071
+ if (typeof spec === "string") return namedEasing(spec);
1072
+ if (spec.kind === "cubicBezier") return cubicBezier(...spec.pts);
1073
+ return springEasing(spec);
1074
+ }
1075
+ const warnedNumericDerivative = /* @__PURE__ */ new Set();
1076
+ /**
1077
+ * Analytic d(u) for an ease spec (§B.6). Custom-registered eases without a
1078
+ * derivative fall back to a symmetric difference with a one-time dev warning.
1079
+ */
1080
+ function resolveEaseDerivative(spec) {
1081
+ if (spec === void 0) return easingDerivatives["linear"];
1082
+ if (typeof spec === "string") {
1083
+ const d = easingDerivatives[spec];
1084
+ if (d) return d;
1085
+ const fn = namedEasing(spec);
1086
+ if (!warnedNumericDerivative.has(spec)) {
1087
+ warnedNumericDerivative.add(spec);
1088
+ emitDevWarning(`easing '${spec}' has no registered derivative; velocity uses a numeric fallback — register one in easingDerivatives for exact interruption handoff`);
1089
+ }
1090
+ const h = 1 / 1024;
1091
+ return (u) => (fn(Math.min(1, u + h)) - fn(Math.max(0, u - h))) / (Math.min(1, u + h) - Math.max(0, u - h));
1092
+ }
1093
+ if (spec.kind === "cubicBezier") return cubicBezierDerivative(...spec.pts);
1094
+ return springEasingDerivative(spec);
1095
+ }
1096
+ const samplerStates = /* @__PURE__ */ new WeakMap();
1097
+ function state(tr) {
1098
+ let s = samplerStates.get(tr);
1099
+ if (!s) {
1100
+ s = {
1101
+ cursor: 1,
1102
+ easeCache: new Array(tr.keys.length)
1103
+ };
1104
+ samplerStates.set(tr, s);
1105
+ }
1106
+ return s;
1107
+ }
1108
+ function easeFor(tr, s, i) {
1109
+ let fn = s.easeCache[i];
1110
+ if (!fn) {
1111
+ fn = resolveEase(tr.keys[i].ease);
1112
+ s.easeCache[i] = fn;
1113
+ }
1114
+ return fn;
1115
+ }
1116
+ /**
1117
+ * Find i such that keys[i-1].t <= t < keys[i].t, i.e. the index of the
1118
+ * arrival key of the segment containing t. Returns 0 if t < first key,
1119
+ * keys.length if t >= last key.
1120
+ */
1121
+ function findSegment(keys, t, hint) {
1122
+ const n = keys.length;
1123
+ for (let i = Math.max(1, hint - 1); i <= Math.min(n - 1, hint + 1); i++) if (keys[i - 1].t <= t && t < keys[i].t) return i;
1124
+ if (t < keys[0].t) return 0;
1125
+ if (t >= keys[n - 1].t) return n;
1126
+ let lo = 1;
1127
+ let hi = n - 1;
1128
+ while (lo < hi) {
1129
+ const mid = lo + hi >> 1;
1130
+ if (keys[mid].t <= t) lo = mid + 1;
1131
+ else hi = mid;
1132
+ }
1133
+ return lo;
1134
+ }
1135
+ /**
1136
+ * Analytic track derivative at time t, in value-units per second of local
1137
+ * track time (v2 addendum §B.3/§B.6 conventions, pinned):
1138
+ * (a) at a key boundary, velocity is the RIGHT derivative;
1139
+ * (b) hold segments and the clamped regions outside the keys have v = 0;
1140
+ * (c) types without sub/scale operators return null (no kinetic velocity).
1141
+ */
1142
+ function velocityAt(tr, t) {
1143
+ const vt = getValueType(tr.type);
1144
+ if (!vt.sub || !vt.scale) return null;
1145
+ const keys = tr.keys;
1146
+ const n = keys.length;
1147
+ const s = state(tr);
1148
+ const i = findSegment(keys, t, s.cursor);
1149
+ const zero = vt.scale(vt.sub(keys[0].value, keys[0].value), 0);
1150
+ if (i === 0 || i >= n) return zero;
1151
+ const arrival = keys[i];
1152
+ if (arrival.interp === "hold") return zero;
1153
+ const prev = keys[i - 1];
1154
+ const segDur = arrival.t - prev.t;
1155
+ const p = (t - prev.t) / segDur;
1156
+ if (!vt.extrapolates) {
1157
+ const eased = easeFor(tr, s, i)(p);
1158
+ if (eased < 0 || eased > 1) return zero;
1159
+ }
1160
+ const d = resolveEaseDerivative(arrival.ease)(p);
1161
+ return vt.scale(vt.sub(arrival.value, prev.value), d / segDur);
1162
+ }
1163
+ /** Pure sample of a track at time t (§2.4). */
1164
+ function sampleTrack(tr, t) {
1165
+ if (tr.expr !== void 0) {
1166
+ const s = state(tr);
1167
+ if (!exprCompiler) throw new TrackValidationError(tr.target, NO_EXPR);
1168
+ s.exprEval ??= exprCompiler(tr.expr);
1169
+ return s.exprEval(t);
1170
+ }
1171
+ const keys = tr.keys;
1172
+ const n = keys.length;
1173
+ const s = state(tr);
1174
+ const i = findSegment(keys, t, s.cursor);
1175
+ if (i === 0) return keys[0].value;
1176
+ if (i >= n) return keys[n - 1].value;
1177
+ s.cursor = i;
1178
+ const arrival = keys[i];
1179
+ const prev = keys[i - 1];
1180
+ if (arrival.interp === "hold") return prev.value;
1181
+ const vt = getValueType(tr.type);
1182
+ const p = (t - prev.t) / (arrival.t - prev.t);
1183
+ let easedT = easeFor(tr, s, i)(p);
1184
+ if (!vt.extrapolates && (easedT < 0 || easedT > 1)) {
1185
+ if (!s.warnedClamp) {
1186
+ s.warnedClamp = true;
1187
+ emitDevWarning(`track '${tr.target}' (type '${tr.type}') clamped an out-of-range eased value — non-extrapolating types can't overshoot, so a spring/overshooting ease on this track is flattened`);
1188
+ }
1189
+ easedT = Math.min(1, Math.max(0, easedT));
1190
+ }
1191
+ return vt.lerp(prev.value, arrival.value, easedT);
1192
+ }
1193
+ //#endregion
1194
+ export { vec2Type as A, springEasing as B, paintType as C, stringType as D, reprOf as E, parseColor as F, cubicBezier as G, springPresets as H, rgbaToOklab as I, easings as J, cubicBezierDerivative as K, emitDevWarning as L, formatColor as M, lerpColor as N, vec2ArcType as O, oklabToRgba as P, setDevWarning as R, numberType as S, registerValueType as T, DEFAULT_EASE as U, springEasingDerivative as V, UnknownEasingError as W, namedEasing as Y, colorType as _, resolveEaseDerivative as a, inferValueType as b, setExprCompiler as c, track as d, validateTrack as f, booleanType as g, ValueTypeInferenceError as h, resolveEase as i, ColorParseError as j, vec2Equals as k, springTo as l, UnknownValueTypeError as m, key as n, retime as o, velocityAt as p, easingDerivatives as q, makeExprTrack as r, sampleTrack as s, TrackValidationError as t, stagger as u, fontAxesType as v, pathType as w, listValueTypes as x, getValueType as y, spring as z };