@glissade/core 0.6.0 → 0.7.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -423,6 +423,14 @@ interface CompiledTimeline {
423
423
  /** Audio clips rebased to the root time axis (§5.3); sync timeScale scales playbackRate. */
424
424
  audio: AudioClip[];
425
425
  }
426
+ /**
427
+ * Sample-indexed clip offset — the single A/V-sync source of truth (§5.3),
428
+ * `round(at * sampleRate)`. Both the CLI (FFmpeg) and the browser
429
+ * (OfflineAudioContext) mixers derive their delay from this, so A/V offsets
430
+ * agree across paths by construction rather than one rounding to milliseconds
431
+ * and the other to raw float seconds. Default rate is the canonical mix grid.
432
+ */
433
+ declare function audioOffsetSamples(at: number, sampleRate?: number): number;
426
434
  declare function compileTimeline(doc: Timeline): CompiledTimeline;
427
435
  //#endregion
428
436
  //#region src/targetRef.d.ts
@@ -567,7 +575,7 @@ interface SidecarDoc {
567
575
  sidecarVersion: 1;
568
576
  /** Editor-owned tracks, replacing same-target code tracks wholesale. */
569
577
  tracks: Track[];
570
- /** Editor-owned labels; merged over code labels by name. */
578
+ /** Editor-created labels; code labels are authoritative and win on a name collision (§6.2). */
571
579
  labels?: Record<string, number>;
572
580
  }
573
581
  declare class SidecarVersionError extends Error {
@@ -595,4 +603,4 @@ declare function normalizeEditedKeys(keys: Key[]): Key[];
595
603
  */
596
604
  declare function mergeSidecar(code: Timeline, sidecar: SidecarDoc | null | undefined): Timeline;
597
605
  //#endregion
598
- export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type GainEnvelope, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type OkLab, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type RetargetSpring, type Rgba, type Rng, type SidecarDoc, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
606
+ export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type GainEnvelope, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type OkLab, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type RetargetSpring, type Rgba, type Rng, type SidecarDoc, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/dist/index.js CHANGED
@@ -865,8 +865,11 @@ var TrackValidationError = class extends Error {
865
865
  }
866
866
  };
867
867
  function validateTrack(track) {
868
- getValueType(track.type);
868
+ const vt = getValueType(track.type);
869
869
  if (track.keys.length === 0) throw new TrackValidationError(track.target, "must have at least one key");
870
+ if (vt.defaultHandoff === "cut") {
871
+ for (const k of track.keys) if (k.interp !== "hold") k.interp = "hold";
872
+ }
870
873
  for (let i = 1; i < track.keys.length; i++) {
871
874
  const prev = track.keys[i - 1];
872
875
  const cur = track.keys[i];
@@ -960,7 +963,7 @@ function resolveEaseDerivative(spec) {
960
963
  warnedNumericDerivative.add(spec);
961
964
  emitDevWarning(`easing '${spec}' has no registered derivative; velocity uses a numeric fallback — register one in easingDerivatives for exact interruption handoff`);
962
965
  }
963
- const h = 1e-5;
966
+ const h = 1 / 1024;
964
967
  return (u) => (fn(Math.min(1, u + h)) - fn(Math.max(0, u - h))) / (Math.min(1, u + h) - Math.max(0, u - h));
965
968
  }
966
969
  if (spec.kind === "cubicBezier") return cubicBezierDerivative(...spec.pts);
@@ -1048,7 +1051,13 @@ function sampleTrack(tr, t) {
1048
1051
  const vt = getValueType(tr.type);
1049
1052
  const p = (t - prev.t) / (arrival.t - prev.t);
1050
1053
  let easedT = easeFor(tr, s, i)(p);
1051
- if (!vt.extrapolates) easedT = Math.min(1, Math.max(0, easedT));
1054
+ if (!vt.extrapolates && (easedT < 0 || easedT > 1)) {
1055
+ if (!s.warnedClamp) {
1056
+ s.warnedClamp = true;
1057
+ emitDevWarning(`track '${tr.target}' (type '${tr.type}') clamped an out-of-range eased value — non-extrapolating types can't overshoot, so a spring/overshooting ease on this track is flattened`);
1058
+ }
1059
+ easedT = Math.min(1, Math.max(0, easedT));
1060
+ }
1052
1061
  return vt.lerp(prev.value, arrival.value, easedT);
1053
1062
  }
1054
1063
  //#endregion
@@ -1089,13 +1098,23 @@ function validateSpringKeys(tr) {
1089
1098
  }
1090
1099
  }
1091
1100
  }
1101
+ /**
1102
+ * Sample-indexed clip offset — the single A/V-sync source of truth (§5.3),
1103
+ * `round(at * sampleRate)`. Both the CLI (FFmpeg) and the browser
1104
+ * (OfflineAudioContext) mixers derive their delay from this, so A/V offsets
1105
+ * agree across paths by construction rather than one rounding to milliseconds
1106
+ * and the other to raw float seconds. Default rate is the canonical mix grid.
1107
+ */
1108
+ function audioOffsetSamples(at, sampleRate = 48e3) {
1109
+ return Math.round(at * sampleRate);
1110
+ }
1092
1111
  function rebaseKeys(keys, at, timeScale) {
1093
1112
  return keys.map((k) => ({
1094
1113
  ...k,
1095
1114
  t: at + k.t / timeScale
1096
1115
  }));
1097
1116
  }
