@glissade/core 0.18.0-pre.2 → 0.18.0-pre.4

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/clips.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as ValueTypeId, Y as EaseSpec, r as Track, t as Key } from "./track.js";
1
+ import { D as Vec2, T as ValueTypeId, Y as EaseSpec, r as Track, t as Key } from "./track.js";
2
2
  import { r as TweenTarget } from "./targetRef.js";
3
3
 
4
4
  //#region src/clip.d.ts
@@ -182,20 +182,63 @@ declare function morph(from: Box, to: Box, targets: MorphTargets, opts: MorphOpt
182
182
  declare class PresenceError extends Error {
183
183
  constructor(message: string);
184
184
  }
185
+ /**
186
+ * An inline ENTER/EXIT literal (0.18 sugar): a terse alternative to hand-building
187
+ * a `clip({channels})`. `transitionToClip` compiles it to the SAME `Clip` an author
188
+ * would write by hand, so presence() then runs UNCHANGED on the result — the inline
189
+ * spelling is byte-INDISTINGUISHABLE from the hand-authored clip path.
190
+ *
191
+ * Conventions mirror clipStdlib (`slideIn`/`popIn`): a scalar `offset` slides that
192
+ * magnitude in along `edge` (default 'bottom' = slide up from below); a scalar
193
+ * `scale` broadcasts to a Vec2; OMITTING `opacity` emits NO opacity channel so
194
+ * presence()'s synthesized rise/fall takes over (matching the Clip path exactly).
195
+ */
196
+ interface PresenceTransition {
197
+ /** Opacity endpoints [from,to]. OMIT to rely on presence()'s synthesized rise/fall. */
198
+ opacity?: [number, number];
199
+ /**
200
+ * Slide displacement. A scalar slides that magnitude along `edge`; a single Vec2
201
+ * is the displaced point (animates to/from [0,0]); a [Vec2,Vec2] is explicit
202
+ * endpoints. Omit for no position channel.
203
+ */
204
+ offset?: number | Vec2 | [Vec2, Vec2];
205
+ /** Slide edge for a scalar `offset` (clipStdlib convention). Default 'bottom'. */
206
+ edge?: SlideEdge;
207
+ /** Scale endpoints. A scalar pair broadcasts each value to a Vec2 (popIn convention). */
208
+ scale?: [number, number] | [Vec2, Vec2];
209
+ /** Transition length in seconds. Default = the presence DEFAULT_FADE (0.3). */
210
+ dur?: number;
211
+ /** Arriving ease of every channel's last segment. */
212
+ ease?: EaseSpec;
213
+ }
185
214
  interface PresenceOpts {
186
215
  /** Wall-clock second the node ENTERS (becomes visible). */
187
- show: number;
216
+ show?: number;
188
217
  /** Wall-clock second the node has fully EXITED (the exit LANDS here). */
189
- hide: number;
190
- /** Entrance clip (default = a plain opacity fade-in 0→1, ~0.3s). */
191
- enter?: Clip;
192
- /** Exit clip (default = a plain opacity fade-out 1→0, ~0.3s). Back-timed to land on `hide`. */
193
- exit?: Clip;
218
+ hide?: number;
219
+ /** Alias for `{ show: window[0], hide: window[1] }`. */
220
+ window?: [number, number];
221
+ /** Entrance clip, or an inline transition literal (default = a plain opacity fade-in 0→1, ~0.3s). */
222
+ enter?: Clip | PresenceTransition;
223
+ /** Exit clip, or an inline transition literal (default = a plain opacity fade-out 1→0, ~0.3s). Back-timed to land on `hide`. */
224
+ exit?: Clip | PresenceTransition;
194
225
  /** Forwarded to `enter.apply` (speed / per-channel overrides). */
195
226
  enterOpts?: ApplyOpts;
196
227
  /** Forwarded to `exit.apply` (speed / per-channel overrides). */
197
228
  exitOpts?: ApplyOpts;
198
229
  }
230
+ /**
231
+ * Compile an inline `PresenceTransition` literal into the SAME `Clip` an author
232
+ * would hand-write — an opacity channel (only when `opacity` is given), a position
233
+ * channel from `offset`+`edge` (slideIn convention), and a scale channel (scalar
234
+ * broadcast to Vec2, popIn convention). presence() then runs UNCHANGED on it.
235
+ *
236
+ * `dir` selects the slide direction: an `enter` slides FROM the edge-displaced
237
+ * point TO [0,0]; an `exit` slides FROM [0,0] TO the edge-displaced point (so a
238
+ * back-timed exit reads as the inverse of the entrance). For an explicit
239
+ * `[Vec2,Vec2]` offset the endpoints are used verbatim (no direction flip).
240
+ */
241
+ declare function transitionToClip(t: PresenceTransition, dir: 'enter' | 'exit'): Clip;
199
242
  interface PresenceResult {
200
243
  /** Reconciled tracks: the opacity guard (fused with enter/exit opacity keys) + any non-opacity channels. */
201
244
  tracks: Track[];
@@ -213,4 +256,4 @@ interface PresenceResult {
213
256
  */
214
257
  declare function presence(nodeId: TweenTarget, opts: PresenceOpts): PresenceResult;
215
258
  //#endregion
216
- export { type ApplyOpts, type ApplyOpts as ClipApplyOpts, type Box, type ChannelOverride, type Clip, type ClipChannel, ClipError, type ClipListDelay, type ClipListOpts, type ClipResult, type ClipSpec, type ClipTarget, type DurationOpts, MorphError, type MorphOpts, type MorphResult, type MorphTargets, PresenceError, type PresenceOpts, type PresenceResult, type SlideEdge, clip, clipList, driftLoop, morph, popIn, presence, pulse, slideIn };
259
+ export { type ApplyOpts, type ApplyOpts as ClipApplyOpts, type Box, type ChannelOverride, type Clip, type ClipChannel, ClipError, type ClipListDelay, type ClipListOpts, type ClipResult, type ClipSpec, type ClipTarget, type DurationOpts, MorphError, type MorphOpts, type MorphResult, type MorphTargets, PresenceError, type PresenceOpts, type PresenceResult, type PresenceTransition, type SlideEdge, clip, clipList, driftLoop, morph, popIn, presence, pulse, slideIn, transitionToClip };
package/dist/clips.js CHANGED
@@ -398,6 +398,82 @@ function defaultExit() {
398
398
  keys: [key(0, 1), key(DEFAULT_FADE, 0)]
399
399
  } } });
