reframe-video 0.6.34 → 0.6.39

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/cli.js CHANGED
@@ -245,11 +245,68 @@ function compileScene(ir) {
245
245
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
246
246
  const natural = durationOf(grouping, 0);
247
247
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
248
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
248
+ const at = typeof tl.at === "number" ? tl.at : void 0;
249
+ const beatStart = at ?? start + (tl.gap ?? 0);
249
250
  return beatStart + k * natural;
250
251
  }
251
252
  }
252
253
  };
254
+ let labelClock;
255
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
256
+ if (ir.timeline && anyAnchor(ir.timeline)) {
257
+ const clock = /* @__PURE__ */ new Map();
258
+ const clockWalk = (tl, start) => {
259
+ let end = start;
260
+ switch (tl.kind) {
261
+ case "seq": {
262
+ let t = start;
263
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
264
+ end = t;
265
+ break;
266
+ }
267
+ case "par": {
268
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
269
+ break;
270
+ }
271
+ case "stagger": {
272
+ tl.children.forEach((c, i) => {
273
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
274
+ });
275
+ break;
276
+ }
277
+ case "wait":
278
+ end = start + tl.duration;
279
+ break;
280
+ case "tween":
281
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
282
+ break;
283
+ case "motionPath":
284
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
285
+ break;
286
+ case "to": {
287
+ const override = ir.states?.[tl.state] ?? {};
288
+ const si = tl.stagger ?? 0;
289
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
290
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
291
+ break;
292
+ }
293
+ case "beat": {
294
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
295
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
296
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
297
+ const at = typeof tl.at === "number" ? tl.at : void 0;
298
+ const beatStart = at ?? start + (tl.gap ?? 0);
299
+ end = clockWalk(inner, beatStart);
300
+ clock.set(tl.name, { t0: beatStart, t1: end });
301
+ break;
302
+ }
303
+ }
304
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
305
+ return end;
306
+ };
307
+ clockWalk(ir.timeline, 0);
308
+ labelClock = clock;
309
+ }
253
310
  const walk = (tl, start) => {
254
311
  const end = walkInner(tl, start);
255
312
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -266,7 +323,8 @@ function compileScene(ir) {
266
323
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
267
324
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
268
325
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
269
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
326
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
327
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
270
328
  const end = walk(inner, beatStart);
271
329
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
272
330
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -586,6 +644,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
586
644
  function validateScene(ir) {
587
645
  const problems = [];
588
646
  const nodeById = /* @__PURE__ */ new Map();
647
+ const startAnchors = [];
589
648
  const checkPaint = (where, value) => {
590
649
  if (typeof value !== "object" || value === null) return;
591
650
  const g = value;
@@ -618,6 +677,7 @@ function validateScene(ir) {
618
677
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
619
678
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
620
679
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
680
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
621
681
  if (node.type === "group") {
622
682
  const clip = node.props.clip;
623
683
  if (clip) {
@@ -678,6 +738,7 @@ function validateScene(ir) {
678
738
  );
679
739
  }
680
740
  const labels = /* @__PURE__ */ new Set();
741
+ const beatAnchors = [];
681
742
  const checkEase = (path2, ease) => {
682
743
  if (ease === void 0) return;
683
744
  if (typeof ease === "string") {
@@ -769,6 +830,7 @@ function validateScene(ir) {
769
830
  );
770
831
  }
771
832
  labels.add(tl.name);
833
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
772
834
  if (tl.duration !== void 0 && tl.duration <= 0) {
773
835
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
774
836
  }
@@ -787,6 +849,22 @@ function validateScene(ir) {
787
849
  }
788
850
  };
789
851
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
852
+ for (const a of beatAnchors) {
853
+ if (a.at === a.name) {
854
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
855
+ } else if (!labels.has(a.at)) {
856
+ problems.push(
857
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
858
+ );
859
+ }
860
+ }
861
+ for (const a of startAnchors) {
862
+ if (!labels.has(a.at)) {
863
+ problems.push(
864
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
865
+ );
866
+ }
867
+ }
790
868
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
791
869
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
792
870
  }
@@ -913,6 +991,13 @@ function compileComposition(comp) {
913
991
 
914
992
  // ../core/src/compose.ts
915
993
  var SCENE_PATCHABLE = ["background", "duration", "fps"];
994
+ var TIMELINE_PATCHABLE = {
995
+ to: ["duration", "ease", "stagger"],
996
+ tween: ["duration", "ease"],
997
+ wait: ["duration"],
998
+ motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
999
+ beat: ["at", "gap", "scale", "duration", "order"]
1000
+ };
916
1001
  function composeScene(base, ...overlays) {
917
1002
  const ir = structuredClone(base);
918
1003
  const report = { applied: [], orphans: [], warnings: [] };
@@ -1047,13 +1132,6 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1047
1132
  if ("children" in tl) tl.children.forEach(walkTimeline);
1048
1133
  };
1049
1134
  if (ir.timeline) walkTimeline(ir.timeline);
1050
- const PATCHABLE = {
1051
- to: ["duration", "ease", "stagger"],
1052
- tween: ["duration", "ease"],
1053
- wait: ["duration"],
1054
- motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
1055
- beat: ["at", "gap", "scale", "duration", "order"]
1056
- };
1057
1135
  let timingPatched = false;
1058
1136
  for (const [label, patch] of Object.entries(overlay.timeline)) {
1059
1137
  const step = byLabel.get(label);
@@ -1064,7 +1142,7 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1064
1142
  );
1065
1143
  continue;
1066
1144
  }
1067
- const allowed = PATCHABLE[step.kind] ?? [];
1145
+ const allowed = TIMELINE_PATCHABLE[step.kind] ?? [];
1068
1146
  for (const [key2, value] of Object.entries(patch)) {
1069
1147
  if (value === void 0) continue;
1070
1148
  if (!allowed.includes(key2)) {
@@ -1443,13 +1521,14 @@ var SFX_DURATION = {
1443
1521
  camera: 0.18
1444
1522
  };
1445
1523
  var FILE_CUE_DURATION = 0.4;
1446
- function collectClipAudio(ir, duration, warnings) {
1524
+ function collectClipAudio(ir, duration, labelTimes, warnings) {
1447
1525
  const out = [];
1448
1526
  const walk = (nodes) => {
1449
1527
  for (const node of nodes) {
1450
1528
  if (node.type === "video") {
1451
1529
  const gain = node.props.volume ?? 1;
1452
- const start = node.props.start ?? 0;
1530
+ const startRaw = node.props.start;
1531
+ const start = typeof startRaw === "string" ? labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
1453
1532
  if (gain <= 0) continue;
1454
1533
  if (start >= duration) {
1455
1534
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
@@ -1467,7 +1546,7 @@ function resolveAudioPlan(compiled) {
1467
1546
  const audio = compiled.ir.audio;
1468
1547
  const warnings = [];
1469
1548
  const duration = compiled.duration;
1470
- const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
1549
+ const clipAudio = collectClipAudio(compiled.ir, duration, compiled.labelTimes, warnings);
1471
1550
  const autoCues = audio?.autoFoley ? autoFoley(compiled, audio.autoFoley === true ? {} : audio.autoFoley) : [];
1472
1551
  const manualCues = [...audio?.cues ?? [], ...autoCues];
1473
1552
  if (!audio || !audio.bgm && manualCues.length === 0) {
@@ -2664,7 +2743,7 @@ ${stderr.slice(-2e3)}`))
2664
2743
  });
2665
2744
  }
2666
2745
  function neededSeconds(node, duration) {
2667
- const start = node.props.start ?? 0;
2746
+ const start = typeof node.props.start === "string" ? 0 : node.props.start ?? 0;
2668
2747
  const rate = node.props.rate ?? 1;
2669
2748
  const clipStart = node.props.clipStart ?? 0;
2670
2749
  return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;
@@ -142,6 +142,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
142
142
  function validateScene(ir) {
143
143
  const problems = [];
144
144
  const nodeById = /* @__PURE__ */ new Map();
145
+ const startAnchors = [];
145
146
  const checkPaint = (where, value) => {
146
147
  if (typeof value !== "object" || value === null) return;
147
148
  const g = value;
@@ -174,6 +175,7 @@ function validateScene(ir) {
174
175
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
175
176
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
176
177
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
178
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
177
179
  if (node.type === "group") {
178
180
  const clip = node.props.clip;
179
181
  if (clip) {
@@ -234,6 +236,7 @@ function validateScene(ir) {
234
236
  );
235
237
  }
236
238
  const labels = /* @__PURE__ */ new Set();
239
+ const beatAnchors = [];
237
240
  const checkEase = (path2, ease) => {
238
241
  if (ease === void 0) return;
239
242
  if (typeof ease === "string") {
@@ -325,6 +328,7 @@ function validateScene(ir) {
325
328
  );
326
329
  }
327
330
  labels.add(tl.name);
331
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
328
332
  if (tl.duration !== void 0 && tl.duration <= 0) {
329
333
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
330
334
  }
@@ -343,6 +347,22 @@ function validateScene(ir) {
343
347
  }
344
348
  };
345
349
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
350
+ for (const a of beatAnchors) {
351
+ if (a.at === a.name) {
352
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
353
+ } else if (!labels.has(a.at)) {
354
+ problems.push(
355
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
356
+ );
357
+ }
358
+ }
359
+ for (const a of startAnchors) {
360
+ if (!labels.has(a.at)) {
361
+ problems.push(
362
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
363
+ );
364
+ }
365
+ }
346
366
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
347
367
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
348
368
  }
package/dist/compile.js CHANGED
@@ -147,6 +147,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
147
147
  function validateScene(ir) {
148
148
  const problems = [];
149
149
  const nodeById = /* @__PURE__ */ new Map();
150
+ const startAnchors = [];
150
151
  const checkPaint = (where, value) => {
151
152
  if (typeof value !== "object" || value === null) return;
152
153
  const g = value;
@@ -179,6 +180,7 @@ function validateScene(ir) {
179
180
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
180
181
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
181
182
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
183
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
182
184
  if (node.type === "group") {
183
185
  const clip = node.props.clip;
184
186
  if (clip) {
@@ -239,6 +241,7 @@ function validateScene(ir) {
239
241
  );
240
242
  }
241
243
  const labels = /* @__PURE__ */ new Set();
244
+ const beatAnchors = [];
242
245
  const checkEase = (path2, ease) => {
243
246
  if (ease === void 0) return;
244
247
  if (typeof ease === "string") {
@@ -330,6 +333,7 @@ function validateScene(ir) {
330
333
  );
331
334
  }
332
335
  labels.add(tl.name);
336
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
333
337
  if (tl.duration !== void 0 && tl.duration <= 0) {
334
338
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
335
339
  }
@@ -348,6 +352,22 @@ function validateScene(ir) {
348
352
  }
349
353
  };
350
354
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
355
+ for (const a of beatAnchors) {
356
+ if (a.at === a.name) {
357
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
358
+ } else if (!labels.has(a.at)) {
359
+ problems.push(
360
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
361
+ );
362
+ }
363
+ }
364
+ for (const a of startAnchors) {
365
+ if (!labels.has(a.at)) {
366
+ problems.push(
367
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
368
+ );
369
+ }
370
+ }
351
371
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
352
372
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
353
373
  }
package/dist/diff.js CHANGED
@@ -251,11 +251,68 @@ function compileScene(ir) {
251
251
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
252
252
  const natural = durationOf(grouping, 0);
253
253
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
254
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
254
+ const at = typeof tl.at === "number" ? tl.at : void 0;
255
+ const beatStart = at ?? start + (tl.gap ?? 0);
255
256
  return beatStart + k * natural;
256
257
  }
257
258
  }
258
259
  };
260
+ let labelClock;
261
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
262
+ if (ir.timeline && anyAnchor(ir.timeline)) {
263
+ const clock = /* @__PURE__ */ new Map();
264
+ const clockWalk = (tl, start) => {
265
+ let end = start;
266
+ switch (tl.kind) {
267
+ case "seq": {
268
+ let t = start;
269
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
270
+ end = t;
271
+ break;
272
+ }
273
+ case "par": {
274
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
275
+ break;
276
+ }
277
+ case "stagger": {
278
+ tl.children.forEach((c, i) => {
279
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
280
+ });
281
+ break;
282
+ }
283
+ case "wait":
284
+ end = start + tl.duration;
285
+ break;
286
+ case "tween":
287
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
288
+ break;
289
+ case "motionPath":
290
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
291
+ break;
292
+ case "to": {
293
+ const override = ir.states?.[tl.state] ?? {};
294
+ const si = tl.stagger ?? 0;
295
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
296
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
297
+ break;
298
+ }
299
+ case "beat": {
300
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
301
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
302
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
303
+ const at = typeof tl.at === "number" ? tl.at : void 0;
304
+ const beatStart = at ?? start + (tl.gap ?? 0);
305
+ end = clockWalk(inner, beatStart);
306
+ clock.set(tl.name, { t0: beatStart, t1: end });
307
+ break;
308
+ }
309
+ }
310
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
311
+ return end;
312
+ };
313
+ clockWalk(ir.timeline, 0);
314
+ labelClock = clock;
315
+ }
259
316
  const walk = (tl, start) => {
260
317
  const end = walkInner(tl, start);
261
318
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -272,7 +329,8 @@ function compileScene(ir) {
272
329
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
273
330
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
274
331
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
275
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
332
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
333
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
276
334
  const end = walk(inner, beatStart);
277
335
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
278
336
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -473,6 +531,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
473
531
  function validateScene(ir) {
474
532
  const problems = [];
475
533
  const nodeById = /* @__PURE__ */ new Map();
534
+ const startAnchors = [];
476
535
  const checkPaint = (where, value) => {
477
536
  if (typeof value !== "object" || value === null) return;
478
537
  const g = value;
@@ -505,6 +564,7 @@ function validateScene(ir) {
505
564
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
506
565
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
507
566
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
567
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
508
568
  if (node.type === "group") {
509
569
  const clip = node.props.clip;
510
570
  if (clip) {
@@ -565,6 +625,7 @@ function validateScene(ir) {
565
625
  );
566
626
  }
567
627
  const labels = /* @__PURE__ */ new Set();
628
+ const beatAnchors = [];
568
629
  const checkEase = (path2, ease) => {
569
630
  if (ease === void 0) return;
570
631
  if (typeof ease === "string") {
@@ -656,6 +717,7 @@ function validateScene(ir) {
656
717
  );
657
718
  }
658
719
  labels.add(tl.name);
720
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
659
721
  if (tl.duration !== void 0 && tl.duration <= 0) {
660
722
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
661
723
  }
@@ -674,6 +736,22 @@ function validateScene(ir) {
674
736
  }
675
737
  };
676
738
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
739
+ for (const a of beatAnchors) {
740
+ if (a.at === a.name) {
741
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
742
+ } else if (!labels.has(a.at)) {
743
+ problems.push(
744
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
745
+ );
746
+ }
747
+ }
748
+ for (const a of startAnchors) {
749
+ if (!labels.has(a.at)) {
750
+ problems.push(
751
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
752
+ );
753
+ }
754
+ }
677
755
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
678
756
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
679
757
  }
@@ -980,7 +1058,7 @@ ${stderr.slice(-2e3)}`))
980
1058
  });
981
1059
  }
982
1060
  function neededSeconds(node, duration) {
983
- const start = node.props.start ?? 0;
1061
+ const start = typeof node.props.start === "string" ? 0 : node.props.start ?? 0;
984
1062
  const rate = node.props.rate ?? 1;
985
1063
  const clipStart = node.props.clipStart ?? 0;
986
1064
  return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;
package/dist/frame.js CHANGED
@@ -278,11 +278,68 @@ function compileScene(ir) {
278
278
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
279
279
  const natural = durationOf(grouping, 0);
280
280
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
281
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
281
+ const at = typeof tl.at === "number" ? tl.at : void 0;
282
+ const beatStart = at ?? start + (tl.gap ?? 0);
282
283
  return beatStart + k * natural;
283
284
  }
284
285
  }
285
286
  };
287
+ let labelClock;
288
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
289
+ if (ir.timeline && anyAnchor(ir.timeline)) {
290
+ const clock = /* @__PURE__ */ new Map();
291
+ const clockWalk = (tl, start) => {
292
+ let end = start;
293
+ switch (tl.kind) {
294
+ case "seq": {
295
+ let t = start;
296
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
297
+ end = t;
298
+ break;
299
+ }
300
+ case "par": {
301
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
302
+ break;
303
+ }
304
+ case "stagger": {
305
+ tl.children.forEach((c, i) => {
306
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
307
+ });
308
+ break;
309
+ }
310
+ case "wait":
311
+ end = start + tl.duration;
312
+ break;
313
+ case "tween":
314
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
315
+ break;
316
+ case "motionPath":
317
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
318
+ break;
319
+ case "to": {
320
+ const override = ir.states?.[tl.state] ?? {};
321
+ const si = tl.stagger ?? 0;
322
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
323
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
324
+ break;
325
+ }
326
+ case "beat": {
327
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
328
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
329
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
330
+ const at = typeof tl.at === "number" ? tl.at : void 0;
331
+ const beatStart = at ?? start + (tl.gap ?? 0);
332
+ end = clockWalk(inner, beatStart);
333
+ clock.set(tl.name, { t0: beatStart, t1: end });
334
+ break;
335
+ }
336
+ }
337
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
338
+ return end;
339
+ };
340
+ clockWalk(ir.timeline, 0);
341
+ labelClock = clock;
342
+ }
286
343
  const walk = (tl, start) => {
287
344
  const end = walkInner(tl, start);
288
345
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -299,7 +356,8 @@ function compileScene(ir) {
299
356
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
300
357
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
301
358
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
302
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
359
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
360
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
303
361
  const end = walk(inner, beatStart);
304
362
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
305
363
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -500,6 +558,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
500
558
  function validateScene(ir) {
501
559
  const problems = [];
502
560
  const nodeById = /* @__PURE__ */ new Map();
561
+ const startAnchors = [];
503
562
  const checkPaint = (where, value) => {
504
563
  if (typeof value !== "object" || value === null) return;
505
564
  const g = value;
@@ -532,6 +591,7 @@ function validateScene(ir) {
532
591
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
533
592
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
534
593
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
594
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
535
595
  if (node.type === "group") {
536
596
  const clip = node.props.clip;
537
597
  if (clip) {
@@ -592,6 +652,7 @@ function validateScene(ir) {
592
652
  );
593
653
  }
594
654
  const labels = /* @__PURE__ */ new Set();
655
+ const beatAnchors = [];
595
656
  const checkEase = (path2, ease) => {
596
657
  if (ease === void 0) return;
597
658
  if (typeof ease === "string") {
@@ -683,6 +744,7 @@ function validateScene(ir) {
683
744
  );
684
745
  }
685
746
  labels.add(tl.name);
747
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
686
748
  if (tl.duration !== void 0 && tl.duration <= 0) {
687
749
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
688
750
  }
@@ -701,6 +763,22 @@ function validateScene(ir) {
701
763
  }
702
764
  };
703
765
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
766
+ for (const a of beatAnchors) {
767
+ if (a.at === a.name) {
768
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
769
+ } else if (!labels.has(a.at)) {
770
+ problems.push(
771
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
772
+ );
773
+ }
774
+ }
775
+ for (const a of startAnchors) {
776
+ if (!labels.has(a.at)) {
777
+ problems.push(
778
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
779
+ );
780
+ }
781
+ }
704
782
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
705
783
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
706
784
  }
@@ -891,7 +969,7 @@ ${stderr.slice(-2e3)}`))
891
969
  });
892
970
  }
893
971
  function neededSeconds(node, duration) {
894
- const start = node.props.start ?? 0;
972
+ const start = typeof node.props.start === "string" ? 0 : node.props.start ?? 0;
895
973
  const rate = node.props.rate ?? 1;
896
974
  const clipStart = node.props.clipStart ?? 0;
897
975
  return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;