1098
- function flatten(doc, at, timeScale, opaque, out) {
1117
+ function flatten(doc, at, timeScale, unit, out, counter) {
1099
1118
  for (const tr of doc.tracks) {
1100
1119
  validateTrack(tr);
1101
1120
  validateSpringKeys(tr);
@@ -1104,14 +1123,15 @@ function flatten(doc, at, timeScale, opaque, out) {
1104
1123
  ...tr,
1105
1124
  keys: rebaseKeys(tr.keys, at, timeScale)
1106
1125
  },
1107
- opaque
1126
+ unit
1108
1127
  });
1109
1128
  }
1110
1129
  for (const child of doc.children ?? []) {
1111
1130
  const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
1112
1131
  if (child.mode === "add" && child.timeScale !== void 0) throw new TimelineValidationError("timeScale is only valid on mode:'sync' children (§2.3)");
1113
1132
  if (scale <= 0) throw new TimelineValidationError("sync timeScale must be > 0");
1114
- flatten(child.timeline, at + child.at / timeScale, timeScale * scale, opaque || child.mode === "sync", out);
1133
+ const childUnit = child.mode === "sync" ? ++counter.n : unit;
1134
+ flatten(child.timeline, at + child.at / timeScale, timeScale * scale, childUnit, out, counter);
1115
1135
  }
1116
1136
  }
1117
1137
  /**
@@ -1121,25 +1141,31 @@ function flatten(doc, at, timeScale, opaque, out) {
1121
1141
  */
1122
1142
  function coalesce(entries) {
1123
1143
  const byTarget = /* @__PURE__ */ new Map();
1124
- for (const { track: tr } of entries) {
1144
+ for (const { track: tr, unit } of entries) {
1125
1145
  const existing = byTarget.get(tr.target);
1126
1146
  if (!existing) {
1127
1147
  byTarget.set(tr.target, {
1128
- ...tr,
1129
- keys: [...tr.keys]
1148
+ track: {
1149
+ ...tr,
1150
+ keys: [...tr.keys]
1151
+ },
1152
+ unit
1130
1153
  });
1131
1154
  continue;
1132
1155
  }
1133
- if (existing.type !== tr.type) throw new TimelineValidationError(`target '${tr.target}' has conflicting value types '${existing.type}' and '${tr.type}'`);
1156
+ 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)`);
1157
+ if (existing.track.type !== tr.type) throw new TimelineValidationError(`target '${tr.target}' has conflicting value types '${existing.track.type}' and '${tr.type}'`);
1134
1158
  const start = tr.keys[0].t;
1135
1159
  const end = tr.keys[tr.keys.length - 1].t;
1136
- const existingStart = existing.keys[0].t;
1137
- const existingEnd = existing.keys[existing.keys.length - 1].t;
1138
- const kept = existing.keys.filter((k) => k.t < start || k.t > end);
1139
- if (existingStart <= end && start <= existingEnd) emitDevWarning(`overlapping tracks for '${tr.target}' in [${start}, ${end}]: later insertion wins (${existing.keys.length - kept.length} earlier key(s) dropped)`);
1140
- existing.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
1160
+ const existingStart = existing.track.keys[0].t;
1161
+ const existingEnd = existing.track.keys[existing.track.keys.length - 1].t;
1162
+ const kept = existing.track.keys.filter((k) => k.t < start || k.t > end);
1163
+ 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)`);
1164
+ existing.track.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
1141
1165
  }
1142
- return byTarget;
1166
+ const result = /* @__PURE__ */ new Map();
1167
+ for (const [target, { track }] of byTarget) result.set(target, track);
1168
+ return result;
1143
1169
  }
1144
1170
  function childExtent(child) {
1145
1171
  const scale = child.mode === "sync" ? child.timeScale ?? 1 : 1;
@@ -1159,7 +1185,7 @@ function computeDuration(doc) {
1159
1185
  function compileTimeline(doc) {
1160
1186
  if (doc.version !== 1) throw new TimelineValidationError(`unsupported timeline document version ${String(doc.version)}`);
1161
1187
  const flat = [];
1162
- flatten(doc, 0, 1, false, flat);
1188
+ flatten(doc, 0, 1, 0, flat, { n: 0 });
1163
1189
  const tracks = coalesce(flat);
1164
1190
  const labels = { ...doc.labels };
1165
1191
  const markers = [...doc.markers ?? []];
@@ -1667,11 +1693,16 @@ function mergeSidecar(code, sidecar) {
1667
1693
  ...code,
1668
1694
  tracks
1669
1695
  };
1670
- if (sidecar.labels && Object.keys(sidecar.labels).length > 0) merged.labels = {
1671
- ...code.labels,
1672
- ...sidecar.labels
1673
- };
1696
+ if (sidecar.labels && Object.keys(sidecar.labels).length > 0) {
1697
+ const codeLabels = code.labels ?? {};
1698
+ const shadowed = Object.keys(sidecar.labels).filter((n) => n in codeLabels);
1699
+ if (shadowed.length) emitDevWarning(`sidecar label(s) ${shadowed.join(", ")} collide with code labels; code wins (§6.2)`);
1700
+ merged.labels = {
1701
+ ...sidecar.labels,
1702
+ ...codeLabels
1703
+ };
1704
+ }
1674
1705
  return merged;
1675
1706
  }
1676
1707
  //#endregion
1677
- 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, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
1708
+ export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0-pre.0",
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
  "type": "module",