400
400
  }
401
+ /** A `PresenceTransition` is a plain bag; a `Clip` carries `apply`. Discriminate on that. */
402
+ function isClip(t) {
403
+ return typeof t.apply === "function";
404
+ }
405
+ /** Is a value a Vec2 ([number, number])? */
406
+ function isVec2(v) {
407
+ return Array.isArray(v);
408
+ }
409
+ /**
410
+ * Compile an inline `PresenceTransition` literal into the SAME `Clip` an author
411
+ * would hand-write — an opacity channel (only when `opacity` is given), a position
412
+ * channel from `offset`+`edge` (slideIn convention), and a scale channel (scalar
413
+ * broadcast to Vec2, popIn convention). presence() then runs UNCHANGED on it.
414
+ *
415
+ * `dir` selects the slide direction: an `enter` slides FROM the edge-displaced
416
+ * point TO [0,0]; an `exit` slides FROM [0,0] TO the edge-displaced point (so a
417
+ * back-timed exit reads as the inverse of the entrance). For an explicit
418
+ * `[Vec2,Vec2]` offset the endpoints are used verbatim (no direction flip).
419
+ */
420
+ function transitionToClip(t, dir) {
421
+ const d = t.dur ?? DEFAULT_FADE;
422
+ const ease = t.ease;
423
+ const channels = {};
424
+ if (t.opacity !== void 0) {
425
+ const [from, to] = t.opacity;
426
+ channels.opacity = {
427
+ path: "opacity",
428
+ keys: [key(0, from), key(d, to, ...easeArg(ease))]
429
+ };
430
+ }
431
+ if (t.offset !== void 0) {
432
+ const [from, to] = offsetEndpoints(t.offset, t.edge ?? "bottom", dir);
433
+ channels.offset = {
434
+ path: "position",
435
+ keys: [key(0, from), key(d, to, ...easeArg(ease))]
436
+ };
437
+ }
438
+ if (t.scale !== void 0) {
439
+ const [from, to] = scaleEndpoints(t.scale);
440
+ channels.scale = {
441
+ path: "scale",
442
+ keys: [key(0, from), key(d, to, ...easeArg(ease))]
443
+ };
444
+ }
445
+ return clip({ channels });
446
+ }
447
+ /** Spread-arg helper: pass `ease` to `key()` only when defined (no `undefined` arg). */
448
+ function easeArg(ease) {
449
+ return ease !== void 0 ? [ease] : [];
450
+ }
451
+ /** Edge → unit displacement direction (matches clipStdlib's slideIn `from` vectors). */
452
+ function edgeVec(edge, dist) {
453
+ return edge === "left" ? [-dist, 0] : edge === "right" ? [dist, 0] : edge === "top" ? [0, -dist] : [0, dist];
454
+ }
455
+ /**
456
+ * Resolve the `offset` field to position [from, to] endpoints. A scalar slides that
457
+ * magnitude along `edge`; a single Vec2 is the displaced point; a [Vec2,Vec2] is
458
+ * verbatim endpoints. enter goes displaced→[0,0]; exit goes [0,0]→displaced.
459
+ */
460
+ function offsetEndpoints(offset, edge, dir) {
461
+ if (Array.isArray(offset) && isVec2(offset[0])) return offset;
462
+ const displaced = isVec2(offset) ? offset : edgeVec(edge, offset);
463
+ const origin = [0, 0];
464
+ return dir === "enter" ? [displaced, origin] : [origin, displaced];
465
+ }
466
+ /** Resolve the `scale` field to [from, to] Vec2 endpoints (scalar pair → broadcast). */
467
+ function scaleEndpoints(scale) {
468
+ const [a, b] = scale;
469
+ const broadcast = (v) => isVec2(v) ? v : [v, v];
470
+ return [broadcast(a), broadcast(b)];
471
+ }
472
+ /** Normalize an enter/exit option (Clip or inline literal) to a Clip. */
473
+ function resolveTransition(t, dir, fallback) {
474
+ if (t === void 0) return fallback();
475
+ return isClip(t) ? t : transitionToClip(t, dir);
476
+ }
401
477
  /** Effective duration of a clip after a speed multiplier (`apply` divides every t by speed). */
