@glissade/core 0.19.0 → 0.20.0-pre.3
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 +22 -4
- package/dist/index.js +12 -6
- package/dist/sidecar.d.ts +1 -1
- package/dist/sidecar.js +2 -188
- package/dist/studioHost.d.ts +1 -1
- package/dist/studioHost.js +1 -1
- package/dist/timeline.js +188 -0
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { $ as cubicBezier, A as getValueType, B as vec2Equals, C as UnknownValueTypeError, D as Vec2, E as ValueTypeInferenceError, F as pathType, G as spring, H as RetargetSpring, I as registerValueType, J as springPresets, K as springEasing, L as reprOf, M as listValueTypes, N as numberType, O as booleanType, P as paintType, Q as UnknownEasingError, R as stringType, S as PathValue, T as ValueTypeId, U as SpringConfig, V as vec2Type, W as SpringEase, X as EaseSpec, Y as DEFAULT_EASE, Z as EasingFn, _ as MeshInterpolation, a as key, b as Paint, c as sampleTrack, d as track, et as cubicBezierDerivative, f as validateTrack, g as HandoffKind, h as GradientInterpolation, i as TrackValidationError, j as inferValueType, k as colorType, l as springTo, m as ColorStop, n as KeyOpts, nt as easings, o as resolveEase, p as velocityAt, q as springEasingDerivative, r as Track, rt as namedEasing, s as resolveEaseDerivative, t as Key, tt as easingDerivatives, u as stagger, v as MeshPaint, w as ValueType, x as PathContour, y as MeshPoint, z as vec2ArcType } from "./track.js";
|
|
2
2
|
import { a as isEditableNodeId, i as UnresolvableTargetError, n as TargetCarrier, o as resolveTweenTarget, r as TweenTarget, s as targetNodeId, t as TARGET_PATH } from "./targetRef.js";
|
|
3
3
|
import { _ as setDevWarning, a as FontFaceRef, c as Marker, d as TimelineValidationError, f as audioOffsetSamples, g as emitDevWarning, h as DevWarning, i as CompiledTimeline, l as Timeline, m as isDurationEditable, n as AudioClip, o as GainEnvelope, p as compileTimeline, r as ChildEntry, s as Json, t as AssetRef, u as TimelineInit } from "./timeline.js";
|
|
4
|
-
import { _ as setSidecarTrack, a as SidecarOrphan, c as SidecarVersionError, d as emptySidecar, f as hashKeys, g as normalizeEditedKeys, h as migrateSidecar, i as SidecarDocV1, l as assignKeyIds, m as mergeSidecarDetailed, n as OrphanReason, o as SidecarTimelineEntry, p as mergeSidecar, r as SidecarDoc, s as SidecarTrackEntry, t as MergeResult, u as deleteSidecarTrack } from "./sidecar.js";
|
|
5
4
|
|
|
6
5
|
//#region src/signal.d.ts
|
|
7
6
|
|
|
@@ -374,7 +373,13 @@ declare function timeline(build: (tl: TimelineBuilder) => void, init?: Omit<Time
|
|
|
374
373
|
type Playhead = BindableSignal<number>;
|
|
375
374
|
declare function createPlayhead(initial?: number): Playhead;
|
|
376
375
|
declare class UnboundTargetError extends Error {
|
|
377
|
-
|
|
376
|
+
/**
|
|
377
|
+
* `message` overrides the default generic text — the seam a layer with more
|
|
378
|
+
* context (e.g. `scene`, which knows the node's construction-prop schema) uses
|
|
379
|
+
* to throw a friendlier, more specific reason while keeping the same error
|
|
380
|
+
* type (so existing `instanceof UnboundTargetError` catches still fire).
|
|
381
|
+
*/
|
|
382
|
+
constructor(target: string, message?: string);
|
|
378
383
|
}
|
|
379
384
|
/**
|
|
380
385
|
* A track's value type doesn't match the shape of the property it targets — the
|
|
@@ -421,12 +426,25 @@ interface BoundTimeline {
|
|
|
421
426
|
/** Detach every track binding, freezing signals at their last values. */
|
|
422
427
|
unbind(): void;
|
|
423
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Optional knobs for {@link bindTimeline}.
|
|
431
|
+
*/
|
|
432
|
+
interface BindOptions {
|
|
433
|
+
/**
|
|
434
|
+
* Asked for a friendlier message when a target fails to resolve. `core` knows
|
|
435
|
+
* nothing about node types or construction props; a caller with that context
|
|
436
|
+
* (e.g. `scene`) can return a more-specific reason string — used verbatim as
|
|
437
|
+
* the {@link UnboundTargetError} message — or `undefined` to fall through to
|
|
438
|
+
* the generic "no property signal resolves to it".
|
|
439
|
+
*/
|
|
440
|
+
unboundMessage?: (target: string) => string | undefined;
|
|
441
|
+
}
|
|
424
442
|
/**
|
|
425
443
|
* Bind a compiled timeline's tracks to property signals. `resolve` returns
|
|
426
444
|
* the signal for a target path, or undefined — which is a compile-time-style
|
|
427
445
|
* error (§2.2: unbound tracks are build errors, not silent no-ops).
|
|
428
446
|
*/
|
|
429
|
-
declare function bindTimeline(compiled: CompiledTimeline, resolve: (target: string) => BindTarget | undefined, playhead?: Playhead): BoundTimeline;
|
|
447
|
+
declare function bindTimeline(compiled: CompiledTimeline, resolve: (target: string) => BindTarget | undefined, playhead?: Playhead, options?: BindOptions): BoundTimeline;
|
|
430
448
|
/**
|
|
431
449
|
* The evaluation entry discipline (§2.5): the playhead write is the sanctioned
|
|
432
450
|
* entry mutation, then the read phase begins and `read` must be pure.
|
|
@@ -477,4 +495,4 @@ interface CheckpointedSim {
|
|
|
477
495
|
}
|
|
478
496
|
declare function bakeCheckpointed<W, S = W>(cfg: CheckpointedBakeConfig<W, S>): CheckpointedSim;
|
|
479
497
|
//#endregion
|
|
480
|
-
export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, BindTypeMismatchError, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type ColorStop, type CompiledTimeline, type CoverageReport, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type FontFaceRef, type FontMode, type FontRegistry, type FontUsage, FontValidationError, type GainEnvelope, type GradientInterpolation, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type
|
|
498
|
+
export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindOptions, type BindTarget, BindTypeMismatchError, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type ColorStop, type CompiledTimeline, type CoverageReport, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type FontFaceRef, type FontMode, type FontRegistry, type FontUsage, FontValidationError, type GainEnvelope, type GradientInterpolation, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type MeshInterpolation, type MeshPaint, type MeshPoint, type MissingGlyphs, type OkLab, type Paint, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type ResolvedFace, type RetargetSpring, type Rgba, type Rng, type Scheduler, type Signal, type SignalOptions, type SpringConfig, type SpringEase, type StaggerOpts, type StaggerSpec, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValidateFontsOptions, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Component, type Vec2Signal, WriteDuringEvaluationError, audioOffsetSamples, bake, bakeCheckpointed, batch, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, listValueTypes, namedEasing, numberType, oklabToRgba, paintType, parseCmap, parseColor, pathType, random, registerValueType, reprOf, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setScheduler, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, synchronousScheduler, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as vec2Equals, B as spring, C as numberType, D as reprOf, E as registerValueType, F as oklabToRgba, G as UnknownEasingError, H as springEasingDerivative, I as parseColor, J as easingDerivatives, K as cubicBezier, L as rgbaToOklab, M as ColorParseError, N as formatColor, O as stringType, P as lerpColor, R as emitDevWarning, S as listValueTypes, T as pathType, U as springPresets, V as springEasing, W as DEFAULT_EASE, X as namedEasing, Y as easings, _ as ValueTypeInferenceError, a as targetNodeId, b as getValueType, c as resolveEase, d as springTo, f as stagger, g as UnknownValueTypeError, h as velocityAt, i as resolveTweenTarget, j as vec2Type, k as vec2ArcType, l as resolveEaseDerivative, m as validateTrack, n as UnresolvableTargetError, o as TrackValidationError, p as track, q as cubicBezierDerivative, r as isEditableNodeId, s as key, t as TARGET_PATH, u as sampleTrack, v as booleanType, w as paintType, x as inferValueType, y as colorType, z as setDevWarning } from "./targetRef.js";
|
|
2
2
|
import { t as parseCmap } from "./cmap.js";
|
|
3
|
-
import {
|
|
3
|
+
import { a as isDurationEditable, i as compileTimeline, n as audioOffsetSamples, o as namespaceCallName, r as callMarkerPrefix, s as timeline$1, t as TimelineValidationError } from "./timeline.js";
|
|
4
4
|
//#region src/ticker.ts
|
|
5
5
|
/** The default scheduler: flush synchronously, preserving pre-ticker timing. */
|
|
6
6
|
const synchronousScheduler = (flush) => flush();
|
|
@@ -914,8 +914,14 @@ function createPlayhead(initial = 0) {
|
|
|
914
914
|
return signal(initial);
|
|
915
915
|
}
|
|
916
916
|
var UnboundTargetError = class extends Error {
|
|
917
|
-
|
|
918
|
-
|
|
917
|
+
/**
|
|
918
|
+
* `message` overrides the default generic text — the seam a layer with more
|
|
919
|
+
* context (e.g. `scene`, which knows the node's construction-prop schema) uses
|
|
920
|
+
* to throw a friendlier, more specific reason while keeping the same error
|
|
921
|
+
* type (so existing `instanceof UnboundTargetError` catches still fire).
|
|
922
|
+
*/
|
|
923
|
+
constructor(target, message) {
|
|
924
|
+
super(message ?? `timeline targets '${target}' but no property signal resolves to it`);
|
|
919
925
|
this.name = "UnboundTargetError";
|
|
920
926
|
}
|
|
921
927
|
};
|
|
@@ -948,12 +954,12 @@ var BindTypeMismatchError = class extends Error {
|
|
|
948
954
|
* the signal for a target path, or undefined — which is a compile-time-style
|
|
949
955
|
* error (§2.2: unbound tracks are build errors, not silent no-ops).
|
|
950
956
|
*/
|
|
951
|
-
function bindTimeline(compiled, resolve, playhead = createPlayhead()) {
|
|
957
|
+
function bindTimeline(compiled, resolve, playhead = createPlayhead(), options = {}) {
|
|
952
958
|
const bound = [];
|
|
953
959
|
const samplers = /* @__PURE__ */ new Map();
|
|
954
960
|
for (const [target, tr] of compiled.tracks) {
|
|
955
961
|
const sig = resolve(target);
|
|
956
|
-
if (!sig) throw new UnboundTargetError(target);
|
|
962
|
+
if (!sig) throw new UnboundTargetError(target, options.unboundMessage?.(target));
|
|
957
963
|
const got = tr.type;
|
|
958
964
|
const expects = sig.expects;
|
|
959
965
|
if (expects !== void 0) {
|
|
@@ -1109,4 +1115,4 @@ function bakeCheckpointed(cfg) {
|
|
|
1109
1115
|
};
|
|
1110
1116
|
}
|
|
1111
1117
|
//#endregion
|
|
1112
|
-
export { BakeError, BindTypeMismatchError, CircularDependencyError, ColorParseError, DEFAULT_EASE, FontValidationError, PositionError,
|
|
1118
|
+
export { BakeError, BindTypeMismatchError, CircularDependencyError, ColorParseError, DEFAULT_EASE, FontValidationError, PositionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, audioOffsetSamples, bake, bakeCheckpointed, batch, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, listValueTypes, namedEasing, numberType, oklabToRgba, paintType, parseCmap, parseColor, pathType, random, registerValueType, reprOf, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setScheduler, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, synchronousScheduler, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
|
package/dist/sidecar.d.ts
CHANGED
|
@@ -85,4 +85,4 @@ declare function mergeSidecar(code: Timeline, sidecar: SidecarDoc | SidecarDocV1
|
|
|
85
85
|
*/
|
|
86
86
|
declare function normalizeEditedKeys(keys: Key[]): Key[];
|
|
87
87
|
//#endregion
|
|
88
|
-
export {
|
|
88
|
+
export { MergeResult, OrphanReason, SidecarDoc, SidecarDocV1, SidecarOrphan, SidecarTimelineEntry, SidecarTrackEntry, SidecarVersionError, assignKeyIds, deleteSidecarTrack, emptySidecar, hashKeys, mergeSidecar, mergeSidecarDetailed, migrateSidecar, normalizeEditedKeys, setSidecarTrack };
|
package/dist/sidecar.js
CHANGED
|
@@ -1,190 +1,4 @@
|
|
|
1
|
-
import { B as spring, R as emitDevWarning, a as targetNodeId,
|
|
2
|
-
//#region src/timeline.ts
|
|
3
|
-
/**
|
|
4
|
-
* The Timeline document (DESIGN.md §2.3) — the serializable animation source
|
|
5
|
-
* of truth — and its compiler: child flattening (add/sync), same-target
|
|
6
|
-
* coalescing (last-insertion-wins, §2.2), duration computation, validation.
|
|
7
|
-
*/
|
|
8
|
-
function timeline(init) {
|
|
9
|
-
const doc = {
|
|
10
|
-
version: 1,
|
|
11
|
-
tracks: init.tracks ?? []
|
|
12
|
-
};
|
|
13
|
-
if (init.duration !== void 0) doc.duration = init.duration;
|
|
14
|
-
if (init.editableDuration !== void 0) doc.editableDuration = init.editableDuration;
|
|
15
|
-
if (init.fps !== void 0) doc.fps = init.fps;
|
|
16
|
-
if (init.posterTime !== void 0) doc.posterTime = init.posterTime;
|
|
17
|
-
if (init.labels !== void 0) doc.labels = init.labels;
|
|
18
|
-
if (init.markers !== void 0) doc.markers = init.markers;
|
|
19
|
-
if (init.children !== void 0) doc.children = init.children;
|
|
20
|
-
if (init.audio !== void 0) doc.audio = init.audio;
|
|
21
|
-
if (init.assets !== void 0) doc.assets = init.assets;
|
|
22
|
-
return doc;
|
|
23
|
-
}
|
|
24
|
-
var TimelineValidationError = class extends Error {
|
|
25
|
-
constructor(message) {
|
|
26
|
-
super(message);
|
|
27
|
-
this.name = "TimelineValidationError";
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
/** Spring key rule (§2.7): a spring-eased key's t must equal prev.t + spring.duration(cfg). */
|
|
31
|
-
function validateSpringKeys(tr) {
|
|
32
|
-
for (let i = 1; i < tr.keys.length; i++) {
|
|
33
|
-
const k = tr.keys[i];
|
|
34
|
-
if (k.ease && typeof k.ease === "object" && k.ease.kind === "spring") {
|
|
35
|
-
const expected = tr.keys[i - 1].t + spring.duration(k.ease);
|
|
36
|
-
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`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Sample-indexed clip offset — the single A/V-sync source of truth (§5.3),
|
|
42
|
-
* `round(at * sampleRate)`. Both the CLI (FFmpeg) and the browser
|
|
43
|
-
* (OfflineAudioContext) mixers derive their delay from this, so A/V offsets
|
|
44
|
-
* agree across paths by construction rather than one rounding to milliseconds
|
|
45
|
-
* and the other to raw float seconds. Default rate is the canonical mix grid.
|
|
46
|
-
*/
|
|
47
|
-
function audioOffsetSamples(at, sampleRate = 48e3) {
|
|
48
|
-
return Math.round(at * sampleRate);
|
|
49
|
-
}
|
|
50
|
-
function rebaseKeys(keys, at, timeScale) {
|
|
51
|
-
return keys.map((k) => ({
|
|
52
|
-
...k,
|
|
53
|
-
t: at + k.t / timeScale
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* `.call()` markers carry an auto-assigned `call:N` name whose counter resets
|
|
58
|
-
* per document, so two sibling sub-timelines that each define a `.call()` would
|
|
59
|
-
* collide on `call:0` when rebased into one parent (one callback dropped, the
|
|
60
|
-
* other double-firing). We namespace a child's `call:*` markers by the child's
|
|
61
|
-
* position PATH in the parent (`c<index>/…`), and the builder's `add()` applies
|
|
62
|
-
* the EXACT same prefix when forwarding the child's name→fn map — so the rebased
|
|
63
|
-
* marker name and the registered callback key agree by construction. Only
|
|
64
|
-
* `call:*` names are rewritten; author-named cues keep their names. Both surfaces
|
|
65
|
-
* call these so the convention lives in one place.
|
|
66
|
-
*/
|
|
67
|
-
function callMarkerPrefix(childIndex) {
|
|
68
|
-
return `c${childIndex}/`;
|
|
69
|
-
}
|
|
70
|
-
const CALL = /(^|\/)call:\d+$/;
|
|
71
|
-
function namespaceCallName(name, prefix) {
|
|
72
|
-
return prefix !== "" && CALL.test(name) ? prefix + name : name;
|
|
73
|
-
}
|
|
74
|
-
function flatten(doc, at, timeScale, unit, out, counter) {
|
|
75
|
-
for (const tr of doc.tracks) {
|
|
76
|
-
validateTrack(tr);
|
|
77
|
-
validateSpringKeys(tr);
|
|
78
|
-
out.push({
|
|
79
|
-
track: {
|
|
80
|
-
...tr,
|
|
81
|
-
keys: rebaseKeys(tr.keys, at, timeScale)
|
|
82
|
-
},
|
|
83
|
-
unit
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
for (const child of doc.children ?? []) {
|
|
87
|
-
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
88
|
-
if (child.mode === "add" && child.timeScale !== void 0) throw new TimelineValidationError("timeScale is only valid on mode:'sync' children (§2.3)");
|
|
89
|
-
if (scale <= 0) throw new TimelineValidationError("sync timeScale must be > 0");
|
|
90
|
-
const childUnit = child.mode === "sync" ? ++counter.n : unit;
|
|
91
|
-
flatten(child.timeline, at + child.at / timeScale, timeScale * scale, childUnit, out, counter);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Coalesce same-target tracks: later insertion wins where key ranges overlap
|
|
96
|
-
* (§2.2/§2.6 decided rule), with a dev warning. Earlier keys inside the later
|
|
97
|
-
* track's [first.t, last.t] span are dropped.
|
|
98
|
-
*/
|
|
99
|
-
function coalesce(entries) {
|
|
100
|
-
const byTarget = /* @__PURE__ */ new Map();
|
|
101
|
-
for (const { track: tr, unit } of entries) {
|
|
102
|
-
const existing = byTarget.get(tr.target);
|
|
103
|
-
if (!existing) {
|
|
104
|
-
byTarget.set(tr.target, {
|
|
105
|
-
track: {
|
|
106
|
-
...tr,
|
|
107
|
-
keys: [...tr.keys]
|
|
108
|
-
},
|
|
109
|
-
unit
|
|
110
|
-
});
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (existing.unit !== unit) throw new TimelineValidationError(`target '${tr.target}' is animated by a sync (opaque) child and another timeline unit; sync children must own disjoint targets (no last-writer-wins across the sync boundary, §2.3)`);
|
|
114
|
-
if (existing.track.type !== tr.type) throw new TimelineValidationError(`target '${tr.target}' has conflicting value types '${existing.track.type}' and '${tr.type}'`);
|
|
115
|
-
const start = tr.keys[0].t;
|
|
116
|
-
const end = tr.keys[tr.keys.length - 1].t;
|
|
117
|
-
const existingStart = existing.track.keys[0].t;
|
|
118
|
-
const existingEnd = existing.track.keys[existing.track.keys.length - 1].t;
|
|
119
|
-
const kept = existing.track.keys.filter((k) => k.t < start || k.t > end);
|
|
120
|
-
if (existingStart <= end && start <= existingEnd) emitDevWarning(`overlapping tracks for '${tr.target}' in [${start}, ${end}]: later insertion wins (${existing.track.keys.length - kept.length} earlier key(s) dropped)`);
|
|
121
|
-
existing.track.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
|
|
122
|
-
}
|
|
123
|
-
const result = /* @__PURE__ */ new Map();
|
|
124
|
-
for (const [target, { track }] of byTarget) result.set(target, track);
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Studio reader (§6.2 rule 4): is this timeline's duration opted into editor
|
|
129
|
-
* editing? Code-owned and read-only by default; `editableDuration()` flips it.
|
|
130
|
-
*/
|
|
131
|
-
function isDurationEditable(doc) {
|
|
132
|
-
return doc.editableDuration === true;
|
|
133
|
-
}
|
|
134
|
-
function childExtent(child) {
|
|
135
|
-
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
136
|
-
return child.at + computeDuration(child.timeline) / scale;
|
|
137
|
-
}
|
|
138
|
-
function computeDuration(doc) {
|
|
139
|
-
if (doc.duration !== void 0) return doc.duration;
|
|
140
|
-
let max = 0;
|
|
141
|
-
for (const tr of doc.tracks) {
|
|
142
|
-
const last = tr.keys[tr.keys.length - 1];
|
|
143
|
-
if (last) max = Math.max(max, last.t);
|
|
144
|
-
}
|
|
145
|
-
for (const m of doc.markers ?? []) max = Math.max(max, m.t);
|
|
146
|
-
for (const child of doc.children ?? []) max = Math.max(max, childExtent(child));
|
|
147
|
-
return max;
|
|
148
|
-
}
|
|
149
|
-
function compileTimeline(doc) {
|
|
150
|
-
if (doc.version !== 1) throw new TimelineValidationError(`unsupported timeline document version ${String(doc.version)}`);
|
|
151
|
-
const flat = [];
|
|
152
|
-
flatten(doc, 0, 1, 0, flat, { n: 0 });
|
|
153
|
-
const tracks = coalesce(flat);
|
|
154
|
-
const labels = { ...doc.labels };
|
|
155
|
-
const markers = [...doc.markers ?? []];
|
|
156
|
-
const audio = [...doc.audio ?? []];
|
|
157
|
-
const visitChildren = (children, at, scale, prefix) => {
|
|
158
|
-
(children ?? []).forEach((child, index) => {
|
|
159
|
-
const base = at + child.at / scale;
|
|
160
|
-
const childScale = scale * (child.mode === "sync" ? child.timeScale ?? 1 : 1);
|
|
161
|
-
const childPrefix = prefix + callMarkerPrefix(index);
|
|
162
|
-
for (const [name, t] of Object.entries(child.timeline.labels ?? {})) if (!(name in labels)) labels[name] = base + t / childScale;
|
|
163
|
-
for (const m of child.timeline.markers ?? []) markers.push({
|
|
164
|
-
...m,
|
|
165
|
-
name: namespaceCallName(m.name, childPrefix),
|
|
166
|
-
t: base + m.t / childScale
|
|
167
|
-
});
|
|
168
|
-
for (const clip of child.timeline.audio ?? []) audio.push({
|
|
169
|
-
...clip,
|
|
170
|
-
at: base + clip.at / childScale,
|
|
171
|
-
...childScale !== 1 ? { playbackRate: (clip.playbackRate ?? 1) * childScale } : {}
|
|
172
|
-
});
|
|
173
|
-
visitChildren(child.timeline.children, base, childScale, childPrefix);
|
|
174
|
-
});
|
|
175
|
-
};
|
|
176
|
-
visitChildren(doc.children, 0, 1, "");
|
|
177
|
-
markers.sort((a, b) => a.t - b.t);
|
|
178
|
-
audio.sort((a, b) => a.at - b.at);
|
|
179
|
-
return {
|
|
180
|
-
duration: computeDuration(doc),
|
|
181
|
-
labels,
|
|
182
|
-
markers,
|
|
183
|
-
tracks,
|
|
184
|
-
audio
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
//#endregion
|
|
1
|
+
import { B as spring, R as emitDevWarning, a as targetNodeId, o as TrackValidationError, r as isEditableNodeId } from "./targetRef.js";
|
|
188
2
|
//#region src/sidecar.ts
|
|
189
3
|
/**
|
|
190
4
|
* The editor sidecar (DESIGN.md §6.2): code declares scene structure and
|
|
@@ -411,4 +225,4 @@ function normalizeEditedKeys(keys) {
|
|
|
411
225
|
return out;
|
|
412
226
|
}
|
|
413
227
|
//#endregion
|
|
414
|
-
export {
|
|
228
|
+
export { SidecarVersionError, assignKeyIds, deleteSidecarTrack, emptySidecar, hashKeys, mergeSidecar, mergeSidecarDetailed, migrateSidecar, normalizeEditedKeys, setSidecarTrack };
|
package/dist/studioHost.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { T as ValueTypeId, X as EaseSpec, t as Key } from "./track.js";
|
|
2
2
|
import { a as isEditableNodeId, s as targetNodeId } from "./targetRef.js";
|
|
3
3
|
import { l as Timeline } from "./timeline.js";
|
|
4
|
-
import {
|
|
4
|
+
import { SidecarDoc, SidecarOrphan } from "./sidecar.js";
|
|
5
5
|
|
|
6
6
|
//#region src/timelinePatch.d.ts
|
|
7
7
|
|
package/dist/studioHost.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as targetNodeId, r as isEditableNodeId } from "./targetRef.js";
|
|
2
|
-
import {
|
|
2
|
+
import { assignKeyIds, hashKeys, normalizeEditedKeys } from "./sidecar.js";
|
|
3
3
|
//#region src/timelinePatch.ts
|
|
4
4
|
/** Unambiguous composite map key (no control-char separators). */
|
|
5
5
|
function refKey(timelineId, name) {
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { B as spring, R as emitDevWarning, m as validateTrack } from "./targetRef.js";
|
|
2
|
+
//#region src/timeline.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Timeline document (DESIGN.md §2.3) — the serializable animation source
|
|
5
|
+
* of truth — and its compiler: child flattening (add/sync), same-target
|
|
6
|
+
* coalescing (last-insertion-wins, §2.2), duration computation, validation.
|
|
7
|
+
*/
|
|
8
|
+
function timeline(init) {
|
|
9
|
+
const doc = {
|
|
10
|
+
version: 1,
|
|
11
|
+
tracks: init.tracks ?? []
|
|
12
|
+
};
|
|
13
|
+
if (init.duration !== void 0) doc.duration = init.duration;
|
|
14
|
+
if (init.editableDuration !== void 0) doc.editableDuration = init.editableDuration;
|
|
15
|
+
if (init.fps !== void 0) doc.fps = init.fps;
|
|
16
|
+
if (init.posterTime !== void 0) doc.posterTime = init.posterTime;
|
|
17
|
+
if (init.labels !== void 0) doc.labels = init.labels;
|
|
18
|
+
if (init.markers !== void 0) doc.markers = init.markers;
|
|
19
|
+
if (init.children !== void 0) doc.children = init.children;
|
|
20
|
+
if (init.audio !== void 0) doc.audio = init.audio;
|
|
21
|
+
if (init.assets !== void 0) doc.assets = init.assets;
|
|
22
|
+
return doc;
|
|
23
|
+
}
|
|
24
|
+
var TimelineValidationError = class extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "TimelineValidationError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** Spring key rule (§2.7): a spring-eased key's t must equal prev.t + spring.duration(cfg). */
|
|
31
|
+
function validateSpringKeys(tr) {
|
|
32
|
+
for (let i = 1; i < tr.keys.length; i++) {
|
|
33
|
+
const k = tr.keys[i];
|
|
34
|
+
if (k.ease && typeof k.ease === "object" && k.ease.kind === "spring") {
|
|
35
|
+
const expected = tr.keys[i - 1].t + spring.duration(k.ease);
|
|
36
|
+
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`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sample-indexed clip offset — the single A/V-sync source of truth (§5.3),
|
|
42
|
+
* `round(at * sampleRate)`. Both the CLI (FFmpeg) and the browser
|
|
43
|
+
* (OfflineAudioContext) mixers derive their delay from this, so A/V offsets
|
|
44
|
+
* agree across paths by construction rather than one rounding to milliseconds
|
|
45
|
+
* and the other to raw float seconds. Default rate is the canonical mix grid.
|
|
46
|
+
*/
|
|
47
|
+
function audioOffsetSamples(at, sampleRate = 48e3) {
|
|
48
|
+
return Math.round(at * sampleRate);
|
|
49
|
+
}
|
|
50
|
+
function rebaseKeys(keys, at, timeScale) {
|
|
51
|
+
return keys.map((k) => ({
|
|
52
|
+
...k,
|
|
53
|
+
t: at + k.t / timeScale
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `.call()` markers carry an auto-assigned `call:N` name whose counter resets
|
|
58
|
+
* per document, so two sibling sub-timelines that each define a `.call()` would
|
|
59
|
+
* collide on `call:0` when rebased into one parent (one callback dropped, the
|
|
60
|
+
* other double-firing). We namespace a child's `call:*` markers by the child's
|
|
61
|
+
* position PATH in the parent (`c<index>/…`), and the builder's `add()` applies
|
|
62
|
+
* the EXACT same prefix when forwarding the child's name→fn map — so the rebased
|
|
63
|
+
* marker name and the registered callback key agree by construction. Only
|
|
64
|
+
* `call:*` names are rewritten; author-named cues keep their names. Both surfaces
|
|
65
|
+
* call these so the convention lives in one place.
|
|
66
|
+
*/
|
|
67
|
+
function callMarkerPrefix(childIndex) {
|
|
68
|
+
return `c${childIndex}/`;
|
|
69
|
+
}
|
|
70
|
+
const CALL = /(^|\/)call:\d+$/;
|
|
71
|
+
function namespaceCallName(name, prefix) {
|
|
72
|
+
return prefix !== "" && CALL.test(name) ? prefix + name : name;
|
|
73
|
+
}
|
|
74
|
+
function flatten(doc, at, timeScale, unit, out, counter) {
|
|
75
|
+
for (const tr of doc.tracks) {
|
|
76
|
+
validateTrack(tr);
|
|
77
|
+
validateSpringKeys(tr);
|
|
78
|
+
out.push({
|
|
79
|
+
track: {
|
|
80
|
+
...tr,
|
|
81
|
+
keys: rebaseKeys(tr.keys, at, timeScale)
|
|
82
|
+
},
|
|
83
|
+
unit
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const child of doc.children ?? []) {
|
|
87
|
+
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
88
|
+
if (child.mode === "add" && child.timeScale !== void 0) throw new TimelineValidationError("timeScale is only valid on mode:'sync' children (§2.3)");
|
|
89
|
+
if (scale <= 0) throw new TimelineValidationError("sync timeScale must be > 0");
|
|
90
|
+
const childUnit = child.mode === "sync" ? ++counter.n : unit;
|
|
91
|
+
flatten(child.timeline, at + child.at / timeScale, timeScale * scale, childUnit, out, counter);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Coalesce same-target tracks: later insertion wins where key ranges overlap
|
|
96
|
+
* (§2.2/§2.6 decided rule), with a dev warning. Earlier keys inside the later
|
|
97
|
+
* track's [first.t, last.t] span are dropped.
|
|
98
|
+
*/
|
|
99
|
+
function coalesce(entries) {
|
|
100
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const { track: tr, unit } of entries) {
|
|
102
|
+
const existing = byTarget.get(tr.target);
|
|
103
|
+
if (!existing) {
|
|
104
|
+
byTarget.set(tr.target, {
|
|
105
|
+
track: {
|
|
106
|
+
...tr,
|
|
107
|
+
keys: [...tr.keys]
|
|
108
|
+
},
|
|
109
|
+
unit
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (existing.unit !== unit) throw new TimelineValidationError(`target '${tr.target}' is animated by a sync (opaque) child and another timeline unit; sync children must own disjoint targets (no last-writer-wins across the sync boundary, §2.3)`);
|
|
114
|
+
if (existing.track.type !== tr.type) throw new TimelineValidationError(`target '${tr.target}' has conflicting value types '${existing.track.type}' and '${tr.type}'`);
|
|
115
|
+
const start = tr.keys[0].t;
|
|
116
|
+
const end = tr.keys[tr.keys.length - 1].t;
|
|
117
|
+
const existingStart = existing.track.keys[0].t;
|
|
118
|
+
const existingEnd = existing.track.keys[existing.track.keys.length - 1].t;
|
|
119
|
+
const kept = existing.track.keys.filter((k) => k.t < start || k.t > end);
|
|
120
|
+
if (existingStart <= end && start <= existingEnd) emitDevWarning(`overlapping tracks for '${tr.target}' in [${start}, ${end}]: later insertion wins (${existing.track.keys.length - kept.length} earlier key(s) dropped)`);
|
|
121
|
+
existing.track.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
|
|
122
|
+
}
|
|
123
|
+
const result = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const [target, { track }] of byTarget) result.set(target, track);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Studio reader (§6.2 rule 4): is this timeline's duration opted into editor
|
|
129
|
+
* editing? Code-owned and read-only by default; `editableDuration()` flips it.
|
|
130
|
+
*/
|
|
131
|
+
function isDurationEditable(doc) {
|
|
132
|
+
return doc.editableDuration === true;
|
|
133
|
+
}
|
|
134
|
+
function childExtent(child) {
|
|
135
|
+
const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
|
|
136
|
+
return child.at + computeDuration(child.timeline) / scale;
|
|
137
|
+
}
|
|
138
|
+
function computeDuration(doc) {
|
|
139
|
+
if (doc.duration !== void 0) return doc.duration;
|
|
140
|
+
let max = 0;
|
|
141
|
+
for (const tr of doc.tracks) {
|
|
142
|
+
const last = tr.keys[tr.keys.length - 1];
|
|
143
|
+
if (last) max = Math.max(max, last.t);
|
|
144
|
+
}
|
|
145
|
+
for (const m of doc.markers ?? []) max = Math.max(max, m.t);
|
|
146
|
+
for (const child of doc.children ?? []) max = Math.max(max, childExtent(child));
|
|
147
|
+
return max;
|
|
148
|
+
}
|
|
149
|
+
function compileTimeline(doc) {
|
|
150
|
+
if (doc.version !== 1) throw new TimelineValidationError(`unsupported timeline document version ${String(doc.version)}`);
|
|
151
|
+
const flat = [];
|
|
152
|
+
flatten(doc, 0, 1, 0, flat, { n: 0 });
|
|
153
|
+
const tracks = coalesce(flat);
|
|
154
|
+
const labels = { ...doc.labels };
|
|
155
|
+
const markers = [...doc.markers ?? []];
|
|
156
|
+
const audio = [...doc.audio ?? []];
|
|
157
|
+
const visitChildren = (children, at, scale, prefix) => {
|
|
158
|
+
(children ?? []).forEach((child, index) => {
|
|
159
|
+
const base = at + child.at / scale;
|
|
160
|
+
const childScale = scale * (child.mode === "sync" ? child.timeScale ?? 1 : 1);
|
|
161
|
+
const childPrefix = prefix + callMarkerPrefix(index);
|
|
162
|
+
for (const [name, t] of Object.entries(child.timeline.labels ?? {})) if (!(name in labels)) labels[name] = base + t / childScale;
|
|
163
|
+
for (const m of child.timeline.markers ?? []) markers.push({
|
|
164
|
+
...m,
|
|
165
|
+
name: namespaceCallName(m.name, childPrefix),
|
|
166
|
+
t: base + m.t / childScale
|
|
167
|
+
});
|
|
168
|
+
for (const clip of child.timeline.audio ?? []) audio.push({
|
|
169
|
+
...clip,
|
|
170
|
+
at: base + clip.at / childScale,
|
|
171
|
+
...childScale !== 1 ? { playbackRate: (clip.playbackRate ?? 1) * childScale } : {}
|
|
172
|
+
});
|
|
173
|
+
visitChildren(child.timeline.children, base, childScale, childPrefix);
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
visitChildren(doc.children, 0, 1, "");
|
|
177
|
+
markers.sort((a, b) => a.t - b.t);
|
|
178
|
+
audio.sort((a, b) => a.at - b.at);
|
|
179
|
+
return {
|
|
180
|
+
duration: computeDuration(doc),
|
|
181
|
+
labels,
|
|
182
|
+
markers,
|
|
183
|
+
tracks,
|
|
184
|
+
audio
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { isDurationEditable as a, compileTimeline as i, audioOffsetSamples as n, namespaceCallName as o, callMarkerPrefix as r, timeline as s, TimelineValidationError as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0-pre.3",
|
|
4
4
|
"description": "glissade core: signals, tracks, timeline document, evaluation, easing, springs, seeded RNG. Zero DOM/Node dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"types": "./dist/clips.d.ts",
|
|
22
22
|
"default": "./dist/clips.js"
|
|
23
23
|
},
|
|
24
|
+
"./sidecar": {
|
|
25
|
+
"types": "./dist/sidecar.d.ts",
|
|
26
|
+
"default": "./dist/sidecar.js"
|
|
27
|
+
},
|
|
24
28
|
"./i18n": {
|
|
25
29
|
"types": "./dist/i18n.d.ts",
|
|
26
30
|
"default": "./dist/i18n.js"
|