@glissade/core 0.8.1-pre.1 → 0.9.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/index.d.ts +79 -442
- package/dist/index.js +253 -1180
- package/dist/sidecar.d.ts +510 -0
- package/dist/sidecar.js +1268 -0
- package/dist/studioHost.d.ts +124 -0
- package/dist/studioHost.js +232 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $ as vec2Equals, A as velocityAt, B as easings, C as resolveEase, D as stagger, E as springTo, F as DEFAULT_EASE, G as colorType, H as UnknownValueTypeError, I as UnknownEasingError, J as numberType, K as getValueType, L as cubicBezier, M as springEasing, N as springEasingDerivative, O as track, P as springPresets, Q as vec2ArcType, R as cubicBezierDerivative, S as key, T as sampleTrack, U as ValueTypeInferenceError, V as namedEasing, W as booleanType, X as registerValueType, Y as pathType, Z as stringType, _ as audioOffsetSamples, a as hashKeys, at as lerpColor, b as timeline$1, c as migrateSidecar, ct as rgbaToOklab, d as TARGET_PATH, et as vec2Type, f as UnresolvableTargetError, g as TimelineValidationError, h as targetNodeId, i as emptySidecar, it as formatColor, j as spring, k as validateTrack, l as normalizeEditedKeys, m as resolveTweenTarget, n as assignKeyIds, nt as setDevWarning, o as mergeSidecar, ot as oklabToRgba, p as isEditableNodeId, q as inferValueType, r as deleteSidecarTrack, rt as ColorParseError, s as mergeSidecarDetailed, st as parseColor, t as SidecarVersionError, tt as emitDevWarning, u as setSidecarTrack, v as compileTimeline, w as resolveEaseDerivative, x as TrackValidationError, y as isDurationEditable, z as easingDerivatives } from "./sidecar.js";
|
|
1
2
|
//#region src/signal.ts
|
|
2
3
|
let activeConsumer = null;
|
|
3
4
|
let readPhaseDepth = 0;
|
|
@@ -194,271 +195,6 @@ function untracked(fn) {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
//#endregion
|
|
197
|
-
//#region src/color.ts
|
|
198
|
-
const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
199
|
-
const RGB_RE = /^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*(?:[,/]\s*([\d.]+%?)\s*)?\)$/i;
|
|
200
|
-
var ColorParseError = class extends Error {
|
|
201
|
-
constructor(input) {
|
|
202
|
-
super(`cannot parse color '${input}' (supported: #rgb[a], #rrggbb[aa], rgb()/rgba())`);
|
|
203
|
-
this.name = "ColorParseError";
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
function parseColor(input) {
|
|
207
|
-
const hex = HEX_RE.exec(input);
|
|
208
|
-
if (hex) {
|
|
209
|
-
let h = hex[1];
|
|
210
|
-
if (h.length <= 4) h = [...h].map((c) => c + c).join("");
|
|
211
|
-
const n = parseInt(h, 16);
|
|
212
|
-
if (h.length === 6) return {
|
|
213
|
-
r: n >> 16,
|
|
214
|
-
g: n >> 8 & 255,
|
|
215
|
-
b: n & 255,
|
|
216
|
-
a: 1
|
|
217
|
-
};
|
|
218
|
-
return {
|
|
219
|
-
r: n >>> 24 & 255,
|
|
220
|
-
g: n >>> 16 & 255,
|
|
221
|
-
b: n >>> 8 & 255,
|
|
222
|
-
a: (n & 255) / 255
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
const rgb = RGB_RE.exec(input);
|
|
226
|
-
if (rgb) {
|
|
227
|
-
const alphaRaw = rgb[4];
|
|
228
|
-
const a = alphaRaw === void 0 ? 1 : alphaRaw.endsWith("%") ? parseFloat(alphaRaw) / 100 : parseFloat(alphaRaw);
|
|
229
|
-
return {
|
|
230
|
-
r: parseFloat(rgb[1]),
|
|
231
|
-
g: parseFloat(rgb[2]),
|
|
232
|
-
b: parseFloat(rgb[3]),
|
|
233
|
-
a
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
throw new ColorParseError(input);
|
|
237
|
-
}
|
|
238
|
-
function formatColor(c) {
|
|
239
|
-
const clampByte = (v) => Math.min(255, Math.max(0, Math.round(v)));
|
|
240
|
-
const hex = (v) => clampByte(v).toString(16).padStart(2, "0");
|
|
241
|
-
const base = `#${hex(c.r)}${hex(c.g)}${hex(c.b)}`;
|
|
242
|
-
const a = Math.min(1, Math.max(0, c.a));
|
|
243
|
-
return a >= 1 ? base : `${base}${hex(a * 255)}`;
|
|
244
|
-
}
|
|
245
|
-
function srgbToLinear(c) {
|
|
246
|
-
const v = c / 255;
|
|
247
|
-
return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
|
|
248
|
-
}
|
|
249
|
-
function linearToSrgb(v) {
|
|
250
|
-
return (v <= .0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - .055) * 255;
|
|
251
|
-
}
|
|
252
|
-
function rgbaToOklab(c) {
|
|
253
|
-
const r = srgbToLinear(c.r);
|
|
254
|
-
const g = srgbToLinear(c.g);
|
|
255
|
-
const b = srgbToLinear(c.b);
|
|
256
|
-
const l = Math.cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * b);
|
|
257
|
-
const m = Math.cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * b);
|
|
258
|
-
const s = Math.cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * b);
|
|
259
|
-
return {
|
|
260
|
-
L: .2104542553 * l + .793617785 * m - .0040720468 * s,
|
|
261
|
-
a: 1.9779984951 * l - 2.428592205 * m + .4505937099 * s,
|
|
262
|
-
b: .0259040371 * l + .7827717662 * m - .808675766 * s,
|
|
263
|
-
alpha: c.a
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
function oklabToRgba(c) {
|
|
267
|
-
const l = (c.L + .3963377774 * c.a + .2158037573 * c.b) ** 3;
|
|
268
|
-
const m = (c.L - .1055613458 * c.a - .0638541728 * c.b) ** 3;
|
|
269
|
-
const s = (c.L - .0894841775 * c.a - 1.291485548 * c.b) ** 3;
|
|
270
|
-
return {
|
|
271
|
-
r: linearToSrgb(4.0767416621 * l - 3.3077115913 * m + .2309699292 * s),
|
|
272
|
-
g: linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - .3413193965 * s),
|
|
273
|
-
b: linearToSrgb(-.0041960863 * l - .7034186147 * m + 1.707614701 * s),
|
|
274
|
-
a: c.alpha
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
/** Interpolate two CSS color strings in OKLab; t may extrapolate. */
|
|
278
|
-
function lerpColor(from, to, t) {
|
|
279
|
-
const a = rgbaToOklab(parseColor(from));
|
|
280
|
-
const b = rgbaToOklab(parseColor(to));
|
|
281
|
-
const mix = (x, y) => x + (y - x) * t;
|
|
282
|
-
return formatColor(oklabToRgba({
|
|
283
|
-
L: mix(a.L, b.L),
|
|
284
|
-
a: mix(a.a, b.a),
|
|
285
|
-
b: mix(a.b, b.b),
|
|
286
|
-
alpha: mix(a.alpha, b.alpha)
|
|
287
|
-
}));
|
|
288
|
-
}
|
|
289
|
-
//#endregion
|
|
290
|
-
//#region src/devWarning.ts
|
|
291
|
-
let devWarn = (msg) => {
|
|
292
|
-
globalThis.console?.warn(`[glissade] ${msg}`);
|
|
293
|
-
};
|
|
294
|
-
function setDevWarning(fn) {
|
|
295
|
-
devWarn = fn;
|
|
296
|
-
}
|
|
297
|
-
/** Internal: emit through the configurable channel. */
|
|
298
|
-
function emitDevWarning(message) {
|
|
299
|
-
devWarn(message);
|
|
300
|
-
}
|
|
301
|
-
//#endregion
|
|
302
|
-
//#region src/valueTypes.ts
|
|
303
|
-
/**
|
|
304
|
-
* Value-type registry with pluggable per-type interpolation (DESIGN.md §2.2).
|
|
305
|
-
* `extrapolates` declares whether a type's lerp accepts easedT outside [0,1]
|
|
306
|
-
* (spring overshoot); non-extrapolating types clamp.
|
|
307
|
-
*/
|
|
308
|
-
const registry = /* @__PURE__ */ new Map();
|
|
309
|
-
function registerValueType(vt) {
|
|
310
|
-
registry.set(vt.id, vt);
|
|
311
|
-
}
|
|
312
|
-
var UnknownValueTypeError = class extends Error {
|
|
313
|
-
constructor(id) {
|
|
314
|
-
super(`unknown value type '${id}'; register it via registerValueType()`);
|
|
315
|
-
this.name = "UnknownValueTypeError";
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
function getValueType(id) {
|
|
319
|
-
const vt = registry.get(id);
|
|
320
|
-
if (!vt) throw new UnknownValueTypeError(id);
|
|
321
|
-
return vt;
|
|
322
|
-
}
|
|
323
|
-
const numberType = {
|
|
324
|
-
id: "number",
|
|
325
|
-
lerp: (a, b, t) => a + (b - a) * t,
|
|
326
|
-
extrapolates: true,
|
|
327
|
-
equals: Object.is,
|
|
328
|
-
add: (a, b) => a + b,
|
|
329
|
-
sub: (a, b) => a - b,
|
|
330
|
-
scale: (a, k) => a * k,
|
|
331
|
-
defaultHandoff: "spring"
|
|
332
|
-
};
|
|
333
|
-
const vec2Equals = (a, b) => a[0] === b[0] && a[1] === b[1];
|
|
334
|
-
const vec2Type = {
|
|
335
|
-
id: "vec2",
|
|
336
|
-
lerp: (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
|
|
337
|
-
extrapolates: true,
|
|
338
|
-
equals: vec2Equals,
|
|
339
|
-
add: (a, b) => [a[0] + b[0], a[1] + b[1]],
|
|
340
|
-
sub: (a, b) => [a[0] - b[0], a[1] - b[1]],
|
|
341
|
-
scale: (a, k) => [a[0] * k, a[1] * k],
|
|
342
|
-
defaultHandoff: "spring"
|
|
343
|
-
};
|
|
344
|
-
/** vec2 swept along a circular arc: polar lerp of radius + shortest-path angle (§2.2). */
|
|
345
|
-
const vec2ArcType = {
|
|
346
|
-
id: "vec2-arc",
|
|
347
|
-
lerp: (a, b, t) => {
|
|
348
|
-
const ra = Math.hypot(a[0], a[1]);
|
|
349
|
-
const rb = Math.hypot(b[0], b[1]);
|
|
350
|
-
const angA = Math.atan2(a[1], a[0]);
|
|
351
|
-
let dAng = Math.atan2(b[1], b[0]) - angA;
|
|
352
|
-
while (dAng > Math.PI) dAng -= 2 * Math.PI;
|
|
353
|
-
while (dAng < -Math.PI) dAng += 2 * Math.PI;
|
|
354
|
-
const r = ra + (rb - ra) * t;
|
|
355
|
-
const ang = angA + dAng * t;
|
|
356
|
-
return [r * Math.cos(ang), r * Math.sin(ang)];
|
|
357
|
-
},
|
|
358
|
-
extrapolates: true,
|
|
359
|
-
equals: vec2Equals,
|
|
360
|
-
defaultHandoff: "blend-from-frozen"
|
|
361
|
-
};
|
|
362
|
-
const colorType = {
|
|
363
|
-
id: "color",
|
|
364
|
-
lerp: lerpColor,
|
|
365
|
-
extrapolates: true,
|
|
366
|
-
equals: (a, b) => a === b,
|
|
367
|
-
defaultHandoff: "blend-from-frozen"
|
|
368
|
-
};
|
|
369
|
-
/** Discrete types: hold-only by construction (§2.2); lerp snaps at t=1. */
|
|
370
|
-
function discrete(id) {
|
|
371
|
-
return {
|
|
372
|
-
id,
|
|
373
|
-
lerp: (a, b, t) => t >= 1 ? b : a,
|
|
374
|
-
extrapolates: false,
|
|
375
|
-
equals: (a, b) => Object.is(a, b),
|
|
376
|
-
defaultHandoff: "cut"
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
const stringType = discrete("string");
|
|
380
|
-
const booleanType = discrete("boolean");
|
|
381
|
-
const lerpV = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
382
|
-
/** Topology must match for a morph (contour count, closed flags, vertex counts). */
|
|
383
|
-
function pathTopologyMatches(a, b) {
|
|
384
|
-
if (a.length !== b.length) return false;
|
|
385
|
-
for (let i = 0; i < a.length; i++) {
|
|
386
|
-
const ca = a[i];
|
|
387
|
-
const cb = b[i];
|
|
388
|
-
if (ca.closed !== cb.closed || ca.v.length !== cb.v.length) return false;
|
|
389
|
-
}
|
|
390
|
-
return true;
|
|
391
|
-
}
|
|
392
|
-
let warnedPathTopology = false;
|
|
393
|
-
/**
|
|
394
|
-
* Path morphing (§2.2): pairwise lerp of anchors and tangents — exactly how
|
|
395
|
-
* lottie-web morphs, so imported animations are pixel-faithful. Mismatched
|
|
396
|
-
* topology snaps (hold a, then b at t ≥ 1) with a one-time dev warning; the
|
|
397
|
-
* de Casteljau normalization fallback for arbitrary native morphs is tracked
|
|
398
|
-
* future work. Lerp-only: offsets are not well-defined under mismatched
|
|
399
|
-
* topology, so no add/sub/scale — handoffs blend from the frozen value.
|
|
400
|
-
*/
|
|
401
|
-
const pathType = {
|
|
402
|
-
id: "path",
|
|
403
|
-
lerp: (a, b, t) => {
|
|
404
|
-
if (!pathTopologyMatches(a, b)) {
|
|
405
|
-
if (!warnedPathTopology) {
|
|
406
|
-
warnedPathTopology = true;
|
|
407
|
-
emitDevWarning("path lerp with mismatched topology (contour/vertex counts or closed flags differ): snapping instead of morphing — supply matched vertex counts (§2.2)");
|
|
408
|
-
}
|
|
409
|
-
return t >= 1 ? b : a;
|
|
410
|
-
}
|
|
411
|
-
return a.map((ca, i) => {
|
|
412
|
-
const cb = b[i];
|
|
413
|
-
return {
|
|
414
|
-
closed: ca.closed,
|
|
415
|
-
v: ca.v.map((p, j) => lerpV(p, cb.v[j], t)),
|
|
416
|
-
in: ca.in.map((p, j) => lerpV(p, cb.in[j], t)),
|
|
417
|
-
out: ca.out.map((p, j) => lerpV(p, cb.out[j], t))
|
|
418
|
-
};
|
|
419
|
-
});
|
|
420
|
-
},
|
|
421
|
-
extrapolates: false,
|
|
422
|
-
equals: (a, b) => {
|
|
423
|
-
if (a === b) return true;
|
|
424
|
-
if (!pathTopologyMatches(a, b)) return false;
|
|
425
|
-
const eq = (x, y) => x[0] === y[0] && x[1] === y[1];
|
|
426
|
-
return a.every((ca, i) => {
|
|
427
|
-
const cb = b[i];
|
|
428
|
-
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]));
|
|
429
|
-
});
|
|
430
|
-
},
|
|
431
|
-
defaultHandoff: "blend-from-frozen"
|
|
432
|
-
};
|
|
433
|
-
var ValueTypeInferenceError = class extends Error {
|
|
434
|
-
constructor(value) {
|
|
435
|
-
super(`cannot infer a value type for ${JSON.stringify(value)}; register a custom type`);
|
|
436
|
-
this.name = "ValueTypeInferenceError";
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
const isContour = (c) => typeof c === "object" && c !== null && typeof c.closed === "boolean" && Array.isArray(c.v) && Array.isArray(c.in) && Array.isArray(c.out);
|
|
440
|
-
/** Infer a registered type id from a sample value (builder + bake authoring surfaces). */
|
|
441
|
-
function inferValueType(value) {
|
|
442
|
-
if (typeof value === "number") return "number";
|
|
443
|
-
if (typeof value === "boolean") return "boolean";
|
|
444
|
-
if (Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === "number")) return "vec2";
|
|
445
|
-
if (Array.isArray(value) && value.length > 0 && value.every(isContour)) return "path";
|
|
446
|
-
if (typeof value === "string") try {
|
|
447
|
-
parseColor(value);
|
|
448
|
-
return "color";
|
|
449
|
-
} catch {
|
|
450
|
-
return "string";
|
|
451
|
-
}
|
|
452
|
-
throw new ValueTypeInferenceError(value);
|
|
453
|
-
}
|
|
454
|
-
registerValueType(numberType);
|
|
455
|
-
registerValueType(vec2Type);
|
|
456
|
-
registerValueType(vec2ArcType);
|
|
457
|
-
registerValueType(colorType);
|
|
458
|
-
registerValueType(stringType);
|
|
459
|
-
registerValueType(booleanType);
|
|
460
|
-
registerValueType(pathType);
|
|
461
|
-
//#endregion
|
|
462
198
|
//#region src/vec2Signal.ts
|
|
463
199
|
/**
|
|
464
200
|
* Compound Vec2 signal (DESIGN.md §2.1): sub-signals are real signals, the
|
|
@@ -505,757 +241,289 @@ function vec2Signal(initial) {
|
|
|
505
241
|
return sig;
|
|
506
242
|
}
|
|
507
243
|
//#endregion
|
|
508
|
-
//#region src/
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
const easings = {
|
|
523
|
-
linear: (t) => t,
|
|
524
|
-
easeInQuad: (t) => t * t,
|
|
525
|
-
easeOutQuad: (t) => 1 - (1 - t) * (1 - t),
|
|
526
|
-
easeInOutQuad: (t) => t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2,
|
|
527
|
-
easeInCubic: (t) => t ** 3,
|
|
528
|
-
easeOutCubic: (t) => 1 - (1 - t) ** 3,
|
|
529
|
-
easeInOutCubic: (t) => t < .5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2,
|
|
530
|
-
easeInQuart: (t) => t ** 4,
|
|
531
|
-
easeOutQuart: (t) => 1 - (1 - t) ** 4,
|
|
532
|
-
easeInOutQuart: (t) => t < .5 ? 8 * t ** 4 : 1 - (-2 * t + 2) ** 4 / 2,
|
|
533
|
-
easeInQuint: (t) => t ** 5,
|
|
534
|
-
easeOutQuint: (t) => 1 - (1 - t) ** 5,
|
|
535
|
-
easeInOutQuint: (t) => t < .5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2,
|
|
536
|
-
easeInSine: (t) => 1 - Math.cos(t * Math.PI / 2),
|
|
537
|
-
easeOutSine: (t) => Math.sin(t * Math.PI / 2),
|
|
538
|
-
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
539
|
-
easeInExpo: (t) => t === 0 ? 0 : 2 ** (10 * t - 10),
|
|
540
|
-
easeOutExpo: (t) => t === 1 ? 1 : 1 - 2 ** (-10 * t),
|
|
541
|
-
easeInOutExpo: (t) => t === 0 ? 0 : t === 1 ? 1 : t < .5 ? 2 ** (20 * t - 10) / 2 : (2 - 2 ** (-20 * t + 10)) / 2,
|
|
542
|
-
easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
|
|
543
|
-
easeOutCirc: (t) => Math.sqrt(1 - (t - 1) * (t - 1)),
|
|
544
|
-
easeInOutCirc: (t) => t < .5 ? (1 - Math.sqrt(1 - (2 * t) ** 2)) / 2 : (Math.sqrt(1 - (-2 * t + 2) ** 2) + 1) / 2,
|
|
545
|
-
easeInBack: (t) => c3 * t ** 3 - c1 * t * t,
|
|
546
|
-
easeOutBack: (t) => 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2,
|
|
547
|
-
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,
|
|
548
|
-
easeInElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : -(2 ** (10 * t - 10)) * Math.sin((t * 10 - 10.75) * c4),
|
|
549
|
-
easeOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - .75) * c4) + 1,
|
|
550
|
-
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,
|
|
551
|
-
easeInBounce: (t) => 1 - bounceOut(1 - t),
|
|
552
|
-
easeOutBounce: bounceOut,
|
|
553
|
-
easeInOutBounce: (t) => t < .5 ? (1 - bounceOut(1 - 2 * t)) / 2 : (1 + bounceOut(2 * t - 1)) / 2
|
|
554
|
-
};
|
|
555
|
-
function bounceOutD(t) {
|
|
556
|
-
const n1 = 7.5625;
|
|
557
|
-
const d1 = 2.75;
|
|
558
|
-
if (t < 1 / d1) return 2 * n1 * t;
|
|
559
|
-
if (t < 2 / d1) return 2 * n1 * (t - 1.5 / d1);
|
|
560
|
-
if (t < 2.5 / d1) return 2 * n1 * (t - 2.25 / d1);
|
|
561
|
-
return 2 * n1 * (t - 2.625 / d1);
|
|
244
|
+
//#region src/fontRegistry.ts
|
|
245
|
+
function facesOf(family, ref) {
|
|
246
|
+
if (ref.faces && ref.faces.length > 0) return ref.faces.map((f) => ({
|
|
247
|
+
family,
|
|
248
|
+
url: f.url,
|
|
249
|
+
weight: f.weight ?? 400,
|
|
250
|
+
style: f.style ?? "normal"
|
|
251
|
+
}));
|
|
252
|
+
return [{
|
|
253
|
+
family,
|
|
254
|
+
url: ref.url,
|
|
255
|
+
weight: 400,
|
|
256
|
+
style: "normal"
|
|
257
|
+
}];
|
|
562
258
|
}
|
|
563
|
-
const LN2 = Math.LN2;
|
|
564
259
|
/**
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
260
|
+
* CSS Fonts §5.2 nearest-weight: among candidates (already filtered to the
|
|
261
|
+
* desired style when possible), pick the closest weight. Ties below the target
|
|
262
|
+
* prefer the lighter; the standard's directional preference (≤500 search down
|
|
263
|
+
* first, ≥500 search up first) collapses to "closest, lighter on a tie" here,
|
|
264
|
+
* which is sufficient for the small, explicit face sets glissade documents
|
|
265
|
+
* carry.
|
|
568
266
|
*/
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
easeInQuart: (t) => 4 * t ** 3,
|
|
578
|
-
easeOutQuart: (t) => 4 * (1 - t) ** 3,
|
|
579
|
-
easeInOutQuart: (t) => t < .5 ? 32 * t ** 3 : 32 * (1 - t) ** 3,
|
|
580
|
-
easeInQuint: (t) => 5 * t ** 4,
|
|
581
|
-
easeOutQuint: (t) => 5 * (1 - t) ** 4,
|
|
582
|
-
easeInOutQuint: (t) => t < .5 ? 80 * t ** 4 : 80 * (1 - t) ** 4,
|
|
583
|
-
easeInSine: (t) => Math.PI / 2 * Math.sin(t * Math.PI / 2),
|
|
584
|
-
easeOutSine: (t) => Math.PI / 2 * Math.cos(t * Math.PI / 2),
|
|
585
|
-
easeInOutSine: (t) => Math.PI / 2 * Math.sin(Math.PI * t),
|
|
586
|
-
easeInExpo: (t) => t === 0 ? 0 : 10 * LN2 * 2 ** (10 * t - 10),
|
|
587
|
-
easeOutExpo: (t) => t === 1 ? 0 : 10 * LN2 * 2 ** (-10 * t),
|
|
588
|
-
easeInOutExpo: (t) => t === 0 || t === 1 ? 0 : t < .5 ? 10 * LN2 * 2 ** (20 * t - 10) : 10 * LN2 * 2 ** (-20 * t + 10),
|
|
589
|
-
easeInCirc: (t) => t / Math.sqrt(1 - t * t),
|
|
590
|
-
easeOutCirc: (t) => (1 - t) / Math.sqrt(1 - (t - 1) * (t - 1)),
|
|
591
|
-
easeInOutCirc: (t) => t < .5 ? 2 * t / Math.sqrt(1 - 4 * t * t) : (2 - 2 * t) / Math.sqrt(1 - (-2 * t + 2) ** 2),
|
|
592
|
-
easeInBack: (t) => 3 * c3 * t ** 2 - 2 * c1 * t,
|
|
593
|
-
easeOutBack: (t) => 3 * c3 * (t - 1) ** 2 + 2 * c1 * (t - 1),
|
|
594
|
-
easeInOutBack: (t) => t < .5 ? 12 * 3.5949095 * t ** 2 - 4 * c2 * t : 3 * 3.5949095 * (2 * t - 2) ** 2 + 2 * c2 * (2 * t - 2),
|
|
595
|
-
easeInElastic: (t) => {
|
|
596
|
-
if (t === 0 || t === 1) return 0;
|
|
597
|
-
const theta = (t * 10 - 10.75) * c4;
|
|
598
|
-
const amp = 2 ** (10 * t - 10);
|
|
599
|
-
return -(10 * LN2 * amp * Math.sin(theta) + amp * 10 * c4 * Math.cos(theta));
|
|
600
|
-
},
|
|
601
|
-
easeOutElastic: (t) => {
|
|
602
|
-
if (t === 0 || t === 1) return 0;
|
|
603
|
-
const phi = (t * 10 - .75) * c4;
|
|
604
|
-
const amp = 2 ** (-10 * t);
|
|
605
|
-
return -10 * LN2 * amp * Math.sin(phi) + amp * 10 * c4 * Math.cos(phi);
|
|
606
|
-
},
|
|
607
|
-
easeInOutElastic: (t) => {
|
|
608
|
-
if (t === 0 || t === 1) return 0;
|
|
609
|
-
const psi = (20 * t - 11.125) * c5;
|
|
610
|
-
if (t < .5) {
|
|
611
|
-
const amp = 2 ** (20 * t - 10);
|
|
612
|
-
return -(20 * LN2 * amp * Math.sin(psi) + amp * 20 * c5 * Math.cos(psi)) / 2;
|
|
613
|
-
}
|
|
614
|
-
const amp = 2 ** (-20 * t + 10);
|
|
615
|
-
return (-20 * LN2 * amp * Math.sin(psi) + amp * 20 * c5 * Math.cos(psi)) / 2;
|
|
616
|
-
},
|
|
617
|
-
easeInBounce: (t) => bounceOutD(1 - t),
|
|
618
|
-
easeOutBounce: bounceOutD,
|
|
619
|
-
easeInOutBounce: (t) => t < .5 ? bounceOutD(1 - 2 * t) : bounceOutD(2 * t - 1)
|
|
620
|
-
};
|
|
621
|
-
/** Default property-tween ease (Motion Canvas precedent). */
|
|
622
|
-
const DEFAULT_EASE = "easeInOutCubic";
|
|
623
|
-
function bezierKernel(p1x, p1y, p2x, p2y) {
|
|
624
|
-
const ax = 3 * p1x - 3 * p2x + 1;
|
|
625
|
-
const bx = 3 * p2x - 6 * p1x;
|
|
626
|
-
const cx = 3 * p1x;
|
|
627
|
-
const ay = 3 * p1y - 3 * p2y + 1;
|
|
628
|
-
const by = 3 * p2y - 6 * p1y;
|
|
629
|
-
const cy = 3 * p1y;
|
|
630
|
-
const sampleX = (u) => ((ax * u + bx) * u + cx) * u;
|
|
631
|
-
const sampleY = (u) => ((ay * u + by) * u + cy) * u;
|
|
632
|
-
const sampleDX = (u) => (3 * ax * u + 2 * bx) * u + cx;
|
|
633
|
-
const sampleDY = (u) => (3 * ay * u + 2 * by) * u + cy;
|
|
634
|
-
const solveU = (x) => {
|
|
635
|
-
let u = x;
|
|
636
|
-
for (let i = 0; i < 8; i++) {
|
|
637
|
-
const err = sampleX(u) - x;
|
|
638
|
-
if (Math.abs(err) < 1e-7) return u;
|
|
639
|
-
const d = sampleDX(u);
|
|
640
|
-
if (Math.abs(d) < 1e-6) break;
|
|
641
|
-
u -= err / d;
|
|
642
|
-
}
|
|
643
|
-
let lo = 0;
|
|
644
|
-
let hi = 1;
|
|
645
|
-
u = x;
|
|
646
|
-
while (hi - lo > 1e-7) {
|
|
647
|
-
if (sampleX(u) < x) lo = u;
|
|
648
|
-
else hi = u;
|
|
649
|
-
u = (lo + hi) / 2;
|
|
267
|
+
function nearestWeight(faces, weight) {
|
|
268
|
+
let best;
|
|
269
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
270
|
+
for (const f of faces) {
|
|
271
|
+
const dist = Math.abs(f.weight - weight);
|
|
272
|
+
if (dist < bestDist || dist === bestDist && best !== void 0 && f.weight < best.weight) {
|
|
273
|
+
best = f;
|
|
274
|
+
bestDist = dist;
|
|
650
275
|
}
|
|
651
|
-
return u;
|
|
652
|
-
};
|
|
653
|
-
return {
|
|
654
|
-
sampleY,
|
|
655
|
-
sampleDX,
|
|
656
|
-
sampleDY,
|
|
657
|
-
solveU
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* CSS-style cubic bézier where x is time and y is progress. Newton's method
|
|
662
|
-
* with a bisection fallback for the flat-derivative regions.
|
|
663
|
-
*/
|
|
664
|
-
function cubicBezier(p1x, p1y, p2x, p2y) {
|
|
665
|
-
const k = bezierKernel(p1x, p1y, p2x, p2y);
|
|
666
|
-
return (t) => {
|
|
667
|
-
if (t <= 0) return 0;
|
|
668
|
-
if (t >= 1) return 1;
|
|
669
|
-
return k.sampleY(k.solveU(t));
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
/** Analytic dy/dx of a cubic bézier ease: y'(s)/x'(s) at the solved parameter (§B.6). */
|
|
673
|
-
function cubicBezierDerivative(p1x, p1y, p2x, p2y) {
|
|
674
|
-
const k = bezierKernel(p1x, p1y, p2x, p2y);
|
|
675
|
-
return (t) => {
|
|
676
|
-
const u = k.solveU(Math.min(1, Math.max(0, t)));
|
|
677
|
-
const dx = k.sampleDX(u);
|
|
678
|
-
if (Math.abs(dx) < 1e-9) return 0;
|
|
679
|
-
return k.sampleDY(u) / dx;
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
var UnknownEasingError = class extends Error {
|
|
683
|
-
constructor(name) {
|
|
684
|
-
super(`unknown easing '${name}'; register it via easings or use cubicBezier/spring`);
|
|
685
|
-
this.name = "UnknownEasingError";
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
function namedEasing(name) {
|
|
689
|
-
const fn = easings[name];
|
|
690
|
-
if (!fn) throw new UnknownEasingError(name);
|
|
691
|
-
return fn;
|
|
692
|
-
}
|
|
693
|
-
//#endregion
|
|
694
|
-
//#region src/spring.ts
|
|
695
|
-
const DEFAULT_SETTLE_TOLERANCE = .005;
|
|
696
|
-
function params(cfg) {
|
|
697
|
-
const mass = cfg.mass ?? 1;
|
|
698
|
-
if (!(cfg.stiffness > 0) || !(cfg.damping > 0) || !(mass > 0)) throw new RangeError("spring stiffness, damping, and mass must all be > 0");
|
|
699
|
-
return {
|
|
700
|
-
w0: Math.sqrt(cfg.stiffness / mass),
|
|
701
|
-
zeta: cfg.damping / (2 * Math.sqrt(cfg.stiffness * mass))
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
/** Raw closed-form spring velocity at time t — d/dt of rawValue's three branches. */
|
|
705
|
-
function rawDerivative(cfg, t) {
|
|
706
|
-
if (t <= 0) return 0;
|
|
707
|
-
const { w0, zeta } = params(cfg);
|
|
708
|
-
if (Math.abs(zeta - 1) < 1e-9) return w0 * w0 * t * Math.exp(-w0 * t);
|
|
709
|
-
if (zeta < 1) {
|
|
710
|
-
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
711
|
-
return Math.exp(-zeta * w0 * t) * (w0 * w0 / wd) * Math.sin(wd * t);
|
|
712
|
-
}
|
|
713
|
-
const s = Math.sqrt(zeta * zeta - 1);
|
|
714
|
-
const r1 = -w0 * (zeta - s);
|
|
715
|
-
const r2 = -w0 * (zeta + s);
|
|
716
|
-
return r1 * r2 * (Math.exp(r1 * t) - Math.exp(r2 * t)) / (r1 - r2);
|
|
717
|
-
}
|
|
718
|
-
/** Raw closed-form spring position at time t (seconds). Approaches 1, may overshoot. */
|
|
719
|
-
function rawValue(cfg, t) {
|
|
720
|
-
if (t <= 0) return 0;
|
|
721
|
-
const { w0, zeta } = params(cfg);
|
|
722
|
-
if (Math.abs(zeta - 1) < 1e-9) return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
723
|
-
if (zeta < 1) {
|
|
724
|
-
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
725
|
-
return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + zeta * w0 / wd * Math.sin(wd * t));
|
|
726
276
|
}
|
|
727
|
-
|
|
728
|
-
const r1 = -w0 * (zeta - s);
|
|
729
|
-
const r2 = -w0 * (zeta + s);
|
|
730
|
-
return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
|
|
277
|
+
return best;
|
|
731
278
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (zeta < 1) {
|
|
741
|
-
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
742
|
-
const amp = Math.sqrt(1 + (zeta * w0 / wd) ** 2);
|
|
743
|
-
return Math.log(amp / tol) / (zeta * w0);
|
|
744
|
-
}
|
|
745
|
-
let hi = 1 / w0;
|
|
746
|
-
while (1 - rawValue(cfg, hi) > tol) hi *= 2;
|
|
747
|
-
let lo = 0;
|
|
748
|
-
for (let i = 0; i < 64 && hi - lo > 1e-9; i++) {
|
|
749
|
-
const mid = (lo + hi) / 2;
|
|
750
|
-
if (1 - rawValue(cfg, mid) > tol) lo = mid;
|
|
751
|
-
else hi = mid;
|
|
752
|
-
}
|
|
753
|
-
return hi;
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Spring progress at local time t, affinely rescaled so value(duration) = 1
|
|
757
|
-
* exactly — the raw form only approaches 1, and an unscaled curve would snap
|
|
758
|
-
* at the key (§2.7 "endpoint continuity"). May exceed 1 (overshoot).
|
|
759
|
-
*/
|
|
760
|
-
function value(cfg, t, opts) {
|
|
761
|
-
const d = duration(cfg, opts);
|
|
762
|
-
return rawValue(cfg, Math.min(t, d)) / rawValue(cfg, d);
|
|
763
|
-
}
|
|
764
|
-
function retarget(cfg, x0, v0) {
|
|
765
|
-
const { w0, zeta } = params(cfg);
|
|
766
|
-
let value0;
|
|
767
|
-
let velocity0;
|
|
768
|
-
let envelope;
|
|
769
|
-
if (Math.abs(zeta - 1) < 1e-9) {
|
|
770
|
-
const b = v0 + w0 * x0;
|
|
771
|
-
value0 = (tau) => tau <= 0 ? x0 : Math.exp(-w0 * tau) * (x0 + b * tau);
|
|
772
|
-
velocity0 = (tau) => tau <= 0 ? v0 : Math.exp(-w0 * tau) * (v0 - w0 * b * tau);
|
|
773
|
-
envelope = (tau) => Math.exp(-w0 * tau) * (Math.abs(x0) + Math.abs(b) * tau);
|
|
774
|
-
} else if (zeta < 1) {
|
|
775
|
-
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
776
|
-
const c2v = (v0 + zeta * w0 * x0) / wd;
|
|
777
|
-
const amp = Math.hypot(x0, c2v);
|
|
778
|
-
value0 = (tau) => tau <= 0 ? x0 : Math.exp(-zeta * w0 * tau) * (x0 * Math.cos(wd * tau) + c2v * Math.sin(wd * tau));
|
|
779
|
-
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));
|
|
780
|
-
envelope = (tau) => Math.exp(-zeta * w0 * tau) * amp;
|
|
781
|
-
} else {
|
|
782
|
-
const s = Math.sqrt(zeta * zeta - 1);
|
|
783
|
-
const rp = w0 * (-zeta + s);
|
|
784
|
-
const rm = w0 * (-zeta - s);
|
|
785
|
-
const cp = (v0 - rm * x0) / (rp - rm);
|
|
786
|
-
const cm = (rp * x0 - v0) / (rp - rm);
|
|
787
|
-
value0 = (tau) => tau <= 0 ? x0 : cp * Math.exp(rp * tau) + cm * Math.exp(rm * tau);
|
|
788
|
-
velocity0 = (tau) => tau <= 0 ? v0 : cp * rp * Math.exp(rp * tau) + cm * rm * Math.exp(rm * tau);
|
|
789
|
-
envelope = (tau) => Math.abs(cp) * Math.exp(rp * tau) + Math.abs(cm) * Math.exp(rm * tau);
|
|
279
|
+
function buildFontRegistry(assets) {
|
|
280
|
+
const families = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const [family, ref] of Object.entries(assets ?? {})) {
|
|
282
|
+
if (ref.kind !== "font") continue;
|
|
283
|
+
families.set(family, {
|
|
284
|
+
faces: facesOf(family, ref),
|
|
285
|
+
fallback: ref.fallback ? [...ref.fallback] : []
|
|
286
|
+
});
|
|
790
287
|
}
|
|
791
|
-
const settleTime = (tol) => {
|
|
792
|
-
const eps = tol ?? Math.abs(x0) * .005 + 1e-6;
|
|
793
|
-
if (envelope(0) <= eps && Math.abs(v0) < 1e-12) return 0;
|
|
794
|
-
let hi = 1 / w0;
|
|
795
|
-
let guard = 0;
|
|
796
|
-
while (envelope(hi) > eps && guard++ < 64) hi *= 2;
|
|
797
|
-
let lo = 0;
|
|
798
|
-
for (let i = 0; i < 64 && hi - lo > 1e-9; i++) {
|
|
799
|
-
const mid = (lo + hi) / 2;
|
|
800
|
-
if (Math.max(envelope(mid), envelope(mid * 1.05 + 1e-6)) > eps) lo = mid;
|
|
801
|
-
else hi = mid;
|
|
802
|
-
}
|
|
803
|
-
return hi;
|
|
804
|
-
};
|
|
805
288
|
return {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
289
|
+
has(family) {
|
|
290
|
+
return families.has(family);
|
|
291
|
+
},
|
|
292
|
+
faces() {
|
|
293
|
+
const out = [];
|
|
294
|
+
for (const entry of families.values()) out.push(...entry.faces);
|
|
295
|
+
return out;
|
|
296
|
+
},
|
|
297
|
+
resolveFace(family, weight = 400, style = "normal") {
|
|
298
|
+
const entry = families.get(family);
|
|
299
|
+
if (!entry) return void 0;
|
|
300
|
+
const styled = entry.faces.filter((f) => f.style === style);
|
|
301
|
+
return nearestWeight(styled.length > 0 ? styled : entry.faces, weight);
|
|
302
|
+
},
|
|
303
|
+
fallbackChain(family) {
|
|
304
|
+
const entry = families.get(family);
|
|
305
|
+
return entry ? [family, ...entry.fallback] : [family];
|
|
306
|
+
}
|
|
809
307
|
};
|
|
810
308
|
}
|
|
811
|
-
const spring = Object.assign((cfg) => {
|
|
812
|
-
params(cfg);
|
|
813
|
-
return {
|
|
814
|
-
kind: "spring",
|
|
815
|
-
stiffness: cfg.stiffness,
|
|
816
|
-
damping: cfg.damping,
|
|
817
|
-
mass: cfg.mass ?? 1
|
|
818
|
-
};
|
|
819
|
-
}, {
|
|
820
|
-
duration,
|
|
821
|
-
value,
|
|
822
|
-
retarget
|
|
823
|
-
});
|
|
824
|
-
/**
|
|
825
|
-
* Named spring feels (react-spring conventions, mass 1) — a vocabulary instead
|
|
826
|
-
* of hand-tuned stiffness/damping. Use `spring(springPresets.wobbly)` or
|
|
827
|
-
* `springTo(t, a, b, springPresets.gentle)`.
|
|
828
|
-
*/
|
|
829
|
-
const springPresets = {
|
|
830
|
-
default: {
|
|
831
|
-
stiffness: 170,
|
|
832
|
-
damping: 26
|
|
833
|
-
},
|
|
834
|
-
gentle: {
|
|
835
|
-
stiffness: 120,
|
|
836
|
-
damping: 14
|
|
837
|
-
},
|
|
838
|
-
wobbly: {
|
|
839
|
-
stiffness: 180,
|
|
840
|
-
damping: 12
|
|
841
|
-
},
|
|
842
|
-
stiff: {
|
|
843
|
-
stiffness: 210,
|
|
844
|
-
damping: 20
|
|
845
|
-
},
|
|
846
|
-
slow: {
|
|
847
|
-
stiffness: 280,
|
|
848
|
-
damping: 60
|
|
849
|
-
},
|
|
850
|
-
molasses: {
|
|
851
|
-
stiffness: 280,
|
|
852
|
-
damping: 120
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
/**
|
|
856
|
-
* The spring as a normalized easing over a segment whose length must equal
|
|
857
|
-
* spring.duration(cfg) (validated at the document layer, §2.7).
|
|
858
|
-
*/
|
|
859
|
-
function springEasing(cfg) {
|
|
860
|
-
const d = duration(cfg);
|
|
861
|
-
return (p) => value(cfg, p * d);
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Analytic d/dp of springEasing (§B.6): oscillator derivative × the affine
|
|
865
|
-
* rescale factor × duration (chain rule p → t = p·D). Flat past p=1,
|
|
866
|
-
* matching value()'s clamp (right-derivative convention).
|
|
867
|
-
*/
|
|
868
|
-
function springEasingDerivative(cfg) {
|
|
869
|
-
const d = duration(cfg);
|
|
870
|
-
const scale = d / rawValue(cfg, d);
|
|
871
|
-
return (p) => p >= 1 ? 0 : rawDerivative(cfg, p * d) * scale;
|
|
872
|
-
}
|
|
873
309
|
//#endregion
|
|
874
|
-
//#region src/
|
|
875
|
-
/**
|
|
876
|
-
* Track & keyframe model (DESIGN.md §2.2) and sampling (§2.4): binary search
|
|
877
|
-
* per track with a last-segment cursor — sanctioned memoization, semantics
|
|
878
|
-
* identical to a cold search (property-tested).
|
|
879
|
-
*/
|
|
880
|
-
var TrackValidationError = class extends Error {
|
|
881
|
-
constructor(target, message) {
|
|
882
|
-
super(`track '${target}': ${message}`);
|
|
883
|
-
this.name = "TrackValidationError";
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
function validateTrack(track) {
|
|
887
|
-
const vt = getValueType(track.type);
|
|
888
|
-
if (track.keys.length === 0) throw new TrackValidationError(track.target, "must have at least one key");
|
|
889
|
-
if (vt.defaultHandoff === "cut") {
|
|
890
|
-
for (const k of track.keys) if (k.interp !== "hold") k.interp = "hold";
|
|
891
|
-
}
|
|
892
|
-
for (let i = 1; i < track.keys.length; i++) {
|
|
893
|
-
const prev = track.keys[i - 1];
|
|
894
|
-
const cur = track.keys[i];
|
|
895
|
-
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})`);
|
|
896
|
-
}
|
|
897
|
-
if (!/^[^/]+\/.+$/.test(track.target)) throw new TrackValidationError(track.target, "target must be '<nodeId>/<prop.path>' (e.g. 'circle/opacity')");
|
|
898
|
-
}
|
|
899
|
-
function key(t, value, easeOrOpts) {
|
|
900
|
-
if (easeOrOpts === void 0) return {
|
|
901
|
-
t,
|
|
902
|
-
value
|
|
903
|
-
};
|
|
904
|
-
if (typeof easeOrOpts === "string" || "kind" in easeOrOpts) return {
|
|
905
|
-
t,
|
|
906
|
-
value,
|
|
907
|
-
ease: easeOrOpts
|
|
908
|
-
};
|
|
909
|
-
const opts = easeOrOpts;
|
|
910
|
-
const k = {
|
|
911
|
-
t,
|
|
912
|
-
value
|
|
913
|
-
};
|
|
914
|
-
if (opts.ease !== void 0) k.ease = opts.ease;
|
|
915
|
-
if (opts.interp !== void 0) k.interp = opts.interp;
|
|
916
|
-
if (opts.id !== void 0) k.id = opts.id;
|
|
917
|
-
if (opts.derived !== void 0) k.derived = opts.derived;
|
|
918
|
-
return k;
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* The settle-ON-the-beat helper: a spring key must sit at prev.t +
|
|
922
|
-
* spring.duration(cfg) (§2.7), so beat-anchored authoring otherwise means
|
|
923
|
-
* hand-computing the launch time. springTo returns the [launch, settle] key
|
|
924
|
-
* pair with the arithmetic done — spread it into a raw track():
|
|
925
|
-
* track('x/width', 'number', [...springTo(beats.start('drop'), 0, 320, cfg)])
|
|
926
|
-
*/
|
|
927
|
-
function springTo(endT, from, to, cfg) {
|
|
928
|
-
const d = spring.duration(cfg);
|
|
929
|
-
if (endT - d < 0) throw new TrackValidationError("springTo", `this spring needs ${d.toFixed(3)}s to settle — endT must be ≥ its duration (got ${endT})`);
|
|
930
|
-
return [key(endT - d, from), key(endT, to, spring(cfg))];
|
|
931
|
-
}
|
|
310
|
+
//#region src/cmap.ts
|
|
932
311
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
* the
|
|
936
|
-
*
|
|
312
|
+
* Minimal sfnt `cmap` reader (DESIGN.md §3.6 glyph-coverage check) — pure,
|
|
313
|
+
* byte-only, ZERO DOM / ZERO Node. Given the raw bytes of a TrueType/OpenType
|
|
314
|
+
* (or the first table of a TTC) font, returns the set of Unicode code points
|
|
315
|
+
* the font claims to cover. I/O (reading the file / fetching the URL) happens
|
|
316
|
+
* at the call site; this function only parses bytes.
|
|
937
317
|
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
318
|
+
* Hand-rolled (no font-parsing dependency) to keep core zero-dep and in budget.
|
|
319
|
+
* It covers the two cmap subtable formats that matter in practice:
|
|
320
|
+
* - format 4 (segment mapping to delta values — the BMP workhorse)
|
|
321
|
+
* - format 12 (segmented coverage — astral planes, e.g. emoji)
|
|
322
|
+
* Malformed or unsupported input yields an empty set; it never throws or hangs.
|
|
940
323
|
*/
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const fn = namedEasing(spec);
|
|
981
|
-
if (!warnedNumericDerivative.has(spec)) {
|
|
982
|
-
warnedNumericDerivative.add(spec);
|
|
983
|
-
emitDevWarning(`easing '${spec}' has no registered derivative; velocity uses a numeric fallback — register one in easingDerivatives for exact interruption handoff`);
|
|
324
|
+
const PLATFORM_UNICODE = 0;
|
|
325
|
+
const PLATFORM_WINDOWS = 3;
|
|
326
|
+
/** Read a big-endian u16 with bounds checking (0 when out of range). */
|
|
327
|
+
function u16(dv, off) {
|
|
328
|
+
return off + 2 <= dv.byteLength ? dv.getUint16(off) : 0;
|
|
329
|
+
}
|
|
330
|
+
function u32(dv, off) {
|
|
331
|
+
return off + 4 <= dv.byteLength ? dv.getUint32(off) : 0;
|
|
332
|
+
}
|
|
333
|
+
/** Rank a (platform, encoding) cmap subtable: higher = better Unicode coverage. */
|
|
334
|
+
function encodingScore(platform, encoding) {
|
|
335
|
+
if (platform === PLATFORM_WINDOWS && encoding === 10) return 5;
|
|
336
|
+
if (platform === PLATFORM_UNICODE && encoding >= 4) return 5;
|
|
337
|
+
if (platform === PLATFORM_WINDOWS && encoding === 1) return 3;
|
|
338
|
+
if (platform === PLATFORM_UNICODE) return 3;
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
function parseFormat4(dv, base, into) {
|
|
342
|
+
const segCountX2 = u16(dv, base + 6);
|
|
343
|
+
const segCount = segCountX2 >> 1;
|
|
344
|
+
const endOff = base + 14;
|
|
345
|
+
const startOff = endOff + segCountX2 + 2;
|
|
346
|
+
const deltaOff = startOff + segCountX2;
|
|
347
|
+
const rangeOff = deltaOff + segCountX2;
|
|
348
|
+
for (let i = 0; i < segCount; i++) {
|
|
349
|
+
const end = u16(dv, endOff + i * 2);
|
|
350
|
+
const start = u16(dv, startOff + i * 2);
|
|
351
|
+
if (start > end) continue;
|
|
352
|
+
const idDelta = u16(dv, deltaOff + i * 2);
|
|
353
|
+
const idRangeOffset = u16(dv, rangeOff + i * 2);
|
|
354
|
+
for (let c = start; c <= end; c++) {
|
|
355
|
+
if (c === 65535) continue;
|
|
356
|
+
let glyph;
|
|
357
|
+
if (idRangeOffset === 0) glyph = c + idDelta & 65535;
|
|
358
|
+
else {
|
|
359
|
+
const g = u16(dv, rangeOff + i * 2 + idRangeOffset + (c - start) * 2);
|
|
360
|
+
glyph = g === 0 ? 0 : g + idDelta & 65535;
|
|
361
|
+
}
|
|
362
|
+
if (glyph !== 0) into.add(c);
|
|
984
363
|
}
|
|
985
|
-
const h = 1 / 1024;
|
|
986
|
-
return (u) => (fn(Math.min(1, u + h)) - fn(Math.max(0, u - h))) / (Math.min(1, u + h) - Math.max(0, u - h));
|
|
987
|
-
}
|
|
988
|
-
if (spec.kind === "cubicBezier") return cubicBezierDerivative(...spec.pts);
|
|
989
|
-
return springEasingDerivative(spec);
|
|
990
|
-
}
|
|
991
|
-
const samplerStates = /* @__PURE__ */ new WeakMap();
|
|
992
|
-
function state(tr) {
|
|
993
|
-
let s = samplerStates.get(tr);
|
|
994
|
-
if (!s) {
|
|
995
|
-
s = {
|
|
996
|
-
cursor: 1,
|
|
997
|
-
easeCache: new Array(tr.keys.length)
|
|
998
|
-
};
|
|
999
|
-
samplerStates.set(tr, s);
|
|
1000
|
-
}
|
|
1001
|
-
return s;
|
|
1002
|
-
}
|
|
1003
|
-
function easeFor(tr, s, i) {
|
|
1004
|
-
let fn = s.easeCache[i];
|
|
1005
|
-
if (!fn) {
|
|
1006
|
-
fn = resolveEase(tr.keys[i].ease);
|
|
1007
|
-
s.easeCache[i] = fn;
|
|
1008
|
-
}
|
|
1009
|
-
return fn;
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Find i such that keys[i-1].t <= t < keys[i].t, i.e. the index of the
|
|
1013
|
-
* arrival key of the segment containing t. Returns 0 if t < first key,
|
|
1014
|
-
* keys.length if t >= last key.
|
|
1015
|
-
*/
|
|
1016
|
-
function findSegment(keys, t, hint) {
|
|
1017
|
-
const n = keys.length;
|
|
1018
|
-
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;
|
|
1019
|
-
if (t < keys[0].t) return 0;
|
|
1020
|
-
if (t >= keys[n - 1].t) return n;
|
|
1021
|
-
let lo = 1;
|
|
1022
|
-
let hi = n - 1;
|
|
1023
|
-
while (lo < hi) {
|
|
1024
|
-
const mid = lo + hi >> 1;
|
|
1025
|
-
if (keys[mid].t <= t) lo = mid + 1;
|
|
1026
|
-
else hi = mid;
|
|
1027
364
|
}
|
|
1028
|
-
return lo;
|
|
1029
365
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const keys = tr.keys;
|
|
1041
|
-
const n = keys.length;
|
|
1042
|
-
const s = state(tr);
|
|
1043
|
-
const i = findSegment(keys, t, s.cursor);
|
|
1044
|
-
const zero = vt.scale(vt.sub(keys[0].value, keys[0].value), 0);
|
|
1045
|
-
if (i === 0 || i >= n) return zero;
|
|
1046
|
-
const arrival = keys[i];
|
|
1047
|
-
if (arrival.interp === "hold") return zero;
|
|
1048
|
-
const prev = keys[i - 1];
|
|
1049
|
-
const segDur = arrival.t - prev.t;
|
|
1050
|
-
const p = (t - prev.t) / segDur;
|
|
1051
|
-
if (!vt.extrapolates) {
|
|
1052
|
-
const eased = easeFor(tr, s, i)(p);
|
|
1053
|
-
if (eased < 0 || eased > 1) return zero;
|
|
366
|
+
function parseFormat12(dv, base, into) {
|
|
367
|
+
const nGroups = u32(dv, base + 12);
|
|
368
|
+
let off = base + 16;
|
|
369
|
+
for (let i = 0; i < nGroups; i++, off += 12) {
|
|
370
|
+
const startChar = u32(dv, off);
|
|
371
|
+
const endChar = u32(dv, off + 4);
|
|
372
|
+
const startGlyph = u32(dv, off + 8);
|
|
373
|
+
if (startChar > endChar) continue;
|
|
374
|
+
if (endChar - startChar > 2097152) continue;
|
|
375
|
+
for (let c = startChar; c <= endChar; c++) if (startGlyph + (c - startChar) !== 0) into.add(c);
|
|
1054
376
|
}
|
|
1055
|
-
const d = resolveEaseDerivative(arrival.ease)(p);
|
|
1056
|
-
return vt.scale(vt.sub(arrival.value, prev.value), d / segDur);
|
|
1057
377
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
1076
|
-
|
|
378
|
+
function parseCmap(bytes) {
|
|
379
|
+
const out = /* @__PURE__ */ new Set();
|
|
380
|
+
try {
|
|
381
|
+
const dv = new DataView(bytes);
|
|
382
|
+
if (dv.byteLength < 12) return out;
|
|
383
|
+
let sfntOff = 0;
|
|
384
|
+
if (u32(dv, 0) === 1953784678) sfntOff = u32(dv, 12);
|
|
385
|
+
const numTables = u16(dv, sfntOff + 4);
|
|
386
|
+
let cmapOff = 0;
|
|
387
|
+
for (let i = 0; i < numTables; i++) {
|
|
388
|
+
const rec = sfntOff + 12 + i * 16;
|
|
389
|
+
if (u32(dv, rec) === 1668112752) {
|
|
390
|
+
cmapOff = u32(dv, rec + 8);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (cmapOff === 0) return out;
|
|
395
|
+
const nSub = u16(dv, cmapOff + 2);
|
|
396
|
+
let bestOff = 0;
|
|
397
|
+
let bestScore = -1;
|
|
398
|
+
for (let i = 0; i < nSub; i++) {
|
|
399
|
+
const rec = cmapOff + 4 + i * 8;
|
|
400
|
+
const platform = u16(dv, rec);
|
|
401
|
+
const encoding = u16(dv, rec + 2);
|
|
402
|
+
const subOff = cmapOff + u32(dv, rec + 4);
|
|
403
|
+
const score = encodingScore(platform, encoding);
|
|
404
|
+
if (score > bestScore && subOff + 2 <= dv.byteLength) {
|
|
405
|
+
bestScore = score;
|
|
406
|
+
bestOff = subOff;
|
|
407
|
+
}
|
|
1077
408
|
}
|
|
1078
|
-
|
|
409
|
+
if (bestScore < 0) return out;
|
|
410
|
+
const format = u16(dv, bestOff);
|
|
411
|
+
if (format === 4) parseFormat4(dv, bestOff, out);
|
|
412
|
+
else if (format === 12) parseFormat12(dv, bestOff, out);
|
|
413
|
+
} catch {
|
|
414
|
+
return out;
|
|
1079
415
|
}
|
|
1080
|
-
return
|
|
416
|
+
return out;
|
|
1081
417
|
}
|
|
1082
418
|
//#endregion
|
|
1083
|
-
//#region src/
|
|
419
|
+
//#region src/fontValidation.ts
|
|
1084
420
|
/**
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
421
|
+
* Font validation (DESIGN.md §3.6) — the family-level + glyph-coverage check.
|
|
422
|
+
* Pure: given the text usages collected from a scene, the FontRegistry, and the
|
|
423
|
+
* already-parsed cmap coverage per family, it reports unregistered families and
|
|
424
|
+
* glyphs that would hit system fallback ("héllo 👋 renders emoji in Chrome,
|
|
425
|
+
* tofu in Skia"). Strict mode throws; dev mode warns and returns the report.
|
|
426
|
+
*
|
|
427
|
+
* The strict-vs-dev switch is a per-render/per-export OPTION, never a Timeline
|
|
428
|
+
* document flag. Generic and OS families are exempt — only a non-generic,
|
|
429
|
+
* UNREGISTERED family is a strict error (the locked decision).
|
|
1088
430
|
*/
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (init.fps !== void 0) doc.fps = init.fps;
|
|
1096
|
-
if (init.posterTime !== void 0) doc.posterTime = init.posterTime;
|
|
1097
|
-
if (init.labels !== void 0) doc.labels = init.labels;
|
|
1098
|
-
if (init.markers !== void 0) doc.markers = init.markers;
|
|
1099
|
-
if (init.children !== void 0) doc.children = init.children;
|
|
1100
|
-
if (init.audio !== void 0) doc.audio = init.audio;
|
|
1101
|
-
if (init.assets !== void 0) doc.assets = init.assets;
|
|
1102
|
-
return doc;
|
|
1103
|
-
}
|
|
1104
|
-
var TimelineValidationError = class extends Error {
|
|
1105
|
-
constructor(message) {
|
|
1106
|
-
super(message);
|
|
1107
|
-
this.name = "TimelineValidationError";
|
|
431
|
+
var FontValidationError = class extends Error {
|
|
432
|
+
report;
|
|
433
|
+
constructor(report) {
|
|
434
|
+
super(formatReport(report));
|
|
435
|
+
this.name = "FontValidationError";
|
|
436
|
+
this.report = report;
|
|
1108
437
|
}
|
|
1109
438
|
};
|
|
1110
|
-
/** Spring key rule (§2.7): a spring-eased key's t must equal prev.t + spring.duration(cfg). */
|
|
1111
|
-
function validateSpringKeys(tr) {
|
|
1112
|
-
for (let i = 1; i < tr.keys.length; i++) {
|
|
1113
|
-
const k = tr.keys[i];
|
|
1114
|
-
if (k.ease && typeof k.ease === "object" && k.ease.kind === "spring") {
|
|
1115
|
-
const expected = tr.keys[i - 1].t + spring.duration(k.ease);
|
|
1116
|
-
if (Math.abs(k.t - expected) > 1e-6) throw new TimelineValidationError(`track '${tr.target}': spring-eased key at t=${k.t} must sit at prev.t + spring.duration (${expected.toFixed(6)}); the builder computes this automatically`);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
439
|
/**
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
* agree across paths by construction rather than one rounding to milliseconds
|
|
1125
|
-
* and the other to raw float seconds. Default rate is the canonical mix grid.
|
|
440
|
+
* Generic CSS families (CSS Fonts §15) — exempt from the unregistered check;
|
|
441
|
+
* they intentionally resolve to a system/UA font. `sans-serif` is the Text
|
|
442
|
+
* default, so a default-font Text never errors in strict mode.
|
|
1126
443
|
*/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
444
|
+
const GENERIC_FAMILIES = new Set([
|
|
445
|
+
"serif",
|
|
446
|
+
"sans-serif",
|
|
447
|
+
"monospace",
|
|
448
|
+
"cursive",
|
|
449
|
+
"fantasy",
|
|
450
|
+
"system-ui",
|
|
451
|
+
"ui-serif",
|
|
452
|
+
"ui-sans-serif",
|
|
453
|
+
"ui-monospace",
|
|
454
|
+
"ui-rounded",
|
|
455
|
+
"math",
|
|
456
|
+
"emoji",
|
|
457
|
+
"fangsong"
|
|
458
|
+
]);
|
|
459
|
+
/**
|
|
460
|
+
* Is `family` exempt from the unregistered-family check? Generics are exempt by
|
|
461
|
+
* the spec; an `osFamilies` allowlist lets a caller mark OS-installed families
|
|
462
|
+
* (which the rasterizer can resolve without registration) as exempt too.
|
|
463
|
+
*/
|
|
464
|
+
function isExemptFamily(family, osFamilies) {
|
|
465
|
+
const f = family.trim().toLowerCase();
|
|
466
|
+
if (GENERIC_FAMILIES.has(f)) return true;
|
|
467
|
+
return osFamilies?.has(f) ?? false;
|
|
1135
468
|
}
|
|
1136
|
-
function
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
out.push(
|
|
1141
|
-
track: {
|
|
1142
|
-
...tr,
|
|
1143
|
-
keys: rebaseKeys(tr.keys, at, timeScale)
|
|
1144
|
-
},
|
|
1145
|
-
unit
|
|
1146
|
-
});
|
|
469
|
+
function codePoints(text) {
|
|
470
|
+
const out = [];
|
|
471
|
+
for (const ch of text) {
|
|
472
|
+
const cp = ch.codePointAt(0);
|
|
473
|
+
if (cp !== void 0) out.push(cp);
|
|
1147
474
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
function formatReport(report) {
|
|
478
|
+
const parts = [];
|
|
479
|
+
if (report.unregistered.length > 0) parts.push(`unregistered font ${report.unregistered.length === 1 ? "family" : "families"}: ${report.unregistered.join(", ")}`);
|
|
480
|
+
for (const m of report.missingGlyphs) {
|
|
481
|
+
const cps = m.codePoints.map((c) => "U+" + c.toString(16).toUpperCase().padStart(4, "0")).join(", ");
|
|
482
|
+
parts.push(`family '${m.family}' is missing glyphs for ${cps}`);
|
|
1154
483
|
}
|
|
484
|
+
return "font validation failed (§3.6): " + parts.join("; ") + " — register the family with its faces/fallback, or supply covering glyphs";
|
|
1155
485
|
}
|
|
1156
486
|
/**
|
|
1157
|
-
*
|
|
1158
|
-
* (
|
|
1159
|
-
*
|
|
487
|
+
* Validate `usages` against `registry`. `cmaps` maps family → covered code
|
|
488
|
+
* points (already parsed via parseCmap by the caller's I/O). A family with no
|
|
489
|
+
* cmap entry contributes no coverage (its glyphs all count as missing) unless
|
|
490
|
+
* it is exempt or its chain reaches a covered family.
|
|
491
|
+
*
|
|
492
|
+
* Returns the report. In `'strict'` mode it throws FontValidationError when the
|
|
493
|
+
* report is non-empty; in `'dev'` mode it emits a dev warning instead.
|
|
1160
494
|
*/
|
|
1161
|
-
function
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
keys: [...tr.keys]
|
|
1170
|
-
},
|
|
1171
|
-
unit
|
|
1172
|
-
});
|
|
495
|
+
function validateFonts(usages, registry, cmaps, mode, options = {}) {
|
|
496
|
+
const unregistered = /* @__PURE__ */ new Set();
|
|
497
|
+
const missing = /* @__PURE__ */ new Map();
|
|
498
|
+
for (const usage of usages) {
|
|
499
|
+
const family = usage.family;
|
|
500
|
+
const exempt = isExemptFamily(family, options.osFamilies);
|
|
501
|
+
if (!registry.has(family)) {
|
|
502
|
+
if (!exempt) unregistered.add(family);
|
|
1173
503
|
continue;
|
|
1174
504
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
existing.track.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
|
|
1184
|
-
}
|
|
1185
|
-
const result = /* @__PURE__ */ new Map();
|
|
1186
|
-
for (const [target, { track }] of byTarget) result.set(target, track);
|
|
1187
|
-
return result;
|
|
1188
|
-
}
|
|
1189
|
-
function childExtent(child) {
|
|
1190
|
-
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
1191
|
-
return child.at + computeDuration(child.timeline) / scale;
|
|
1192
|
-
}
|
|
1193
|
-
function computeDuration(doc) {
|
|
1194
|
-
if (doc.duration !== void 0) return doc.duration;
|
|
1195
|
-
let max = 0;
|
|
1196
|
-
for (const tr of doc.tracks) {
|
|
1197
|
-
const last = tr.keys[tr.keys.length - 1];
|
|
1198
|
-
if (last) max = Math.max(max, last.t);
|
|
1199
|
-
}
|
|
1200
|
-
for (const m of doc.markers ?? []) max = Math.max(max, m.t);
|
|
1201
|
-
for (const child of doc.children ?? []) max = Math.max(max, childExtent(child));
|
|
1202
|
-
return max;
|
|
1203
|
-
}
|
|
1204
|
-
function compileTimeline(doc) {
|
|
1205
|
-
if (doc.version !== 1) throw new TimelineValidationError(`unsupported timeline document version ${String(doc.version)}`);
|
|
1206
|
-
const flat = [];
|
|
1207
|
-
flatten(doc, 0, 1, 0, flat, { n: 0 });
|
|
1208
|
-
const tracks = coalesce(flat);
|
|
1209
|
-
const labels = { ...doc.labels };
|
|
1210
|
-
const markers = [...doc.markers ?? []];
|
|
1211
|
-
const audio = [...doc.audio ?? []];
|
|
1212
|
-
const visitChildren = (children, at, scale) => {
|
|
1213
|
-
for (const child of children ?? []) {
|
|
1214
|
-
const base = at + child.at / scale;
|
|
1215
|
-
const childScale = scale * (child.mode === "sync" ? child.timeScale ?? 1 : 1);
|
|
1216
|
-
for (const [name, t] of Object.entries(child.timeline.labels ?? {})) if (!(name in labels)) labels[name] = base + t / childScale;
|
|
1217
|
-
for (const m of child.timeline.markers ?? []) markers.push({
|
|
1218
|
-
...m,
|
|
1219
|
-
t: base + m.t / childScale
|
|
1220
|
-
});
|
|
1221
|
-
for (const clip of child.timeline.audio ?? []) audio.push({
|
|
1222
|
-
...clip,
|
|
1223
|
-
at: base + clip.at / childScale,
|
|
1224
|
-
...childScale !== 1 ? { playbackRate: (clip.playbackRate ?? 1) * childScale } : {}
|
|
1225
|
-
});
|
|
1226
|
-
visitChildren(child.timeline.children, base, childScale);
|
|
505
|
+
const chain = registry.fallbackChain(family);
|
|
506
|
+
for (const cp of codePoints(usage.text)) if (!chain.some((fam) => cmaps.get(fam)?.has(cp) ?? false)) {
|
|
507
|
+
let set = missing.get(family);
|
|
508
|
+
if (!set) {
|
|
509
|
+
set = /* @__PURE__ */ new Set();
|
|
510
|
+
missing.set(family, set);
|
|
511
|
+
}
|
|
512
|
+
set.add(cp);
|
|
1227
513
|
}
|
|
514
|
+
}
|
|
515
|
+
const report = {
|
|
516
|
+
unregistered: [...unregistered].sort(),
|
|
517
|
+
missingGlyphs: [...missing.entries()].map(([family, cps]) => ({
|
|
518
|
+
family,
|
|
519
|
+
codePoints: [...cps].sort((a, b) => a - b)
|
|
520
|
+
})).sort((a, b) => a.family < b.family ? -1 : a.family > b.family ? 1 : 0)
|
|
1228
521
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
return {
|
|
1233
|
-
duration: computeDuration(doc),
|
|
1234
|
-
labels,
|
|
1235
|
-
markers,
|
|
1236
|
-
tracks,
|
|
1237
|
-
audio
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
//#endregion
|
|
1241
|
-
//#region src/targetRef.ts
|
|
1242
|
-
/**
|
|
1243
|
-
* Target references (DESIGN.md §2.6): the builder accepts either a canonical
|
|
1244
|
-
* target string ('circle/opacity') or a property signal that carries its own
|
|
1245
|
-
* path — attached by the scene package at node construction via TARGET_PATH.
|
|
1246
|
-
*/
|
|
1247
|
-
const TARGET_PATH = Symbol.for("glissade.targetPath");
|
|
1248
|
-
var UnresolvableTargetError = class extends Error {
|
|
1249
|
-
constructor() {
|
|
1250
|
-
super("tween target is not addressable: pass a target string (\"node/prop\") or a property signal of a node that has an explicit id (§3.1 — anonymous nodes cannot be track targets)");
|
|
1251
|
-
this.name = "UnresolvableTargetError";
|
|
522
|
+
if (!(report.unregistered.length === 0 && report.missingGlyphs.length === 0)) {
|
|
523
|
+
if (mode === "strict") throw new FontValidationError(report);
|
|
524
|
+
emitDevWarning(formatReport(report));
|
|
1252
525
|
}
|
|
1253
|
-
|
|
1254
|
-
function resolveTweenTarget(target) {
|
|
1255
|
-
if (typeof target === "string") return target;
|
|
1256
|
-
const path = target[TARGET_PATH];
|
|
1257
|
-
if (typeof path !== "string") throw new UnresolvableTargetError();
|
|
1258
|
-
return path;
|
|
526
|
+
return report;
|
|
1259
527
|
}
|
|
1260
528
|
//#endregion
|
|
1261
529
|
//#region src/builder.ts
|
|
@@ -1289,6 +557,7 @@ function buildTimeline(build, init = {}) {
|
|
|
1289
557
|
const children = [];
|
|
1290
558
|
const markers = [];
|
|
1291
559
|
const callbacks = /* @__PURE__ */ new Map();
|
|
560
|
+
let durationEditable = false;
|
|
1292
561
|
let prevStart = 0;
|
|
1293
562
|
let prevEnd = 0;
|
|
1294
563
|
let callCount = 0;
|
|
@@ -1411,8 +680,14 @@ function buildTimeline(build, init = {}) {
|
|
|
1411
680
|
editable() {
|
|
1412
681
|
const last = insertions[insertions.length - 1];
|
|
1413
682
|
if (!last) throw new TimelineValidationError(".editable() requires a preceding insertion");
|
|
683
|
+
const nodeId = targetNodeId(last.target);
|
|
684
|
+
if (!isEditableNodeId(nodeId)) throw new TimelineValidationError(`.editable() needs a node with an explicit id; '${nodeId}' is not an editable host (§6.4)`);
|
|
1414
685
|
last.editable = true;
|
|
1415
686
|
return builder;
|
|
687
|
+
},
|
|
688
|
+
editableDuration() {
|
|
689
|
+
durationEditable = true;
|
|
690
|
+
return builder;
|
|
1416
691
|
}
|
|
1417
692
|
};
|
|
1418
693
|
build(builder);
|
|
@@ -1478,6 +753,7 @@ function buildTimeline(build, init = {}) {
|
|
|
1478
753
|
...init,
|
|
1479
754
|
tracks,
|
|
1480
755
|
labels,
|
|
756
|
+
...durationEditable ? { editableDuration: true } : {},
|
|
1481
757
|
...markers.length ? { markers } : {},
|
|
1482
758
|
...children.length ? { children: children.map(({ _pos, ...c }) => c) } : {}
|
|
1483
759
|
});
|
|
@@ -1664,207 +940,4 @@ function bakeCheckpointed(cfg) {
|
|
|
1664
940
|
};
|
|
1665
941
|
}
|
|
1666
942
|
//#endregion
|
|
1667
|
-
|
|
1668
|
-
/**
|
|
1669
|
-
* The editor sidecar (DESIGN.md §6.2): code declares scene structure and
|
|
1670
|
-
* programmatic tracks; the studio owns keyframe data persisted next to the
|
|
1671
|
-
* scene module and merged at track granularity. Versioned independently of the
|
|
1672
|
-
* API (§7.4) — breaking it orphans users' files, so v1 documents migrate.
|
|
1673
|
-
*
|
|
1674
|
-
* v2 namespaces edits by timeline id ('main' for the linear timeline; v2
|
|
1675
|
-
* machines add more), keys tracks by canonical target, records the code
|
|
1676
|
-
* baseline hash for drift detection, parks drifted tracks as `orphans`, and
|
|
1677
|
-
* gives keys stable `k<N>` ids.
|
|
1678
|
-
*/
|
|
1679
|
-
const MAIN = "main";
|
|
1680
|
-
var SidecarVersionError = class extends Error {
|
|
1681
|
-
constructor(version) {
|
|
1682
|
-
super(`unsupported sidecar version ${String(version)}; this build reads sidecarVersion 1 or 2`);
|
|
1683
|
-
this.name = "SidecarVersionError";
|
|
1684
|
-
}
|
|
1685
|
-
};
|
|
1686
|
-
function emptySidecar() {
|
|
1687
|
-
return {
|
|
1688
|
-
sidecarVersion: 2,
|
|
1689
|
-
timelines: { [MAIN]: { tracks: {} } }
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
/** Lift a v1 (or already-v2) document to the v2 shape. Throws on unknown versions. */
|
|
1693
|
-
function migrateSidecar(doc) {
|
|
1694
|
-
if (!doc) return null;
|
|
1695
|
-
if (doc.sidecarVersion === 2) return doc;
|
|
1696
|
-
if (doc.sidecarVersion === 1) {
|
|
1697
|
-
const tracks = {};
|
|
1698
|
-
for (const t of doc.tracks) tracks[t.target] = {
|
|
1699
|
-
type: t.type,
|
|
1700
|
-
baseHash: null,
|
|
1701
|
-
keys: t.keys
|
|
1702
|
-
};
|
|
1703
|
-
const main = { tracks };
|
|
1704
|
-
if (doc.labels) main.labels = doc.labels;
|
|
1705
|
-
return {
|
|
1706
|
-
sidecarVersion: 2,
|
|
1707
|
-
timelines: { [MAIN]: main }
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
throw new SidecarVersionError(doc.sidecarVersion);
|
|
1711
|
-
}
|
|
1712
|
-
/** Stable hash of a code track's keys — the baseline an editor branch records. */
|
|
1713
|
-
function hashKeys(keys) {
|
|
1714
|
-
const s = JSON.stringify(keys.map((k) => [
|
|
1715
|
-
k.t,
|
|
1716
|
-
k.value,
|
|
1717
|
-
k.ease ?? null,
|
|
1718
|
-
k.interp ?? null
|
|
1719
|
-
]));
|
|
1720
|
-
let h = 2166136261;
|
|
1721
|
-
for (let i = 0; i < s.length; i++) {
|
|
1722
|
-
h ^= s.charCodeAt(i);
|
|
1723
|
-
h = Math.imul(h, 16777619);
|
|
1724
|
-
}
|
|
1725
|
-
return (h >>> 0).toString(16);
|
|
1726
|
-
}
|
|
1727
|
-
/** Assign stable monotonic `k<N>` ids to keys lacking one; existing ids preserved. */
|
|
1728
|
-
function assignKeyIds(keys) {
|
|
1729
|
-
let max = -1;
|
|
1730
|
-
for (const k of keys) {
|
|
1731
|
-
const m = k.id ? /^k(\d+)$/.exec(k.id) : null;
|
|
1732
|
-
if (m) max = Math.max(max, Number(m[1]));
|
|
1733
|
-
}
|
|
1734
|
-
return keys.map((k) => k.id ? { ...k } : {
|
|
1735
|
-
...k,
|
|
1736
|
-
id: `k${++max}`
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
/**
|
|
1740
|
-
* Set one editable track's keys in the sidecar (§6.2) — the studio write path.
|
|
1741
|
-
* `codeBaselineKeys` is the code track this branches from (null = editor-created
|
|
1742
|
-
* with no baseline). Keys get stable `k<N>` ids. Returns a new document.
|
|
1743
|
-
*/
|
|
1744
|
-
function setSidecarTrack(doc, timelineId, target, type, keys, codeBaselineKeys) {
|
|
1745
|
-
const tl = doc.timelines[timelineId] ?? { tracks: {} };
|
|
1746
|
-
const entry = {
|
|
1747
|
-
type,
|
|
1748
|
-
baseHash: codeBaselineKeys ? hashKeys(codeBaselineKeys) : null,
|
|
1749
|
-
keys: assignKeyIds(keys)
|
|
1750
|
-
};
|
|
1751
|
-
return {
|
|
1752
|
-
...doc,
|
|
1753
|
-
timelines: {
|
|
1754
|
-
...doc.timelines,
|
|
1755
|
-
[timelineId]: {
|
|
1756
|
-
...tl,
|
|
1757
|
-
tracks: {
|
|
1758
|
-
...tl.tracks,
|
|
1759
|
-
[target]: entry
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Re-resolve `derived:true` leading keys against the merged track (§2.6): a
|
|
1767
|
-
* derived from-key duplicates the preceding key's held value, so an upstream
|
|
1768
|
-
* edit must flow into it or the segment pops at its start. Build-time derived
|
|
1769
|
-
* keys are already correct; this fixes the ones an edit moved beneath.
|
|
1770
|
-
*/
|
|
1771
|
-
function reresolveDerived(keys) {
|
|
1772
|
-
if (!keys.some((k) => k.derived)) return keys;
|
|
1773
|
-
return keys.map((k, i) => k.derived && i > 0 ? {
|
|
1774
|
-
...k,
|
|
1775
|
-
value: keys[i - 1].value
|
|
1776
|
-
} : k);
|
|
1777
|
-
}
|
|
1778
|
-
/**
|
|
1779
|
-
* Merge with the full §6.2 report: the bindable Timeline, the list of targets
|
|
1780
|
-
* whose code baseline drifted, and the orphaned tracks (type-changed, or whose
|
|
1781
|
-
* code track vanished). Orphans are NEVER merged, so a drifted edit can't make
|
|
1782
|
-
* the whole overlay fail to bind — the good edits survive. Inputs unmutated.
|
|
1783
|
-
*/
|
|
1784
|
-
function mergeSidecarDetailed(code, sidecar) {
|
|
1785
|
-
const doc = migrateSidecar(sidecar);
|
|
1786
|
-
if (!doc) return {
|
|
1787
|
-
timeline: code,
|
|
1788
|
-
drift: [],
|
|
1789
|
-
orphans: {}
|
|
1790
|
-
};
|
|
1791
|
-
const main = doc.timelines[MAIN] ?? { tracks: {} };
|
|
1792
|
-
const overlay = new Map(Object.entries(main.tracks));
|
|
1793
|
-
const drift = [];
|
|
1794
|
-
const orphans = { ...main.orphans ?? {} };
|
|
1795
|
-
const tracks = code.tracks.map((t) => {
|
|
1796
|
-
const entry = overlay.get(t.target);
|
|
1797
|
-
if (!entry) return t;
|
|
1798
|
-
overlay.delete(t.target);
|
|
1799
|
-
if (entry.type !== t.type) {
|
|
1800
|
-
orphans[t.target] = {
|
|
1801
|
-
type: entry.type,
|
|
1802
|
-
keys: entry.keys,
|
|
1803
|
-
reason: "type-changed"
|
|
1804
|
-
};
|
|
1805
|
-
return t;
|
|
1806
|
-
}
|
|
1807
|
-
if (entry.baseHash !== null && entry.baseHash !== hashKeys(t.keys)) drift.push(t.target);
|
|
1808
|
-
return {
|
|
1809
|
-
...t,
|
|
1810
|
-
keys: reresolveDerived(entry.keys.map((k) => ({ ...k }))),
|
|
1811
|
-
editable: true
|
|
1812
|
-
};
|
|
1813
|
-
});
|
|
1814
|
-
for (const [target, entry] of overlay) if (entry.baseHash !== null) orphans[target] = {
|
|
1815
|
-
type: entry.type,
|
|
1816
|
-
keys: entry.keys,
|
|
1817
|
-
reason: "prop-missing"
|
|
1818
|
-
};
|
|
1819
|
-
else tracks.push({
|
|
1820
|
-
target,
|
|
1821
|
-
type: entry.type,
|
|
1822
|
-
keys: reresolveDerived(entry.keys.map((k) => ({ ...k }))),
|
|
1823
|
-
editable: true
|
|
1824
|
-
});
|
|
1825
|
-
const merged = {
|
|
1826
|
-
...code,
|
|
1827
|
-
tracks
|
|
1828
|
-
};
|
|
1829
|
-
if (main.labels && Object.keys(main.labels).length > 0) {
|
|
1830
|
-
const codeLabels = code.labels ?? {};
|
|
1831
|
-
const shadowed = Object.keys(main.labels).filter((n) => n in codeLabels);
|
|
1832
|
-
if (shadowed.length) emitDevWarning(`sidecar label(s) ${shadowed.join(", ")} collide with code labels; code wins (§6.2)`);
|
|
1833
|
-
merged.labels = {
|
|
1834
|
-
...main.labels,
|
|
1835
|
-
...codeLabels
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
if (drift.length) emitDevWarning(`sidecar: code baseline changed beneath edits on ${drift.join(", ")} (§6.2)`);
|
|
1839
|
-
return {
|
|
1840
|
-
timeline: merged,
|
|
1841
|
-
drift,
|
|
1842
|
-
orphans
|
|
1843
|
-
};
|
|
1844
|
-
}
|
|
1845
|
-
/** Merge the sidecar overlay onto the code baseline → a bindable Timeline (§6.2). */
|
|
1846
|
-
function mergeSidecar(code, sidecar) {
|
|
1847
|
-
return mergeSidecarDetailed(code, sidecar).timeline;
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* Editor-edit normalization (§2.7 invariant): a spring-eased key's t is
|
|
1851
|
-
* intrinsic — prev.t + spring.duration(cfg) — so after any retime, sort and
|
|
1852
|
-
* re-pin spring keys to their predecessors. Dragging a spring key itself
|
|
1853
|
-
* therefore snaps back; retiming its predecessor carries it along. Returns a
|
|
1854
|
-
* new array. Colliding keys are NUDGED apart (+1ms), never deleted — an editor
|
|
1855
|
-
* must not silently destroy keyframe data on an exact-t collision.
|
|
1856
|
-
*/
|
|
1857
|
-
function normalizeEditedKeys(keys) {
|
|
1858
|
-
const out = keys.map((k) => ({ ...k })).sort((a, b) => a.t - b.t);
|
|
1859
|
-
for (let pass = 0; pass < 2; pass++) {
|
|
1860
|
-
for (let i = 1; i < out.length; i++) {
|
|
1861
|
-
const ease = out[i].ease;
|
|
1862
|
-
if (ease && typeof ease === "object" && ease.kind === "spring") out[i].t = out[i - 1].t + spring.duration(ease);
|
|
1863
|
-
}
|
|
1864
|
-
out.sort((a, b) => a.t - b.t);
|
|
1865
|
-
}
|
|
1866
|
-
for (let i = 1; i < out.length; i++) if (out[i].t <= out[i - 1].t) out[i].t = out[i - 1].t + .001;
|
|
1867
|
-
return out;
|
|
1868
|
-
}
|
|
1869
|
-
//#endregion
|
|
1870
|
-
export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
|
|
943
|
+
export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, FontValidationError, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, deleteSidecarTrack, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseCmap, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
|