402
478
  function effectiveDuration(c, opts) {
403
479
  return c.duration / (opts?.speed ?? 1);
@@ -425,17 +501,63 @@ function partitionOpacity(tracks, opacityTarget) {
425
501
  };
426
502
  }
427
503
  /**
504
+ * The same stable-sort + coincident-`t` later-wins dedup the opacity guard uses,
505
+ * factored out so the NON-opacity channels reconcile identically. Given keys
506
+ * already in merge order (earlier-emitted first), sort by `t` keeping that order
507
+ * at ties, then collapse a coincident-`t` run to its LAST key. Pure.
508
+ */
509
+ function reconcileKeys(keys) {
510
+ const sorted = stableSortByT(keys);
511
+ const deduped = [];
512
+ for (const k of sorted) {
513
+ const last = deduped[deduped.length - 1];
514
+ if (last && last.t === k.t) deduped[deduped.length - 1] = k;
515
+ else deduped.push(k);
516
+ }
517
+ return deduped;
518
+ }
519
+ /**
520
+ * Reconcile the enter's then the exit's NON-opacity tracks per target. When enter
521
+ * AND exit animate the SAME non-opacity channel (e.g. both slide `position` — a
522
+ * slide-in-hold-slide-out), emit ONE track per target whose keys are the enter's
523
+ * then the exit's, fused with the SAME stable-sort + later-wins dedup as opacity:
524
+ * at a coincident `t` (the enter's settle vs the exit's start) the exit wins, so
525
+ * the merged ramp is continuous and no key is silently dropped by
526
+ * `compileTimeline`'s `coalesce()`. Disjoint targets pass straight through.
527
+ * Output order is stable: first appearance across [enterRest, exitRest].
528
+ */
529
+ function reconcileNonOpacity(enterRest, exitRest) {
530
+ const order = [];
531
+ const byTarget = /* @__PURE__ */ new Map();
532
+ for (const tr of [...enterRest, ...exitRest]) {
533
+ const bucket = byTarget.get(tr.target);
534
+ if (bucket) bucket.push(tr);
535
+ else {
536
+ byTarget.set(tr.target, [tr]);
537
+ order.push(tr.target);
538
+ }
539
+ }
540
+ return order.map((target) => {
541
+ const group = byTarget.get(target);
542
+ if (group.length === 1) return group[0];
543
+ const merged = group.flatMap((tr) => tr.keys);
544
+ return track(target, group[0].type, reconcileKeys(merged));
545
+ });
546
+ }
547
+ /**
428
548
  * Schedule a node's enter/exit presence. Emits keyed `Track[]` only.
429
549
  *
430
550
  * presence('card', { show: 1, hide: 5 }) // fade in at 1, fade out to land on 5
431
551
  */
