@glissade/core 0.1.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/LICENSE +202 -0
- package/dist/index.d.ts +462 -0
- package/dist/index.js +1331 -0
- package/package.json +26 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
//#region src/signal.ts
|
|
2
|
+
let activeConsumer = null;
|
|
3
|
+
let readPhaseDepth = 0;
|
|
4
|
+
var WriteDuringEvaluationError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("signal.set() during evaluation: the read phase is pure (DESIGN.md §2.1). Drivers write the playhead before evaluation begins; stateful animation belongs in bake().");
|
|
7
|
+
this.name = "WriteDuringEvaluationError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var CircularDependencyError = class extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("circular signal dependency detected");
|
|
13
|
+
this.name = "CircularDependencyError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Begin the pure read phase; any signal write until {@link endReadPhase} throws. */
|
|
17
|
+
function beginReadPhase() {
|
|
18
|
+
readPhaseDepth++;
|
|
19
|
+
}
|
|
20
|
+
function endReadPhase() {
|
|
21
|
+
if (readPhaseDepth === 0) throw new Error("endReadPhase() without matching beginReadPhase()");
|
|
22
|
+
readPhaseDepth--;
|
|
23
|
+
}
|
|
24
|
+
function inReadPhase() {
|
|
25
|
+
return readPhaseDepth > 0;
|
|
26
|
+
}
|
|
27
|
+
var SignalNode = class {
|
|
28
|
+
value;
|
|
29
|
+
/** Compute function; null for plain writable sources. Binding installs one. */
|
|
30
|
+
fn;
|
|
31
|
+
equals;
|
|
32
|
+
version = 0;
|
|
33
|
+
state;
|
|
34
|
+
computing = false;
|
|
35
|
+
deps = [];
|
|
36
|
+
depVersions = [];
|
|
37
|
+
dependents = /* @__PURE__ */ new Set();
|
|
38
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
39
|
+
constructor(init) {
|
|
40
|
+
this.fn = init.fn ?? null;
|
|
41
|
+
this.value = init.value;
|
|
42
|
+
this.equals = init.equals ?? Object.is;
|
|
43
|
+
this.state = this.fn ? 2 : 0;
|
|
44
|
+
}
|
|
45
|
+
get() {
|
|
46
|
+
if (activeConsumer) activeConsumer.addDep(this);
|
|
47
|
+
this.updateIfNecessary();
|
|
48
|
+
return this.value;
|
|
49
|
+
}
|
|
50
|
+
peek() {
|
|
51
|
+
this.updateIfNecessary();
|
|
52
|
+
return this.value;
|
|
53
|
+
}
|
|
54
|
+
set(next) {
|
|
55
|
+
if (readPhaseDepth > 0) throw new WriteDuringEvaluationError();
|
|
56
|
+
if (this.fn) this.detachFn();
|
|
57
|
+
this.writeValue(next);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Sanctioned entry write (the playhead at evaluate() entry, DESIGN.md §2.5).
|
|
61
|
+
* Identical to set() but exempt from the phase guard; not part of the public
|
|
62
|
+
* signal surface.
|
|
63
|
+
*/
|
|
64
|
+
forceSet(next) {
|
|
65
|
+
if (this.fn) this.detachFn();
|
|
66
|
+
this.writeValue(next);
|
|
67
|
+
}
|
|
68
|
+
/** Rewire this signal's source to a computation (timeline binding, §2.4). */
|
|
69
|
+
bindSource(fn) {
|
|
70
|
+
if (readPhaseDepth > 0) throw new WriteDuringEvaluationError();
|
|
71
|
+
this.fn = fn;
|
|
72
|
+
this.state = 2;
|
|
73
|
+
this.invalidateDependents(2);
|
|
74
|
+
}
|
|
75
|
+
/** Remove a bound source, freezing the signal at its last value. */
|
|
76
|
+
unbindSource() {
|
|
77
|
+
if (readPhaseDepth > 0) throw new WriteDuringEvaluationError();
|
|
78
|
+
if (!this.fn) return;
|
|
79
|
+
this.updateIfNecessary();
|
|
80
|
+
this.detachFn();
|
|
81
|
+
}
|
|
82
|
+
get isBound() {
|
|
83
|
+
return this.fn !== null;
|
|
84
|
+
}
|
|
85
|
+
subscribe(cb) {
|
|
86
|
+
this.subscribers.add(cb);
|
|
87
|
+
return () => this.subscribers.delete(cb);
|
|
88
|
+
}
|
|
89
|
+
addDep(dep) {
|
|
90
|
+
if (this.deps[this.deps.length - 1] !== dep) {
|
|
91
|
+
this.deps.push(dep);
|
|
92
|
+
this.depVersions.push(-1);
|
|
93
|
+
dep.dependents.add(this);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
detachFn() {
|
|
97
|
+
this.fn = null;
|
|
98
|
+
this.clearDeps();
|
|
99
|
+
this.state = 0;
|
|
100
|
+
}
|
|
101
|
+
writeValue(next) {
|
|
102
|
+
if (this.version > 0 || this.value !== void 0) {
|
|
103
|
+
if (this.equals(this.value, next)) return;
|
|
104
|
+
}
|
|
105
|
+
this.value = next;
|
|
106
|
+
this.version++;
|
|
107
|
+
this.invalidateDependents(2);
|
|
108
|
+
}
|
|
109
|
+
invalidateDependents(level) {
|
|
110
|
+
for (const d of [...this.dependents]) d.markStale(level);
|
|
111
|
+
if (this.subscribers.size > 0) for (const cb of [...this.subscribers]) cb();
|
|
112
|
+
}
|
|
113
|
+
markStale(level) {
|
|
114
|
+
if (this.state >= level) return;
|
|
115
|
+
const wasClean = this.state === 0;
|
|
116
|
+
this.state = level;
|
|
117
|
+
if (wasClean) this.invalidateDependents(1);
|
|
118
|
+
}
|
|
119
|
+
updateIfNecessary() {
|
|
120
|
+
if (this.fn === null || this.state === 0) return;
|
|
121
|
+
if (this.computing) throw new CircularDependencyError();
|
|
122
|
+
if (this.state === 1) {
|
|
123
|
+
this.state = 0;
|
|
124
|
+
for (let i = 0; i < this.deps.length; i++) {
|
|
125
|
+
const dep = this.deps[i];
|
|
126
|
+
dep.updateIfNecessary();
|
|
127
|
+
if (dep.version !== this.depVersions[i]) {
|
|
128
|
+
this.state = 2;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (this.state === 2) this.recompute();
|
|
134
|
+
this.state = 0;
|
|
135
|
+
}
|
|
136
|
+
recompute() {
|
|
137
|
+
this.clearDeps();
|
|
138
|
+
const prevConsumer = activeConsumer;
|
|
139
|
+
activeConsumer = this;
|
|
140
|
+
this.computing = true;
|
|
141
|
+
let next;
|
|
142
|
+
try {
|
|
143
|
+
next = this.fn();
|
|
144
|
+
} finally {
|
|
145
|
+
this.computing = false;
|
|
146
|
+
activeConsumer = prevConsumer;
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < this.deps.length; i++) this.depVersions[i] = this.deps[i].version;
|
|
149
|
+
if (!(this.version > 0 || this.value !== void 0) || !this.equals(this.value, next)) {
|
|
150
|
+
this.value = next;
|
|
151
|
+
this.version++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
clearDeps() {
|
|
155
|
+
for (const dep of this.deps) dep.dependents.delete(this);
|
|
156
|
+
this.deps = [];
|
|
157
|
+
this.depVersions = [];
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function makeCallable(node, extra) {
|
|
161
|
+
const sig = (() => node.get());
|
|
162
|
+
sig["peek"] = () => node.peek();
|
|
163
|
+
sig["subscribe"] = (cb) => node.subscribe(cb);
|
|
164
|
+
extra(sig);
|
|
165
|
+
return sig;
|
|
166
|
+
}
|
|
167
|
+
function signal(initial, options) {
|
|
168
|
+
const node = new SignalNode({
|
|
169
|
+
value: initial,
|
|
170
|
+
equals: options?.equals
|
|
171
|
+
});
|
|
172
|
+
return makeCallable(node, (sig) => {
|
|
173
|
+
sig["set"] = (v) => node.set(v);
|
|
174
|
+
sig["forceSet"] = (v) => node.forceSet(v);
|
|
175
|
+
sig["bindSource"] = (fn) => node.bindSource(fn);
|
|
176
|
+
sig["unbindSource"] = () => node.unbindSource();
|
|
177
|
+
Object.defineProperty(sig, "isBound", { get: () => node.isBound });
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function computed(fn, options) {
|
|
181
|
+
return makeCallable(new SignalNode({
|
|
182
|
+
fn,
|
|
183
|
+
equals: options?.equals
|
|
184
|
+
}), () => {});
|
|
185
|
+
}
|
|
186
|
+
/** Run `fn` without registering dependencies on the active consumer. */
|
|
187
|
+
function untracked(fn) {
|
|
188
|
+
const prev = activeConsumer;
|
|
189
|
+
activeConsumer = null;
|
|
190
|
+
try {
|
|
191
|
+
return fn();
|
|
192
|
+
} finally {
|
|
193
|
+
activeConsumer = prev;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//#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/valueTypes.ts
|
|
291
|
+
/**
|
|
292
|
+
* Value-type registry with pluggable per-type interpolation (DESIGN.md §2.2).
|
|
293
|
+
* `extrapolates` declares whether a type's lerp accepts easedT outside [0,1]
|
|
294
|
+
* (spring overshoot); non-extrapolating types clamp.
|
|
295
|
+
*/
|
|
296
|
+
const registry = /* @__PURE__ */ new Map();
|
|
297
|
+
function registerValueType(vt) {
|
|
298
|
+
registry.set(vt.id, vt);
|
|
299
|
+
}
|
|
300
|
+
var UnknownValueTypeError = class extends Error {
|
|
301
|
+
constructor(id) {
|
|
302
|
+
super(`unknown value type '${id}'; register it via registerValueType()`);
|
|
303
|
+
this.name = "UnknownValueTypeError";
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
function getValueType(id) {
|
|
307
|
+
const vt = registry.get(id);
|
|
308
|
+
if (!vt) throw new UnknownValueTypeError(id);
|
|
309
|
+
return vt;
|
|
310
|
+
}
|
|
311
|
+
const numberType = {
|
|
312
|
+
id: "number",
|
|
313
|
+
lerp: (a, b, t) => a + (b - a) * t,
|
|
314
|
+
extrapolates: true,
|
|
315
|
+
equals: Object.is
|
|
316
|
+
};
|
|
317
|
+
const vec2Equals = (a, b) => a[0] === b[0] && a[1] === b[1];
|
|
318
|
+
const vec2Type = {
|
|
319
|
+
id: "vec2",
|
|
320
|
+
lerp: (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
|
|
321
|
+
extrapolates: true,
|
|
322
|
+
equals: vec2Equals
|
|
323
|
+
};
|
|
324
|
+
const colorType = {
|
|
325
|
+
id: "color",
|
|
326
|
+
lerp: lerpColor,
|
|
327
|
+
extrapolates: true,
|
|
328
|
+
equals: (a, b) => a === b
|
|
329
|
+
};
|
|
330
|
+
/** Discrete types: hold-only by construction (§2.2); lerp snaps at t=1. */
|
|
331
|
+
function discrete(id) {
|
|
332
|
+
return {
|
|
333
|
+
id,
|
|
334
|
+
lerp: (a, b, t) => t >= 1 ? b : a,
|
|
335
|
+
extrapolates: false,
|
|
336
|
+
equals: (a, b) => Object.is(a, b)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const stringType = discrete("string");
|
|
340
|
+
const booleanType = discrete("boolean");
|
|
341
|
+
var ValueTypeInferenceError = class extends Error {
|
|
342
|
+
constructor(value) {
|
|
343
|
+
super(`cannot infer a value type for ${JSON.stringify(value)}; register a custom type`);
|
|
344
|
+
this.name = "ValueTypeInferenceError";
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
/** Infer a registered type id from a sample value (builder + bake authoring surfaces). */
|
|
348
|
+
function inferValueType(value) {
|
|
349
|
+
if (typeof value === "number") return "number";
|
|
350
|
+
if (typeof value === "boolean") return "boolean";
|
|
351
|
+
if (Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === "number")) return "vec2";
|
|
352
|
+
if (typeof value === "string") try {
|
|
353
|
+
parseColor(value);
|
|
354
|
+
return "color";
|
|
355
|
+
} catch {
|
|
356
|
+
return "string";
|
|
357
|
+
}
|
|
358
|
+
throw new ValueTypeInferenceError(value);
|
|
359
|
+
}
|
|
360
|
+
registerValueType(numberType);
|
|
361
|
+
registerValueType(vec2Type);
|
|
362
|
+
registerValueType(colorType);
|
|
363
|
+
registerValueType(stringType);
|
|
364
|
+
registerValueType(booleanType);
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/vec2Signal.ts
|
|
367
|
+
/**
|
|
368
|
+
* Compound Vec2 signal (DESIGN.md §2.1): sub-signals are real signals, the
|
|
369
|
+
* parent read derives from them, and tracks may target either level — a
|
|
370
|
+
* sub-signal binding takes precedence for its component (§2.2).
|
|
371
|
+
*/
|
|
372
|
+
function vec2Signal(initial) {
|
|
373
|
+
const [ix, iy] = Array.isArray(initial) ? initial : [initial.x, initial.y];
|
|
374
|
+
const x = signal(ix);
|
|
375
|
+
const y = signal(iy);
|
|
376
|
+
const compound = signal(null, { equals: (a, b) => a === null || b === null ? a === b : a[0] === b[0] && a[1] === b[1] });
|
|
377
|
+
const baseX = signal(ix);
|
|
378
|
+
const baseY = signal(iy);
|
|
379
|
+
x.bindSource(() => {
|
|
380
|
+
const c = compound();
|
|
381
|
+
return c === null ? baseX() : c[0];
|
|
382
|
+
});
|
|
383
|
+
y.bindSource(() => {
|
|
384
|
+
const c = compound();
|
|
385
|
+
return c === null ? baseY() : c[1];
|
|
386
|
+
});
|
|
387
|
+
const value = computed(() => [x(), y()], { equals: (a, b) => a[0] === b[0] && a[1] === b[1] });
|
|
388
|
+
const sig = (() => value());
|
|
389
|
+
sig["peek"] = () => value.peek();
|
|
390
|
+
sig["subscribe"] = (cb) => value.subscribe(cb);
|
|
391
|
+
sig["set"] = (v) => {
|
|
392
|
+
compound.set(null);
|
|
393
|
+
baseX.set(v[0]);
|
|
394
|
+
baseY.set(v[1]);
|
|
395
|
+
};
|
|
396
|
+
sig["bindSource"] = (fn) => compound.bindSource(() => fn());
|
|
397
|
+
sig["unbindSource"] = () => {
|
|
398
|
+
compound.unbindSource();
|
|
399
|
+
compound.set(null);
|
|
400
|
+
};
|
|
401
|
+
Object.defineProperty(sig, "x", {
|
|
402
|
+
value: x,
|
|
403
|
+
enumerable: true
|
|
404
|
+
});
|
|
405
|
+
Object.defineProperty(sig, "y", {
|
|
406
|
+
value: y,
|
|
407
|
+
enumerable: true
|
|
408
|
+
});
|
|
409
|
+
return sig;
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/easing.ts
|
|
413
|
+
const c1 = 1.70158;
|
|
414
|
+
const c2 = c1 * 1.525;
|
|
415
|
+
const c3 = 2.70158;
|
|
416
|
+
const c4 = 2 * Math.PI / 3;
|
|
417
|
+
const c5 = 2 * Math.PI / 4.5;
|
|
418
|
+
function bounceOut(t) {
|
|
419
|
+
const n1 = 7.5625;
|
|
420
|
+
const d1 = 2.75;
|
|
421
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
422
|
+
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + .75;
|
|
423
|
+
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + .9375;
|
|
424
|
+
return n1 * (t -= 2.625 / d1) * t + .984375;
|
|
425
|
+
}
|
|
426
|
+
const easings = {
|
|
427
|
+
linear: (t) => t,
|
|
428
|
+
easeInQuad: (t) => t * t,
|
|
429
|
+
easeOutQuad: (t) => 1 - (1 - t) * (1 - t),
|
|
430
|
+
easeInOutQuad: (t) => t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2,
|
|
431
|
+
easeInCubic: (t) => t ** 3,
|
|
432
|
+
easeOutCubic: (t) => 1 - (1 - t) ** 3,
|
|
433
|
+
easeInOutCubic: (t) => t < .5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2,
|
|
434
|
+
easeInQuart: (t) => t ** 4,
|
|
435
|
+
easeOutQuart: (t) => 1 - (1 - t) ** 4,
|
|
436
|
+
easeInOutQuart: (t) => t < .5 ? 8 * t ** 4 : 1 - (-2 * t + 2) ** 4 / 2,
|
|
437
|
+
easeInQuint: (t) => t ** 5,
|
|
438
|
+
easeOutQuint: (t) => 1 - (1 - t) ** 5,
|
|
439
|
+
easeInOutQuint: (t) => t < .5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2,
|
|
440
|
+
easeInSine: (t) => 1 - Math.cos(t * Math.PI / 2),
|
|
441
|
+
easeOutSine: (t) => Math.sin(t * Math.PI / 2),
|
|
442
|
+
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
443
|
+
easeInExpo: (t) => t === 0 ? 0 : 2 ** (10 * t - 10),
|
|
444
|
+
easeOutExpo: (t) => t === 1 ? 1 : 1 - 2 ** (-10 * t),
|
|
445
|
+
easeInOutExpo: (t) => t === 0 ? 0 : t === 1 ? 1 : t < .5 ? 2 ** (20 * t - 10) / 2 : (2 - 2 ** (-20 * t + 10)) / 2,
|
|
446
|
+
easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
|
|
447
|
+
easeOutCirc: (t) => Math.sqrt(1 - (t - 1) * (t - 1)),
|
|
448
|
+
easeInOutCirc: (t) => t < .5 ? (1 - Math.sqrt(1 - (2 * t) ** 2)) / 2 : (Math.sqrt(1 - (-2 * t + 2) ** 2) + 1) / 2,
|
|
449
|
+
easeInBack: (t) => c3 * t ** 3 - c1 * t * t,
|
|
450
|
+
easeOutBack: (t) => 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2,
|
|
451
|
+
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,
|
|
452
|
+
easeInElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : -(2 ** (10 * t - 10)) * Math.sin((t * 10 - 10.75) * c4),
|
|
453
|
+
easeOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - .75) * c4) + 1,
|
|
454
|
+
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,
|
|
455
|
+
easeInBounce: (t) => 1 - bounceOut(1 - t),
|
|
456
|
+
easeOutBounce: bounceOut,
|
|
457
|
+
easeInOutBounce: (t) => t < .5 ? (1 - bounceOut(1 - 2 * t)) / 2 : (1 + bounceOut(2 * t - 1)) / 2
|
|
458
|
+
};
|
|
459
|
+
/** Default property-tween ease (Motion Canvas precedent). */
|
|
460
|
+
const DEFAULT_EASE = "easeInOutCubic";
|
|
461
|
+
/**
|
|
462
|
+
* CSS-style cubic bézier where x is time and y is progress. Newton's method
|
|
463
|
+
* with a bisection fallback for the flat-derivative regions.
|
|
464
|
+
*/
|
|
465
|
+
function cubicBezier(p1x, p1y, p2x, p2y) {
|
|
466
|
+
const ax = 3 * p1x - 3 * p2x + 1;
|
|
467
|
+
const bx = 3 * p2x - 6 * p1x;
|
|
468
|
+
const cx = 3 * p1x;
|
|
469
|
+
const ay = 3 * p1y - 3 * p2y + 1;
|
|
470
|
+
const by = 3 * p2y - 6 * p1y;
|
|
471
|
+
const cy = 3 * p1y;
|
|
472
|
+
const sampleX = (u) => ((ax * u + bx) * u + cx) * u;
|
|
473
|
+
const sampleY = (u) => ((ay * u + by) * u + cy) * u;
|
|
474
|
+
const sampleDX = (u) => (3 * ax * u + 2 * bx) * u + cx;
|
|
475
|
+
const solveU = (x) => {
|
|
476
|
+
let u = x;
|
|
477
|
+
for (let i = 0; i < 8; i++) {
|
|
478
|
+
const err = sampleX(u) - x;
|
|
479
|
+
if (Math.abs(err) < 1e-7) return u;
|
|
480
|
+
const d = sampleDX(u);
|
|
481
|
+
if (Math.abs(d) < 1e-6) break;
|
|
482
|
+
u -= err / d;
|
|
483
|
+
}
|
|
484
|
+
let lo = 0;
|
|
485
|
+
let hi = 1;
|
|
486
|
+
u = x;
|
|
487
|
+
while (hi - lo > 1e-7) {
|
|
488
|
+
if (sampleX(u) < x) lo = u;
|
|
489
|
+
else hi = u;
|
|
490
|
+
u = (lo + hi) / 2;
|
|
491
|
+
}
|
|
492
|
+
return u;
|
|
493
|
+
};
|
|
494
|
+
return (t) => {
|
|
495
|
+
if (t <= 0) return 0;
|
|
496
|
+
if (t >= 1) return 1;
|
|
497
|
+
return sampleY(solveU(t));
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
var UnknownEasingError = class extends Error {
|
|
501
|
+
constructor(name) {
|
|
502
|
+
super(`unknown easing '${name}'; register it via easings or use cubicBezier/spring`);
|
|
503
|
+
this.name = "UnknownEasingError";
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function namedEasing(name) {
|
|
507
|
+
const fn = easings[name];
|
|
508
|
+
if (!fn) throw new UnknownEasingError(name);
|
|
509
|
+
return fn;
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/spring.ts
|
|
513
|
+
const DEFAULT_SETTLE_TOLERANCE = .005;
|
|
514
|
+
function params(cfg) {
|
|
515
|
+
const mass = cfg.mass ?? 1;
|
|
516
|
+
if (!(cfg.stiffness > 0) || !(cfg.damping > 0) || !(mass > 0)) throw new RangeError("spring stiffness, damping, and mass must all be > 0");
|
|
517
|
+
return {
|
|
518
|
+
w0: Math.sqrt(cfg.stiffness / mass),
|
|
519
|
+
zeta: cfg.damping / (2 * Math.sqrt(cfg.stiffness * mass))
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/** Raw closed-form spring position at time t (seconds). Approaches 1, may overshoot. */
|
|
523
|
+
function rawValue(cfg, t) {
|
|
524
|
+
if (t <= 0) return 0;
|
|
525
|
+
const { w0, zeta } = params(cfg);
|
|
526
|
+
if (Math.abs(zeta - 1) < 1e-9) return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
527
|
+
if (zeta < 1) {
|
|
528
|
+
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
529
|
+
return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + zeta * w0 / wd * Math.sin(wd * t));
|
|
530
|
+
}
|
|
531
|
+
const s = Math.sqrt(zeta * zeta - 1);
|
|
532
|
+
const r1 = -w0 * (zeta - s);
|
|
533
|
+
const r2 = -w0 * (zeta + s);
|
|
534
|
+
return 1 + (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r1 - r2);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Settle duration: the earliest time after which |x - 1| stays within
|
|
538
|
+
* settleTolerance. Closed-form via the decay envelope for the underdamped
|
|
539
|
+
* case; bisection on the monotone tail otherwise. Deterministic.
|
|
540
|
+
*/
|
|
541
|
+
function duration(cfg, opts) {
|
|
542
|
+
const tol = opts?.settleTolerance ?? DEFAULT_SETTLE_TOLERANCE;
|
|
543
|
+
const { w0, zeta } = params(cfg);
|
|
544
|
+
if (zeta < 1) {
|
|
545
|
+
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
546
|
+
const amp = Math.sqrt(1 + (zeta * w0 / wd) ** 2);
|
|
547
|
+
return Math.log(amp / tol) / (zeta * w0);
|
|
548
|
+
}
|
|
549
|
+
let hi = 1 / w0;
|
|
550
|
+
while (1 - rawValue(cfg, hi) > tol) hi *= 2;
|
|
551
|
+
let lo = 0;
|
|
552
|
+
for (let i = 0; i < 64 && hi - lo > 1e-9; i++) {
|
|
553
|
+
const mid = (lo + hi) / 2;
|
|
554
|
+
if (1 - rawValue(cfg, mid) > tol) lo = mid;
|
|
555
|
+
else hi = mid;
|
|
556
|
+
}
|
|
557
|
+
return hi;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Spring progress at local time t, affinely rescaled so value(duration) = 1
|
|
561
|
+
* exactly — the raw form only approaches 1, and an unscaled curve would snap
|
|
562
|
+
* at the key (§2.7 "endpoint continuity"). May exceed 1 (overshoot).
|
|
563
|
+
*/
|
|
564
|
+
function value(cfg, t, opts) {
|
|
565
|
+
const d = duration(cfg, opts);
|
|
566
|
+
return rawValue(cfg, Math.min(t, d)) / rawValue(cfg, d);
|
|
567
|
+
}
|
|
568
|
+
const spring = Object.assign((cfg) => {
|
|
569
|
+
params(cfg);
|
|
570
|
+
return {
|
|
571
|
+
kind: "spring",
|
|
572
|
+
stiffness: cfg.stiffness,
|
|
573
|
+
damping: cfg.damping,
|
|
574
|
+
mass: cfg.mass ?? 1
|
|
575
|
+
};
|
|
576
|
+
}, {
|
|
577
|
+
duration,
|
|
578
|
+
value
|
|
579
|
+
});
|
|
580
|
+
/**
|
|
581
|
+
* The spring as a normalized easing over a segment whose length must equal
|
|
582
|
+
* spring.duration(cfg) (validated at the document layer, §2.7).
|
|
583
|
+
*/
|
|
584
|
+
function springEasing(cfg) {
|
|
585
|
+
const d = duration(cfg);
|
|
586
|
+
return (p) => value(cfg, p * d);
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/track.ts
|
|
590
|
+
/**
|
|
591
|
+
* Track & keyframe model (DESIGN.md §2.2) and sampling (§2.4): binary search
|
|
592
|
+
* per track with a last-segment cursor — sanctioned memoization, semantics
|
|
593
|
+
* identical to a cold search (property-tested).
|
|
594
|
+
*/
|
|
595
|
+
var TrackValidationError = class extends Error {
|
|
596
|
+
constructor(target, message) {
|
|
597
|
+
super(`track '${target}': ${message}`);
|
|
598
|
+
this.name = "TrackValidationError";
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
function validateTrack(track) {
|
|
602
|
+
getValueType(track.type);
|
|
603
|
+
if (track.keys.length === 0) throw new TrackValidationError(track.target, "must have at least one key");
|
|
604
|
+
for (let i = 1; i < track.keys.length; i++) {
|
|
605
|
+
const prev = track.keys[i - 1];
|
|
606
|
+
const cur = track.keys[i];
|
|
607
|
+
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})`);
|
|
608
|
+
}
|
|
609
|
+
if (!/^[^/]+\/.+$/.test(track.target)) throw new TrackValidationError(track.target, "target must be '<nodeId>/<prop.path>' (e.g. 'circle/opacity')");
|
|
610
|
+
}
|
|
611
|
+
function key(t, value, easeOrOpts) {
|
|
612
|
+
if (easeOrOpts === void 0) return {
|
|
613
|
+
t,
|
|
614
|
+
value
|
|
615
|
+
};
|
|
616
|
+
if (typeof easeOrOpts === "string" || "kind" in easeOrOpts) return {
|
|
617
|
+
t,
|
|
618
|
+
value,
|
|
619
|
+
ease: easeOrOpts
|
|
620
|
+
};
|
|
621
|
+
const opts = easeOrOpts;
|
|
622
|
+
const k = {
|
|
623
|
+
t,
|
|
624
|
+
value
|
|
625
|
+
};
|
|
626
|
+
if (opts.ease !== void 0) k.ease = opts.ease;
|
|
627
|
+
if (opts.interp !== void 0) k.interp = opts.interp;
|
|
628
|
+
if (opts.id !== void 0) k.id = opts.id;
|
|
629
|
+
if (opts.derived !== void 0) k.derived = opts.derived;
|
|
630
|
+
return k;
|
|
631
|
+
}
|
|
632
|
+
function track(target, type, keys, opts) {
|
|
633
|
+
const tr = {
|
|
634
|
+
target,
|
|
635
|
+
type,
|
|
636
|
+
keys
|
|
637
|
+
};
|
|
638
|
+
if (opts?.editable !== void 0) tr.editable = opts.editable;
|
|
639
|
+
validateTrack(tr);
|
|
640
|
+
return tr;
|
|
641
|
+
}
|
|
642
|
+
function resolveEase(spec) {
|
|
643
|
+
if (spec === void 0) return namedEasing("linear");
|
|
644
|
+
if (typeof spec === "string") return namedEasing(spec);
|
|
645
|
+
if (spec.kind === "cubicBezier") return cubicBezier(...spec.pts);
|
|
646
|
+
return springEasing(spec);
|
|
647
|
+
}
|
|
648
|
+
const samplerStates = /* @__PURE__ */ new WeakMap();
|
|
649
|
+
function state(tr) {
|
|
650
|
+
let s = samplerStates.get(tr);
|
|
651
|
+
if (!s) {
|
|
652
|
+
s = {
|
|
653
|
+
cursor: 1,
|
|
654
|
+
easeCache: new Array(tr.keys.length)
|
|
655
|
+
};
|
|
656
|
+
samplerStates.set(tr, s);
|
|
657
|
+
}
|
|
658
|
+
return s;
|
|
659
|
+
}
|
|
660
|
+
function easeFor(tr, s, i) {
|
|
661
|
+
let fn = s.easeCache[i];
|
|
662
|
+
if (!fn) {
|
|
663
|
+
fn = resolveEase(tr.keys[i].ease);
|
|
664
|
+
s.easeCache[i] = fn;
|
|
665
|
+
}
|
|
666
|
+
return fn;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Find i such that keys[i-1].t <= t < keys[i].t, i.e. the index of the
|
|
670
|
+
* arrival key of the segment containing t. Returns 0 if t < first key,
|
|
671
|
+
* keys.length if t >= last key.
|
|
672
|
+
*/
|
|
673
|
+
function findSegment(keys, t, hint) {
|
|
674
|
+
const n = keys.length;
|
|
675
|
+
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;
|
|
676
|
+
if (t < keys[0].t) return 0;
|
|
677
|
+
if (t >= keys[n - 1].t) return n;
|
|
678
|
+
let lo = 1;
|
|
679
|
+
let hi = n - 1;
|
|
680
|
+
while (lo < hi) {
|
|
681
|
+
const mid = lo + hi >> 1;
|
|
682
|
+
if (keys[mid].t <= t) lo = mid + 1;
|
|
683
|
+
else hi = mid;
|
|
684
|
+
}
|
|
685
|
+
return lo;
|
|
686
|
+
}
|
|
687
|
+
/** Pure sample of a track at time t (§2.4). */
|
|
688
|
+
function sampleTrack(tr, t) {
|
|
689
|
+
const keys = tr.keys;
|
|
690
|
+
const n = keys.length;
|
|
691
|
+
const s = state(tr);
|
|
692
|
+
const i = findSegment(keys, t, s.cursor);
|
|
693
|
+
if (i === 0) return keys[0].value;
|
|
694
|
+
if (i >= n) return keys[n - 1].value;
|
|
695
|
+
s.cursor = i;
|
|
696
|
+
const arrival = keys[i];
|
|
697
|
+
const prev = keys[i - 1];
|
|
698
|
+
if (arrival.interp === "hold") return prev.value;
|
|
699
|
+
const vt = getValueType(tr.type);
|
|
700
|
+
const p = (t - prev.t) / (arrival.t - prev.t);
|
|
701
|
+
let easedT = easeFor(tr, s, i)(p);
|
|
702
|
+
if (!vt.extrapolates) easedT = Math.min(1, Math.max(0, easedT));
|
|
703
|
+
return vt.lerp(prev.value, arrival.value, easedT);
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/timeline.ts
|
|
707
|
+
/**
|
|
708
|
+
* The Timeline document (DESIGN.md §2.3) — the serializable animation source
|
|
709
|
+
* of truth — and its compiler: child flattening (add/sync), same-target
|
|
710
|
+
* coalescing (last-insertion-wins, §2.2), duration computation, validation.
|
|
711
|
+
*/
|
|
712
|
+
function timeline$1(init) {
|
|
713
|
+
const doc = {
|
|
714
|
+
version: 1,
|
|
715
|
+
tracks: init.tracks ?? []
|
|
716
|
+
};
|
|
717
|
+
if (init.duration !== void 0) doc.duration = init.duration;
|
|
718
|
+
if (init.fps !== void 0) doc.fps = init.fps;
|
|
719
|
+
if (init.posterTime !== void 0) doc.posterTime = init.posterTime;
|
|
720
|
+
if (init.labels !== void 0) doc.labels = init.labels;
|
|
721
|
+
if (init.markers !== void 0) doc.markers = init.markers;
|
|
722
|
+
if (init.children !== void 0) doc.children = init.children;
|
|
723
|
+
if (init.audio !== void 0) doc.audio = init.audio;
|
|
724
|
+
if (init.assets !== void 0) doc.assets = init.assets;
|
|
725
|
+
return doc;
|
|
726
|
+
}
|
|
727
|
+
var TimelineValidationError = class extends Error {
|
|
728
|
+
constructor(message) {
|
|
729
|
+
super(message);
|
|
730
|
+
this.name = "TimelineValidationError";
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
/** Spring key rule (§2.7): a spring-eased key's t must equal prev.t + spring.duration(cfg). */
|
|
734
|
+
function validateSpringKeys(tr) {
|
|
735
|
+
for (let i = 1; i < tr.keys.length; i++) {
|
|
736
|
+
const k = tr.keys[i];
|
|
737
|
+
if (k.ease && typeof k.ease === "object" && k.ease.kind === "spring") {
|
|
738
|
+
const expected = tr.keys[i - 1].t + spring.duration(k.ease);
|
|
739
|
+
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`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
let devWarn = (msg) => {
|
|
744
|
+
globalThis.console?.warn(`[glissade] ${msg}`);
|
|
745
|
+
};
|
|
746
|
+
function setDevWarning(fn) {
|
|
747
|
+
devWarn = fn;
|
|
748
|
+
}
|
|
749
|
+
/** Internal: emit through the configurable dev-warning channel. */
|
|
750
|
+
function emitDevWarning(message) {
|
|
751
|
+
devWarn(message);
|
|
752
|
+
}
|
|
753
|
+
function rebaseKeys(keys, at, timeScale) {
|
|
754
|
+
return keys.map((k) => ({
|
|
755
|
+
...k,
|
|
756
|
+
t: at + k.t / timeScale
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
function flatten(doc, at, timeScale, opaque, out) {
|
|
760
|
+
for (const tr of doc.tracks) {
|
|
761
|
+
validateTrack(tr);
|
|
762
|
+
validateSpringKeys(tr);
|
|
763
|
+
out.push({
|
|
764
|
+
track: {
|
|
765
|
+
...tr,
|
|
766
|
+
keys: rebaseKeys(tr.keys, at, timeScale)
|
|
767
|
+
},
|
|
768
|
+
opaque
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
for (const child of doc.children ?? []) {
|
|
772
|
+
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
773
|
+
if (child.mode === "add" && child.timeScale !== void 0) throw new TimelineValidationError("timeScale is only valid on mode:'sync' children (§2.3)");
|
|
774
|
+
if (scale <= 0) throw new TimelineValidationError("sync timeScale must be > 0");
|
|
775
|
+
flatten(child.timeline, at + child.at / timeScale, timeScale * scale, opaque || child.mode === "sync", out);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Coalesce same-target tracks: later insertion wins where key ranges overlap
|
|
780
|
+
* (§2.2/§2.6 decided rule), with a dev warning. Earlier keys inside the later
|
|
781
|
+
* track's [first.t, last.t] span are dropped.
|
|
782
|
+
*/
|
|
783
|
+
function coalesce(entries) {
|
|
784
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const { track: tr } of entries) {
|
|
786
|
+
const existing = byTarget.get(tr.target);
|
|
787
|
+
if (!existing) {
|
|
788
|
+
byTarget.set(tr.target, {
|
|
789
|
+
...tr,
|
|
790
|
+
keys: [...tr.keys]
|
|
791
|
+
});
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (existing.type !== tr.type) throw new TimelineValidationError(`target '${tr.target}' has conflicting value types '${existing.type}' and '${tr.type}'`);
|
|
795
|
+
const start = tr.keys[0].t;
|
|
796
|
+
const end = tr.keys[tr.keys.length - 1].t;
|
|
797
|
+
const existingStart = existing.keys[0].t;
|
|
798
|
+
const existingEnd = existing.keys[existing.keys.length - 1].t;
|
|
799
|
+
const kept = existing.keys.filter((k) => k.t < start || k.t > end);
|
|
800
|
+
if (existingStart <= end && start <= existingEnd) devWarn(`overlapping tracks for '${tr.target}' in [${start}, ${end}]: later insertion wins (${existing.keys.length - kept.length} earlier key(s) dropped)`);
|
|
801
|
+
existing.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
|
|
802
|
+
}
|
|
803
|
+
return byTarget;
|
|
804
|
+
}
|
|
805
|
+
function childExtent(child) {
|
|
806
|
+
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
807
|
+
return child.at + computeDuration(child.timeline) / scale;
|
|
808
|
+
}
|
|
809
|
+
function computeDuration(doc) {
|
|
810
|
+
if (doc.duration !== void 0) return doc.duration;
|
|
811
|
+
let max = 0;
|
|
812
|
+
for (const tr of doc.tracks) {
|
|
813
|
+
const last = tr.keys[tr.keys.length - 1];
|
|
814
|
+
if (last) max = Math.max(max, last.t);
|
|
815
|
+
}
|
|
816
|
+
for (const m of doc.markers ?? []) max = Math.max(max, m.t);
|
|
817
|
+
for (const child of doc.children ?? []) max = Math.max(max, childExtent(child));
|
|
818
|
+
return max;
|
|
819
|
+
}
|
|
820
|
+
function compileTimeline(doc) {
|
|
821
|
+
if (doc.version !== 1) throw new TimelineValidationError(`unsupported timeline document version ${String(doc.version)}`);
|
|
822
|
+
const flat = [];
|
|
823
|
+
flatten(doc, 0, 1, false, flat);
|
|
824
|
+
const tracks = coalesce(flat);
|
|
825
|
+
const labels = { ...doc.labels };
|
|
826
|
+
const markers = [...doc.markers ?? []];
|
|
827
|
+
const audio = [...doc.audio ?? []];
|
|
828
|
+
const visitChildren = (children, at, scale) => {
|
|
829
|
+
for (const child of children ?? []) {
|
|
830
|
+
const base = at + child.at / scale;
|
|
831
|
+
const childScale = scale * (child.mode === "sync" ? child.timeScale ?? 1 : 1);
|
|
832
|
+
for (const [name, t] of Object.entries(child.timeline.labels ?? {})) if (!(name in labels)) labels[name] = base + t / childScale;
|
|
833
|
+
for (const m of child.timeline.markers ?? []) markers.push({
|
|
834
|
+
...m,
|
|
835
|
+
t: base + m.t / childScale
|
|
836
|
+
});
|
|
837
|
+
for (const clip of child.timeline.audio ?? []) audio.push({
|
|
838
|
+
...clip,
|
|
839
|
+
at: base + clip.at / childScale,
|
|
840
|
+
...childScale !== 1 ? { playbackRate: (clip.playbackRate ?? 1) * childScale } : {}
|
|
841
|
+
});
|
|
842
|
+
visitChildren(child.timeline.children, base, childScale);
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
visitChildren(doc.children, 0, 1);
|
|
846
|
+
markers.sort((a, b) => a.t - b.t);
|
|
847
|
+
audio.sort((a, b) => a.at - b.at);
|
|
848
|
+
return {
|
|
849
|
+
duration: computeDuration(doc),
|
|
850
|
+
labels,
|
|
851
|
+
markers,
|
|
852
|
+
tracks,
|
|
853
|
+
audio
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/targetRef.ts
|
|
858
|
+
/**
|
|
859
|
+
* Target references (DESIGN.md §2.6): the builder accepts either a canonical
|
|
860
|
+
* target string ('circle/opacity') or a property signal that carries its own
|
|
861
|
+
* path — attached by the scene package at node construction via TARGET_PATH.
|
|
862
|
+
*/
|
|
863
|
+
const TARGET_PATH = Symbol.for("glissade.targetPath");
|
|
864
|
+
var UnresolvableTargetError = class extends Error {
|
|
865
|
+
constructor() {
|
|
866
|
+
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)");
|
|
867
|
+
this.name = "UnresolvableTargetError";
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
function resolveTweenTarget(target) {
|
|
871
|
+
if (typeof target === "string") return target;
|
|
872
|
+
const path = target[TARGET_PATH];
|
|
873
|
+
if (typeof path !== "string") throw new UnresolvableTargetError();
|
|
874
|
+
return path;
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/builder.ts
|
|
878
|
+
/**
|
|
879
|
+
* The fluent timeline builder (DESIGN.md §2.6): imperative-LOOKING chains that
|
|
880
|
+
* compile to the declarative Timeline document. Nothing executes at play time.
|
|
881
|
+
*
|
|
882
|
+
* Position grammar (GSAP-proven): absolute `1.5`; `'+=0.5'`/`'-=0.2'` from the
|
|
883
|
+
* previous insertion's END; `'<'`/`'>'` previous START/END; `'label'`,
|
|
884
|
+
* `'label+=0.3'`. Implicit from-values resolve in a finalize pass in t-order
|
|
885
|
+
* against the complete document — never sampled live (the GSAP invalidate()
|
|
886
|
+
* bug class, §2.6), with the build-time signal value as the t-order base.
|
|
887
|
+
*/
|
|
888
|
+
var PositionError = class extends Error {
|
|
889
|
+
constructor(pos, detail) {
|
|
890
|
+
super(`invalid position '${pos}': ${detail}`);
|
|
891
|
+
this.name = "PositionError";
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
function peekBase(target) {
|
|
895
|
+
return typeof target !== "string" && typeof target.peek === "function" ? target.peek() : void 0;
|
|
896
|
+
}
|
|
897
|
+
/** Callbacks registered via .call(), keyed by the produced document. */
|
|
898
|
+
const timelineCallbacks = /* @__PURE__ */ new WeakMap();
|
|
899
|
+
function getTimelineCallbacks(doc) {
|
|
900
|
+
return timelineCallbacks.get(doc) ?? /* @__PURE__ */ new Map();
|
|
901
|
+
}
|
|
902
|
+
function buildTimeline(build, init = {}) {
|
|
903
|
+
const insertions = [];
|
|
904
|
+
const labels = { ...init.labels };
|
|
905
|
+
const children = [];
|
|
906
|
+
const markers = [];
|
|
907
|
+
const callbacks = /* @__PURE__ */ new Map();
|
|
908
|
+
let prevStart = 0;
|
|
909
|
+
let prevEnd = 0;
|
|
910
|
+
let callCount = 0;
|
|
911
|
+
function resolvePosition(at) {
|
|
912
|
+
if (at === void 0) return prevEnd;
|
|
913
|
+
if (typeof at === "number") return at;
|
|
914
|
+
if (at === "<") return prevStart;
|
|
915
|
+
if (at === ">") return prevEnd;
|
|
916
|
+
const rel = /^([+-])=([\d.]+)$/.exec(at);
|
|
917
|
+
if (rel) return prevEnd + (rel[1] === "+" ? 1 : -1) * parseFloat(rel[2]);
|
|
918
|
+
const labelRel = /^([^+\-=]+?)(?:([+-])=([\d.]+))?$/.exec(at);
|
|
919
|
+
if (labelRel) {
|
|
920
|
+
const name = labelRel[1];
|
|
921
|
+
if (!(name in labels)) throw new PositionError(at, `unknown label '${name}' (labels must be declared before use)`);
|
|
922
|
+
const offset = labelRel[2] ? (labelRel[2] === "+" ? 1 : -1) * parseFloat(labelRel[3]) : 0;
|
|
923
|
+
return labels[name] + offset;
|
|
924
|
+
}
|
|
925
|
+
throw new PositionError(at, "expected a number, '<', '>', '+=x', '-=x', or 'label[+=x]'");
|
|
926
|
+
}
|
|
927
|
+
const builder = {
|
|
928
|
+
to(target, value, opts = {}) {
|
|
929
|
+
const ease = opts.ease ?? "easeInOutCubic";
|
|
930
|
+
const isSpring = typeof ease === "object" && ease.kind === "spring";
|
|
931
|
+
const duration = isSpring ? spring.duration(ease) : opts.duration ?? 1;
|
|
932
|
+
if (isSpring && opts.duration !== void 0) throw new TimelineValidationError("a spring ease determines its duration (spring.duration(cfg)); do not pass duration with a spring");
|
|
933
|
+
const start = resolvePosition(opts.at);
|
|
934
|
+
const ins = {
|
|
935
|
+
kind: "tween",
|
|
936
|
+
target: resolveTweenTarget(target),
|
|
937
|
+
value,
|
|
938
|
+
duration,
|
|
939
|
+
ease,
|
|
940
|
+
at: opts.at,
|
|
941
|
+
baseValue: peekBase(target),
|
|
942
|
+
editable: false,
|
|
943
|
+
start
|
|
944
|
+
};
|
|
945
|
+
if (opts.from !== void 0) ins.explicitFrom = opts.from;
|
|
946
|
+
insertions.push(ins);
|
|
947
|
+
prevStart = start;
|
|
948
|
+
prevEnd = start + duration;
|
|
949
|
+
return builder;
|
|
950
|
+
},
|
|
951
|
+
fromTo(target, from, to, opts = {}) {
|
|
952
|
+
builder.to(target, to, opts);
|
|
953
|
+
insertions[insertions.length - 1].explicitFrom = from;
|
|
954
|
+
return builder;
|
|
955
|
+
},
|
|
956
|
+
set(target, value, opts = {}) {
|
|
957
|
+
const start = resolvePosition(opts.at);
|
|
958
|
+
insertions.push({
|
|
959
|
+
kind: "set",
|
|
960
|
+
target: resolveTweenTarget(target),
|
|
961
|
+
value,
|
|
962
|
+
duration: 0,
|
|
963
|
+
ease: "linear",
|
|
964
|
+
at: opts.at,
|
|
965
|
+
baseValue: peekBase(target),
|
|
966
|
+
editable: false,
|
|
967
|
+
start
|
|
968
|
+
});
|
|
969
|
+
prevStart = start;
|
|
970
|
+
prevEnd = start;
|
|
971
|
+
return builder;
|
|
972
|
+
},
|
|
973
|
+
label(name, at) {
|
|
974
|
+
labels[name] = at === void 0 ? prevEnd : resolvePosition(at);
|
|
975
|
+
return builder;
|
|
976
|
+
},
|
|
977
|
+
add(child, at, opts = {}) {
|
|
978
|
+
const start = at === void 0 ? prevEnd : resolvePosition(at);
|
|
979
|
+
const mode = opts.mode ?? "add";
|
|
980
|
+
const entry = {
|
|
981
|
+
timeline: child,
|
|
982
|
+
at: start,
|
|
983
|
+
mode,
|
|
984
|
+
_pos: at
|
|
985
|
+
};
|
|
986
|
+
if (opts.timeScale !== void 0) entry.timeScale = opts.timeScale;
|
|
987
|
+
children.push(entry);
|
|
988
|
+
const scale = mode === "sync" ? opts.timeScale ?? 1 : 1;
|
|
989
|
+
prevStart = start;
|
|
990
|
+
prevEnd = start + compileTimeline(child).duration / scale;
|
|
991
|
+
return builder;
|
|
992
|
+
},
|
|
993
|
+
call(fn, at) {
|
|
994
|
+
const t = at === void 0 ? prevEnd : resolvePosition(at);
|
|
995
|
+
const name = `call:${callCount++}`;
|
|
996
|
+
markers.push({
|
|
997
|
+
t,
|
|
998
|
+
name
|
|
999
|
+
});
|
|
1000
|
+
callbacks.set(name, fn);
|
|
1001
|
+
return builder;
|
|
1002
|
+
},
|
|
1003
|
+
editable() {
|
|
1004
|
+
const last = insertions[insertions.length - 1];
|
|
1005
|
+
if (!last) throw new TimelineValidationError(".editable() requires a preceding insertion");
|
|
1006
|
+
last.editable = true;
|
|
1007
|
+
return builder;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
build(builder);
|
|
1011
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1012
|
+
for (const ins of insertions) {
|
|
1013
|
+
let list = byTarget.get(ins.target);
|
|
1014
|
+
if (!list) {
|
|
1015
|
+
list = [];
|
|
1016
|
+
byTarget.set(ins.target, list);
|
|
1017
|
+
}
|
|
1018
|
+
list.push(ins);
|
|
1019
|
+
}
|
|
1020
|
+
const tracks = [];
|
|
1021
|
+
for (const [target, list] of byTarget) {
|
|
1022
|
+
list.sort((a, b) => a.start - b.start);
|
|
1023
|
+
const keys = [];
|
|
1024
|
+
const editable = list.some((i) => i.editable);
|
|
1025
|
+
let prevValue = list.find((i) => i.baseValue !== void 0)?.baseValue;
|
|
1026
|
+
const first = list[0];
|
|
1027
|
+
if (first.kind === "tween" && first.explicitFrom === void 0 && prevValue === void 0) emitDevWarning(`'${target}': first tween has no resolvable from-value (string targets have no base) — the track sits at its end state before the tween. Anchor it with { from }, fromTo(), or set(..., { at: 0 }).`);
|
|
1028
|
+
for (const ins of list) {
|
|
1029
|
+
if (ins.kind === "set") {
|
|
1030
|
+
keys.push({
|
|
1031
|
+
t: ins.start,
|
|
1032
|
+
value: ins.value,
|
|
1033
|
+
interp: "hold"
|
|
1034
|
+
});
|
|
1035
|
+
prevValue = ins.value;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const from = ins.explicitFrom !== void 0 ? ins.explicitFrom : prevValue;
|
|
1039
|
+
const lastKey = keys[keys.length - 1];
|
|
1040
|
+
if (from !== void 0 && (!lastKey || lastKey.t < ins.start)) {
|
|
1041
|
+
const fromKey = {
|
|
1042
|
+
t: ins.start,
|
|
1043
|
+
value: from
|
|
1044
|
+
};
|
|
1045
|
+
if (ins.explicitFrom === void 0) fromKey.derived = true;
|
|
1046
|
+
if (!lastKey || ins.start > lastKey.t) keys.push(fromKey);
|
|
1047
|
+
}
|
|
1048
|
+
keys.push({
|
|
1049
|
+
t: ins.start + ins.duration,
|
|
1050
|
+
value: ins.value,
|
|
1051
|
+
ease: ins.ease
|
|
1052
|
+
});
|
|
1053
|
+
prevValue = ins.value;
|
|
1054
|
+
}
|
|
1055
|
+
const deduped = [];
|
|
1056
|
+
for (const k of keys.sort((a, b) => a.t - b.t)) {
|
|
1057
|
+
const last = deduped[deduped.length - 1];
|
|
1058
|
+
if (last && last.t === k.t) deduped[deduped.length - 1] = k;
|
|
1059
|
+
else deduped.push(k);
|
|
1060
|
+
}
|
|
1061
|
+
const tr = {
|
|
1062
|
+
target,
|
|
1063
|
+
type: inferValueType(list[0].value),
|
|
1064
|
+
keys: deduped
|
|
1065
|
+
};
|
|
1066
|
+
if (editable) tr.editable = true;
|
|
1067
|
+
tracks.push(tr);
|
|
1068
|
+
}
|
|
1069
|
+
const doc = timeline$1({
|
|
1070
|
+
...init,
|
|
1071
|
+
tracks,
|
|
1072
|
+
labels,
|
|
1073
|
+
...markers.length ? { markers } : {},
|
|
1074
|
+
...children.length ? { children: children.map(({ _pos, ...c }) => c) } : {}
|
|
1075
|
+
});
|
|
1076
|
+
if (callbacks.size) timelineCallbacks.set(doc, callbacks);
|
|
1077
|
+
return doc;
|
|
1078
|
+
}
|
|
1079
|
+
function timeline(arg, init) {
|
|
1080
|
+
return typeof arg === "function" ? buildTimeline(arg, init) : timeline$1(arg);
|
|
1081
|
+
}
|
|
1082
|
+
//#endregion
|
|
1083
|
+
//#region src/binding.ts
|
|
1084
|
+
/**
|
|
1085
|
+
* Playhead + timeline binding (DESIGN.md §2.4): animated properties are not
|
|
1086
|
+
* written each frame — binding rewires each targeted signal's source to
|
|
1087
|
+
* `() => sampleTrack(track, playhead())`, so evaluation stays pull-only and
|
|
1088
|
+
* unchanged samples don't propagate dirtiness.
|
|
1089
|
+
*/
|
|
1090
|
+
function createPlayhead(initial = 0) {
|
|
1091
|
+
return signal(initial);
|
|
1092
|
+
}
|
|
1093
|
+
var UnboundTargetError = class extends Error {
|
|
1094
|
+
constructor(target) {
|
|
1095
|
+
super(`timeline targets '${target}' but no property signal resolves to it`);
|
|
1096
|
+
this.name = "UnboundTargetError";
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
/**
|
|
1100
|
+
* Bind a compiled timeline's tracks to property signals. `resolve` returns
|
|
1101
|
+
* the signal for a target path, or undefined — which is a compile-time-style
|
|
1102
|
+
* error (§2.2: unbound tracks are build errors, not silent no-ops).
|
|
1103
|
+
*/
|
|
1104
|
+
function bindTimeline(compiled, resolve, playhead = createPlayhead()) {
|
|
1105
|
+
const bound = [];
|
|
1106
|
+
for (const [target, tr] of compiled.tracks) {
|
|
1107
|
+
const sig = resolve(target);
|
|
1108
|
+
if (!sig) throw new UnboundTargetError(target);
|
|
1109
|
+
sig.bindSource(() => sampleTrack(tr, playhead()));
|
|
1110
|
+
bound.push(sig);
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
playhead,
|
|
1114
|
+
unbind: () => {
|
|
1115
|
+
for (const sig of bound) sig.unbindSource();
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* The evaluation entry discipline (§2.5): the playhead write is the sanctioned
|
|
1121
|
+
* entry mutation, then the read phase begins and `read` must be pure.
|
|
1122
|
+
*/
|
|
1123
|
+
function evaluateAt(playhead, t, read) {
|
|
1124
|
+
playhead.forceSet(t);
|
|
1125
|
+
beginReadPhase();
|
|
1126
|
+
try {
|
|
1127
|
+
return read();
|
|
1128
|
+
} finally {
|
|
1129
|
+
endReadPhase();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/rng.ts
|
|
1134
|
+
/** Returns a deterministic [0,1) generator for the given integer seed. */
|
|
1135
|
+
function random(seed) {
|
|
1136
|
+
let a = seed >>> 0;
|
|
1137
|
+
return () => {
|
|
1138
|
+
a = a + 2654435769 | 0;
|
|
1139
|
+
let t = a ^ a >>> 16;
|
|
1140
|
+
t = Math.imul(t, 569420461);
|
|
1141
|
+
t = t ^ t >>> 15;
|
|
1142
|
+
t = Math.imul(t, 1935289751);
|
|
1143
|
+
t = t ^ t >>> 15;
|
|
1144
|
+
return (t >>> 0) / 4294967296;
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
//#endregion
|
|
1148
|
+
//#region src/bake.ts
|
|
1149
|
+
/**
|
|
1150
|
+
* bake() (DESIGN.md §2.8): stateful simulation as a compilation step. Run the
|
|
1151
|
+
* stepper ONCE — fixed dt, seeded RNG — and emit ordinary frame-indexed
|
|
1152
|
+
* Tracks; rendering stays a pure lookup and the §2.5 contract survives.
|
|
1153
|
+
* The checkpointed variant trades memory for bounded re-simulation.
|
|
1154
|
+
*/
|
|
1155
|
+
var BakeError = class extends Error {
|
|
1156
|
+
constructor(detail) {
|
|
1157
|
+
super(`bake(): ${detail}`);
|
|
1158
|
+
this.name = "BakeError";
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function emit(acc, sampled, t) {
|
|
1162
|
+
for (const [path, value] of Object.entries(sampled)) {
|
|
1163
|
+
let a = acc.get(path);
|
|
1164
|
+
if (!a) {
|
|
1165
|
+
a = {
|
|
1166
|
+
keys: [],
|
|
1167
|
+
type: inferValueType(value)
|
|
1168
|
+
};
|
|
1169
|
+
acc.set(path, a);
|
|
1170
|
+
}
|
|
1171
|
+
a.keys.push({
|
|
1172
|
+
t,
|
|
1173
|
+
value
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function toTracks(acc) {
|
|
1178
|
+
return [...acc.entries()].map(([target, a]) => ({
|
|
1179
|
+
target,
|
|
1180
|
+
type: a.type,
|
|
1181
|
+
keys: a.keys
|
|
1182
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
function bake(cfg) {
|
|
1185
|
+
if (!(cfg.fps > 0) || !(cfg.duration >= 0)) throw new BakeError("fps must be > 0 and duration >= 0");
|
|
1186
|
+
const rng = random(cfg.seed ?? 0);
|
|
1187
|
+
const dt = 1 / cfg.fps;
|
|
1188
|
+
const frames = Math.round(cfg.duration * cfg.fps);
|
|
1189
|
+
let world = cfg.setup(rng);
|
|
1190
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1191
|
+
emit(acc, cfg.sample(world), 0);
|
|
1192
|
+
for (let f = 1; f <= frames; f++) {
|
|
1193
|
+
world = cfg.step(world, dt, rng) ?? world;
|
|
1194
|
+
emit(acc, cfg.sample(world), f * dt);
|
|
1195
|
+
}
|
|
1196
|
+
return toTracks(acc);
|
|
1197
|
+
}
|
|
1198
|
+
function bakeCheckpointed(cfg) {
|
|
1199
|
+
if (!(cfg.every >= 1)) throw new BakeError("checkpoint interval `every` must be >= 1 frame");
|
|
1200
|
+
const dt = 1 / cfg.fps;
|
|
1201
|
+
const totalFrames = Math.round(cfg.duration * cfg.fps);
|
|
1202
|
+
let draws = 0;
|
|
1203
|
+
const baseRng = random(cfg.seed ?? 0);
|
|
1204
|
+
const countingRng = () => {
|
|
1205
|
+
draws++;
|
|
1206
|
+
return baseRng();
|
|
1207
|
+
};
|
|
1208
|
+
const rngAt = (targetDraws) => {
|
|
1209
|
+
const r = random(cfg.seed ?? 0);
|
|
1210
|
+
for (let i = 0; i < targetDraws; i++) r();
|
|
1211
|
+
return r;
|
|
1212
|
+
};
|
|
1213
|
+
let world = cfg.setup(countingRng);
|
|
1214
|
+
const checkpoints = [{
|
|
1215
|
+
snap: cfg.snapshot(world),
|
|
1216
|
+
rngDraws: draws
|
|
1217
|
+
}];
|
|
1218
|
+
let simulatedTo = 0;
|
|
1219
|
+
/** Advance the master simulation, recording a checkpoint every K frames. */
|
|
1220
|
+
function ensureSimulatedTo(frame) {
|
|
1221
|
+
while (simulatedTo < Math.min(frame, totalFrames)) {
|
|
1222
|
+
world = cfg.step(world, dt, countingRng) ?? world;
|
|
1223
|
+
simulatedTo++;
|
|
1224
|
+
if (simulatedTo % cfg.every === 0) checkpoints.push({
|
|
1225
|
+
snap: cfg.snapshot(world),
|
|
1226
|
+
rngDraws: draws
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return {
|
|
1231
|
+
frames: totalFrames,
|
|
1232
|
+
bakeRange(fromFrame, toFrame) {
|
|
1233
|
+
if (fromFrame < 0 || toFrame > totalFrames || fromFrame > toFrame) throw new BakeError(`range ${fromFrame}..${toFrame} outside 0..${totalFrames}`);
|
|
1234
|
+
const cpFrame = Math.floor(fromFrame / cfg.every) * cfg.every;
|
|
1235
|
+
ensureSimulatedTo(cpFrame);
|
|
1236
|
+
const cp = checkpoints[cpFrame / cfg.every];
|
|
1237
|
+
let w = cfg.restore(cp.snap);
|
|
1238
|
+
const rng = rngAt(cp.rngDraws);
|
|
1239
|
+
let frame = cpFrame;
|
|
1240
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1241
|
+
if (frame >= fromFrame) emit(acc, cfg.sample(w), frame * dt);
|
|
1242
|
+
while (frame < toFrame) {
|
|
1243
|
+
w = cfg.step(w, dt, rng) ?? w;
|
|
1244
|
+
frame++;
|
|
1245
|
+
if (frame >= fromFrame) emit(acc, cfg.sample(w), frame * dt);
|
|
1246
|
+
}
|
|
1247
|
+
return toTracks(acc);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/sidecar.ts
|
|
1253
|
+
/**
|
|
1254
|
+
* The editor sidecar (DESIGN.md §6.2): code declares scene structure and
|
|
1255
|
+
* programmatic tracks; the studio owns keyframe data persisted as a sidecar
|
|
1256
|
+
* document next to the scene module, merged at track granularity. Versioned
|
|
1257
|
+
* independently of the API (§7.4) — breaking it orphans users' files.
|
|
1258
|
+
*/
|
|
1259
|
+
var SidecarVersionError = class extends Error {
|
|
1260
|
+
constructor(version) {
|
|
1261
|
+
super(`unsupported sidecar version ${String(version)}; this build reads sidecarVersion 1`);
|
|
1262
|
+
this.name = "SidecarVersionError";
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
function emptySidecar() {
|
|
1266
|
+
return {
|
|
1267
|
+
sidecarVersion: 1,
|
|
1268
|
+
tracks: []
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Editor-edit normalization (§2.7 invariant): a spring-eased key's t is
|
|
1273
|
+
* intrinsic — prev.t + spring.duration(cfg) — so after any retime, sort and
|
|
1274
|
+
* re-pin spring keys to their predecessors. Dragging a spring key itself
|
|
1275
|
+
* therefore snaps back; retiming its predecessor carries it along. Returns a
|
|
1276
|
+
* new array. Colliding keys are NUDGED apart (+1ms), never deleted — an
|
|
1277
|
+
* editor must not silently destroy keyframe data on an exact-t collision.
|
|
1278
|
+
*/
|
|
1279
|
+
function normalizeEditedKeys(keys) {
|
|
1280
|
+
const out = keys.map((k) => ({ ...k })).sort((a, b) => a.t - b.t);
|
|
1281
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
1282
|
+
for (let i = 1; i < out.length; i++) {
|
|
1283
|
+
const ease = out[i].ease;
|
|
1284
|
+
if (ease && typeof ease === "object" && ease.kind === "spring") out[i].t = out[i - 1].t + spring.duration(ease);
|
|
1285
|
+
}
|
|
1286
|
+
out.sort((a, b) => a.t - b.t);
|
|
1287
|
+
}
|
|
1288
|
+
for (let i = 1; i < out.length; i++) if (out[i].t <= out[i - 1].t) out[i].t = out[i - 1].t + .001;
|
|
1289
|
+
return out;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Merge rules (§6.2, schema-drift resolution):
|
|
1293
|
+
* - a sidecar track whose target exists in code REPLACES that track's keys
|
|
1294
|
+
* (the editor owns it; `editable` is preserved on the result);
|
|
1295
|
+
* - a sidecar track with no code counterpart is ADDED (editor-created track);
|
|
1296
|
+
* - sidecar tracks targeting nothing the scene can bind fail later at
|
|
1297
|
+
* bindTimeline with UnboundTargetError — surfaced, never silently dropped;
|
|
1298
|
+
* - code tracks without sidecar counterparts pass through untouched.
|
|
1299
|
+
* The input documents are not mutated.
|
|
1300
|
+
*/
|
|
1301
|
+
function mergeSidecar(code, sidecar) {
|
|
1302
|
+
if (!sidecar) return code;
|
|
1303
|
+
if (sidecar.sidecarVersion !== 1) throw new SidecarVersionError(sidecar.sidecarVersion);
|
|
1304
|
+
const overlay = new Map(sidecar.tracks.map((t) => [t.target, t]));
|
|
1305
|
+
const tracks = code.tracks.map((t) => {
|
|
1306
|
+
const replacement = overlay.get(t.target);
|
|
1307
|
+
if (!replacement) return t;
|
|
1308
|
+
overlay.delete(t.target);
|
|
1309
|
+
return {
|
|
1310
|
+
...t,
|
|
1311
|
+
keys: replacement.keys.map((k) => ({ ...k })),
|
|
1312
|
+
editable: true
|
|
1313
|
+
};
|
|
1314
|
+
});
|
|
1315
|
+
for (const added of overlay.values()) tracks.push({
|
|
1316
|
+
...added,
|
|
1317
|
+
keys: added.keys.map((k) => ({ ...k })),
|
|
1318
|
+
editable: true
|
|
1319
|
+
});
|
|
1320
|
+
const merged = {
|
|
1321
|
+
...code,
|
|
1322
|
+
tracks
|
|
1323
|
+
};
|
|
1324
|
+
if (sidecar.labels && Object.keys(sidecar.labels).length > 0) merged.labels = {
|
|
1325
|
+
...code.labels,
|
|
1326
|
+
...sidecar.labels
|
|
1327
|
+
};
|
|
1328
|
+
return merged;
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1331
|
+
export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, easings, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, random, registerValueType, resolveEase, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type };
|