432
552
  function presence(nodeId, opts) {
433
- const { show, hide } = opts;
553
+ const show = opts.show ?? opts.window?.[0];
554
+ const hide = opts.hide ?? opts.window?.[1];
555
+ if (show === void 0 || hide === void 0) throw new PresenceError(`presence() needs a window: pass { show, hide } or { window: [show, hide] }`);
434
556
  const opacityTarget = resolveTweenTarget(typeof nodeId === "string" ? `${nodeId}/opacity` : nodeId);
435
557
  const lastSlash = opacityTarget.lastIndexOf("/");
436
558
  const nodeIdStr = lastSlash < 0 ? opacityTarget : opacityTarget.slice(0, lastSlash);
437
- const enter = opts.enter ?? defaultEnter();
438
- const exit = opts.exit ?? defaultExit();
559
+ const enter = resolveTransition(opts.enter, "enter", defaultEnter);
560
+ const exit = resolveTransition(opts.exit, "exit", defaultExit);
439
561
  const enterRes = enter.apply(nodeIdStr, show, opts.enterOpts);
440
562
  const enterEnd = show + effectiveDuration(enter, opts.enterOpts);
441
563
  const exitDur = effectiveDuration(exit, opts.exitOpts);
@@ -460,23 +582,12 @@ function presence(nodeId, opts) {
460
582
  else overlay.push(key(show, 0), key(enterEnd, 1));
461
583
  if (exitOpacity.length > 0) for (const k of exitOpacity) overlay.push(k);
462
584
  else overlay.push(key(exitStart, 1), key(hide, 0));
463
- const sorted = stableSortByT([...bareGuard, ...overlay]);
464
- const deduped = [];
465
- for (const k of sorted) {
466
- const last = deduped[deduped.length - 1];
467
- if (last && last.t === k.t) deduped[deduped.length - 1] = k;
468
- else deduped.push(k);
469
- }
470
585
  return {
471
- tracks: [
472
- track(opacityTarget, "number", deduped),
473
- ...enterRest,
474
- ...exitRest
475
- ],
586
+ tracks: [track(opacityTarget, "number", reconcileKeys([...bareGuard, ...overlay])), ...reconcileNonOpacity(enterRest, exitRest)],
476
587
  end: hide,
477
588
  shownAt: show,
478
589
  hiddenAt: hide
479
590
  };
480
591
  }
481
592
  //#endregion
482
- export { ClipError, MorphError, PresenceError, clip, clipList, driftLoop, morph, popIn, presence, pulse, slideIn };
593
+ export { ClipError, MorphError, PresenceError, clip, clipList, driftLoop, morph, popIn, presence, pulse, slideIn, transitionToClip };
package/dist/index.d.ts CHANGED
@@ -249,13 +249,22 @@ interface StaggerSpec<T = unknown> {
249
249
  ease?: EaseSpec;
250
250
  }
251
251
  /**
252
- * Stagger placement. `each` is the per-rank delay (seconds, number-only in v1).
253
- * `from` picks the anchor the cascade ranks outward from (GSAP parity); `at`
254
- * places the whole group's base position (defaults to the chain end).
252
+ * Stagger placement.
253
+ *
254
+ * `each` is the per-target delay. A number gives the uniform cascade
255
+ * `d_i = rank_i * each` (the common case). A function `(rank, count) => seconds`
256
+ * maps each target's rank (and the group size) to its own delay, so accelerating
257
+ * / decelerating / eased cascades are author-controlled (GSAP parity). The
258
+ * function must return a finite number for every target.
259
+ *
260
+ * `anchor` picks the placement the cascade ranks outward from — note this is the
261
+ * placement axis, distinct from `StaggerSpec.from` (the start VALUE that routes a
262
+ * target through `fromTo`). `at` places the whole group's base position
263
+ * (defaults to the chain end).
255
264
  */
256
265
  interface StaggerOpts {
257
- each: number;
258
- from?: 'start' | 'end' | 'center' | 'edges' | number;
266
+ each: number | ((rank: number, count: number) => number);
267
+ anchor?: 'start' | 'end' | 'center' | 'edges' | number;
259
268
  at?: Position;
260
269
  }
261
270
  interface TimelineBuilder {
@@ -264,12 +273,15 @@ interface TimelineBuilder {
264
273
  /**
265
274
  * Build-time sugar: loop the shipped `to`/`fromTo` emission over `targets`,
266
275
  * cascading each by a per-rank delay. Emits keys byte-identical to N
267
- * hand-authored offset tweens. The `from` anchor ranks targets over their
268
- * array index i (n = targets.length, c = (n-1)/2): `'start'` → i; `'end'` →
276
+ * hand-authored offset tweens. The `anchor` ranks targets over their array
277
+ * index i (n = targets.length, c = (n-1)/2): `'start'` → i; `'end'` →
269
278
  * (n-1)-i; `'center'` → round(|i-c|); `'edges'` → round(c-|i-c|); numeric k →
270
- * round(|i-k|). Delay d_i = rank_i * each, inserted at `base + d_i` where
271
- * `base = resolvePosition(opts.at)`. The group reads as one block to a
272
- * following `'<'`/`'>'`/`'+='` step.
279
+ * round(|i-k|). The delay is `d_i = rank_i * each` for a numeric `each`, or
280
+ * `d_i = each(rank_i, n)` for a function `each` (accel/decel/eased cascades).
281
+ * Each target is inserted at `base + d_i` where `base = resolvePosition(opts.at)`.
282
+ * The group reads as one block to a following `'<'`/`'>'`/`'+='` step (its
283
+ * bounds are the true min/max delay, so a backward/non-uniform spread reports
284
+ * honestly to the cursor).
273
285
  */
274
286
  stagger<T>(targets: TweenTarget[], spec: StaggerSpec<T>, opts: StaggerOpts): TimelineBuilder;
275
287
  /** Hold key: the value snaps at the resolved position (§2.6). */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { A as vec2Type, B as springEasing, C as paintType, D as stringType, E as reprOf, F as parseColor, G as cubicBezier, H as springPresets, I as rgbaToOklab, J as easings, K as cubicBezierDerivative, L as emitDevWarning, M as formatColor, N as lerpColor, O as vec2ArcType, P as oklabToRgba, R as setDevWarning, S as numberType, T as registerValueType, U as DEFAULT_EASE, V as springEasingDerivative, W as UnknownEasingError, Y as namedEasing, _ 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 ColorParseError, k as vec2Equals, l as resolveEaseDerivative, m as validateTrack, n as UnresolvableTargetError, o as TrackValidationError, p as track, q as easingDerivatives, r as isEditableNodeId, s as key, t as TARGET_PATH, u as sampleTrack, v as booleanType, w as pathType, x as inferValueType, y as colorType, z as spring } from "./targetRef.js";
2
2
  import { t as parseCmap } from "./cmap.js";
3
- import { a as hashKeys, c as migrateSidecar, d as TimelineValidationError, f as audioOffsetSamples, h as timeline$1, i as emptySidecar, l as normalizeEditedKeys, m as isDurationEditable, n as assignKeyIds, o as mergeSidecar, p as compileTimeline, r as deleteSidecarTrack, s as mergeSidecarDetailed, t as SidecarVersionError, u as setSidecarTrack } from "./sidecar.js";
3
+ import { _ as timeline$1, a as hashKeys, c as migrateSidecar, d as TimelineValidationError, f as audioOffsetSamples, g as namespaceCallName, h as isDurationEditable, i as emptySidecar, l as normalizeEditedKeys, m as compileTimeline, n as assignKeyIds, o as mergeSidecar, p as callMarkerPrefix, r as deleteSidecarTrack, s as mergeSidecarDetailed, t as SidecarVersionError, u as setSidecarTrack } from "./sidecar.js";
4
4
  //#region src/ticker.ts
5
5
  /** The default scheduler: flush synchronously, preserving pre-ticker timing. */
6
6
  const synchronousScheduler = (flush) => flush();
@@ -647,30 +647,44 @@ function buildTimeline(build, init = {}) {
647
647
  const n = targets.length;
648
648
  const base = resolvePosition(opts.at);
649
649
  const c = (n - 1) / 2;
650
+ const { anchor = "start", each } = opts;
651
+ const finite = (v) => {
652
+ if (!Number.isFinite(v)) throw new TimelineValidationError(`stagger: non-finite each/anchor (${String(v)})`);
653
+ return v;
654
+ };
655
+ if (typeof anchor === "number") finite(anchor);
656
+ if (typeof each === "number") finite(each);
650
657
  const rankOf = (i) => {
651
- const f = opts.from ?? "start";
652
- if (f === "start") return i;
653
- if (f === "end") return n - 1 - i;
654
- if (f === "center") return Math.round(Math.abs(i - c));
655
- if (f === "edges") return Math.round(c - Math.abs(i - c));
656
- return Math.round(Math.abs(i - f));
658
+ if (anchor === "start") return i;
659
+ if (anchor === "end") return n - 1 - i;
660
+ if (anchor === "center") return Math.round(Math.abs(i - c));
661
+ if (anchor === "edges") return Math.round(c - Math.abs(i - c));
662
+ return Math.round(Math.abs(i - anchor));
657
663
  };
658
- const duration = spec.duration ?? 1;
659
- let maxDelay = 0;
664
+ const delayOf = (rank) => finite(typeof each === "function" ? each(rank, n) : rank * each);
665
+ const ease = spec.ease;
666
+ const effDur = typeof ease === "object" && ease !== null && ease.kind === "spring" ? spring.duration(ease) : spec.duration ?? 1;
660
667
  const tweenOpts = (d) => ({
661
668
  at: base + d,
662
669
  ...spec.duration !== void 0 ? { duration: spec.duration } : {},
663
670
  ...spec.ease !== void 0 ? { ease: spec.ease } : {}
664
671
  });
672
+ let minDelay = 0;
673
+ let maxDelay = 0;
665
674
  for (let i = 0; i < n; i++) {
666
- const d = rankOf(i) * opts.each;
667
- if (d > maxDelay) maxDelay = d;
675
+ const d = delayOf(rankOf(i));
676
+ if (i === 0 || d < minDelay) minDelay = d;
677
+ if (i === 0 || d > maxDelay) maxDelay = d;
678
+ const start = base + d;
679
+ if (start < 0) throw new TimelineValidationError(`stagger: target would land at t=${start} (< 0); shift opts.at`);
668
680
  const t = targets[i];
669
681
  if (spec.from !== void 0) builder.fromTo(t, spec.from, spec.to, tweenOpts(d));
670
682
  else builder.to(t, spec.to, tweenOpts(d));
671
683
  }
672
- prevStart = base;
673
- prevEnd = base + maxDelay + duration;
684
+ if (n > 0) {
685
+ prevStart = base + minDelay;
686
+ prevEnd = base + maxDelay + effDur;
687
+ }
674
688
  return builder;
675
689
  },
676
690
  set(target, value, opts = {}) {
@@ -704,8 +718,10 @@ function buildTimeline(build, init = {}) {
704
718
  _pos: at
705
719
  };
706
720
  if (opts.timeScale !== void 0) entry.timeScale = opts.timeScale;
721
+ const childIndex = children.length;
707
722
  children.push(entry);
708
- for (const [name, fn] of getTimelineCallbacks(child)) if (!callbacks.has(name)) callbacks.set(name, fn);
723
+ const prefix = callMarkerPrefix(childIndex);
724
+ for (const [name, fn] of getTimelineCallbacks(child)) callbacks.set(namespaceCallName(name, prefix), fn);
709
725
  const scale = mode === "sync" ? opts.timeScale ?? 1 : 1;
710
726
  prevStart = start;
711
727
  prevEnd = start + compileTimeline(child).duration / scale;
package/dist/sidecar.js CHANGED
@@ -53,6 +53,24 @@ function rebaseKeys(keys, at, timeScale) {
53
53
  t: at + k.t / timeScale
54
54
  }));
55
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
+ }
56
74
  function flatten(doc, at, timeScale, unit, out, counter) {
57
75
  for (const tr of doc.tracks) {
58
76
  validateTrack(tr);
@@ -136,13 +154,15 @@ function compileTimeline(doc) {
136
154
  const labels = { ...doc.labels };
137
155
  const markers = [...doc.markers ?? []];
138
156
  const audio = [...doc.audio ?? []];
139
- const visitChildren = (children, at, scale) => {
140
- for (const child of children ?? []) {
157
+ const visitChildren = (children, at, scale, prefix) => {
158
+ (children ?? []).forEach((child, index) => {
141
159
  const base = at + child.at / scale;
142
160
  const childScale = scale * (child.mode === "sync" ? child.timeScale ?? 1 : 1);
161
+ const childPrefix = prefix + callMarkerPrefix(index);
143
162
  for (const [name, t] of Object.entries(child.timeline.labels ?? {})) if (!(name in labels)) labels[name] = base + t / childScale;
144
163
  for (const m of child.timeline.markers ?? []) markers.push({
145
164
  ...m,
165
+ name: namespaceCallName(m.name, childPrefix),
146
166
  t: base + m.t / childScale
147
167
  });
148
168
  for (const clip of child.timeline.audio ?? []) audio.push({
@@ -150,10 +170,10 @@ function compileTimeline(doc) {
150
170
  at: base + clip.at / childScale,
151
171
  ...childScale !== 1 ? { playbackRate: (clip.playbackRate ?? 1) * childScale } : {}
152
172
  });
153
- visitChildren(child.timeline.children, base, childScale);
154
- }
173
+ visitChildren(child.timeline.children, base, childScale, childPrefix);
174
+ });
155
175
  };
156
- visitChildren(doc.children, 0, 1);
176
+ visitChildren(doc.children, 0, 1, "");
157
177
  markers.sort((a, b) => a.t - b.t);
158
178
  audio.sort((a, b) => a.at - b.at);
159
179
  return {
@@ -391,4 +411,4 @@ function normalizeEditedKeys(keys) {
391
411
  return out;
392
412
  }
393
413
  //#endregion
394
- export { hashKeys as a, migrateSidecar as c, TimelineValidationError as d, audioOffsetSamples as f, timeline as h, emptySidecar as i, normalizeEditedKeys as l, isDurationEditable as m, assignKeyIds as n, mergeSidecar as o, compileTimeline as p, deleteSidecarTrack as r, mergeSidecarDetailed as s, SidecarVersionError as t, setSidecarTrack as u };
414
+ export { timeline as _, hashKeys as a, migrateSidecar as c, TimelineValidationError as d, audioOffsetSamples as f, namespaceCallName as g, isDurationEditable as h, emptySidecar as i, normalizeEditedKeys as l, compileTimeline as m, assignKeyIds as n, mergeSidecar as o, callMarkerPrefix as p, deleteSidecarTrack as r, mergeSidecarDetailed as s, SidecarVersionError as t, setSidecarTrack as u };
@@ -128,6 +128,18 @@ interface CompiledTimeline {
128
128
  * and the other to raw float seconds. Default rate is the canonical mix grid.
129
129
  */
130
130
  declare function audioOffsetSamples(at: number, sampleRate?: number): number;
131
+ /**
132
+ * `.call()` markers carry an auto-assigned `call:N` name whose counter resets
133
+ * per document, so two sibling sub-timelines that each define a `.call()` would
134
+ * collide on `call:0` when rebased into one parent (one callback dropped, the
135
+ * other double-firing). We namespace a child's `call:*` markers by the child's
136
+ * position PATH in the parent (`c<index>/…`), and the builder's `add()` applies
137
+ * the EXACT same prefix when forwarding the child's name→fn map — so the rebased
138
+ * marker name and the registered callback key agree by construction. Only
139
+ * `call:*` names are rewritten; author-named cues keep their names. Both surfaces
140
+ * call these so the convention lives in one place.
141
+ */
142
+
131
143
  /**
132
144
  * Studio reader (§6.2 rule 4): is this timeline's duration opted into editor
133
145
  * editing? Code-owned and read-only by default; `editableDuration()` flips it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.18.0-pre.2",
3
+ "version": "0.18.0-pre.4",
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": {