reframe-video 0.6.33 → 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.
@@ -219,11 +219,68 @@
219
219
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
220
220
  const natural = durationOf(grouping, 0);
221
221
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
222
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
222
+ const at = typeof tl.at === "number" ? tl.at : void 0;
223
+ const beatStart = at ?? start + (tl.gap ?? 0);
223
224
  return beatStart + k * natural;
224
225
  }
225
226
  }
226
227
  };
228
+ let labelClock;
229
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
230
+ if (ir.timeline && anyAnchor(ir.timeline)) {
231
+ const clock = /* @__PURE__ */ new Map();
232
+ const clockWalk = (tl, start) => {
233
+ let end = start;
234
+ switch (tl.kind) {
235
+ case "seq": {
236
+ let t = start;
237
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
238
+ end = t;
239
+ break;
240
+ }
241
+ case "par": {
242
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
243
+ break;
244
+ }
245
+ case "stagger": {
246
+ tl.children.forEach((c, i) => {
247
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
248
+ });
249
+ break;
250
+ }
251
+ case "wait":
252
+ end = start + tl.duration;
253
+ break;
254
+ case "tween":
255
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
256
+ break;
257
+ case "motionPath":
258
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
259
+ break;
260
+ case "to": {
261
+ const override = ir.states?.[tl.state] ?? {};
262
+ const si = tl.stagger ?? 0;
263
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
264
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
265
+ break;
266
+ }
267
+ case "beat": {
268
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
269
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
270
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
271
+ const at = typeof tl.at === "number" ? tl.at : void 0;
272
+ const beatStart = at ?? start + (tl.gap ?? 0);
273
+ end = clockWalk(inner, beatStart);
274
+ clock.set(tl.name, { t0: beatStart, t1: end });
275
+ break;
276
+ }
277
+ }
278
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
279
+ return end;
280
+ };
281
+ clockWalk(ir.timeline, 0);
282
+ labelClock = clock;
283
+ }
227
284
  const walk = (tl, start) => {
228
285
  const end = walkInner(tl, start);
229
286
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -240,7 +297,8 @@
240
297
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
241
298
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
242
299
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
243
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
300
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
301
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
244
302
  const end = walk(inner, beatStart);
245
303
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
246
304
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -885,7 +943,8 @@
885
943
  const height = num(id, "height", node.props.height);
886
944
  const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
887
945
  const fps = compiled2.ir.fps ?? 30;
888
- const start = node.props.start ?? 0;
946
+ const startRaw = node.props.start;
947
+ const start = typeof startRaw === "string" ? compiled2.labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
889
948
  const rate = node.props.rate ?? 1;
890
949
  const clipStart = node.props.clipStart ?? 0;
891
950
  const srcT = clipStart + Math.max(0, t - start) * rate;
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)) {
@@ -1174,7 +1252,35 @@ function hash01(n, seed) {
1174
1252
  }
1175
1253
 
1176
1254
  // ../core/src/evaluate.ts
1255
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
1256
+ function multiply(m, n) {
1257
+ return [
1258
+ m[0] * n[0] + m[2] * n[1],
1259
+ m[1] * n[0] + m[3] * n[1],
1260
+ m[0] * n[2] + m[2] * n[3],
1261
+ m[1] * n[2] + m[3] * n[3],
1262
+ m[0] * n[4] + m[2] * n[5] + m[4],
1263
+ m[1] * n[4] + m[3] * n[5] + m[5]
1264
+ ];
1265
+ }
1177
1266
  var DEG = Math.PI / 180;
1267
+ function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
1268
+ const r = rotationDeg * Math.PI / 180;
1269
+ if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
1270
+ const cos = Math.cos(r) * scale;
1271
+ const sin = Math.sin(r) * scale;
1272
+ return [cos, sin, -sin, cos, x, y];
1273
+ }
1274
+ const c = Math.cos(r);
1275
+ const s = Math.sin(r);
1276
+ const tx = Math.tan(skewXDeg * Math.PI / 180);
1277
+ const ty = Math.tan(skewYDeg * Math.PI / 180);
1278
+ const R = [c, s, -s, c, 0, 0];
1279
+ const K = [1, ty, tx, 1, 0, 0];
1280
+ const S = [scale * scaleX, 0, 0, scale * scaleY, 0, 0];
1281
+ const m = multiply(R, multiply(K, S));
1282
+ return [m[0], m[1], m[2], m[3], x, y];
1283
+ }
1178
1284
  function behaviorEnvelope(b, t) {
1179
1285
  const from = b.from ?? Number.NEGATIVE_INFINITY;
1180
1286
  const until = b.until ?? Number.POSITIVE_INFINITY;
@@ -1230,6 +1336,38 @@ function sampleProp(compiled, t, target, prop, fallback) {
1230
1336
  }
1231
1337
  return value;
1232
1338
  }
1339
+ function nodeParentMatrix(compiled, id, t) {
1340
+ const num2 = (target, prop, fallback) => {
1341
+ const v = sampleProp(compiled, t, target, prop, fallback);
1342
+ return typeof v === "number" ? v : fallback;
1343
+ };
1344
+ let result = null;
1345
+ const walk = (node, parent) => {
1346
+ if (node.id === id) {
1347
+ result = parent;
1348
+ return true;
1349
+ }
1350
+ if (node.type === "group") {
1351
+ const m = multiply(
1352
+ parent,
1353
+ localMatrix(
1354
+ num2(node.id, "x", node.props.x),
1355
+ num2(node.id, "y", node.props.y),
1356
+ num2(node.id, "rotation", node.props.rotation ?? 0),
1357
+ num2(node.id, "scale", node.props.scale ?? 1),
1358
+ num2(node.id, "scaleX", node.props.scaleX ?? 1),
1359
+ num2(node.id, "scaleY", node.props.scaleY ?? 1),
1360
+ num2(node.id, "skewX", node.props.skewX ?? 0),
1361
+ num2(node.id, "skewY", node.props.skewY ?? 0)
1362
+ )
1363
+ );
1364
+ for (const child of node.children) if (walk(child, m)) return true;
1365
+ }
1366
+ return false;
1367
+ };
1368
+ for (const node of compiled.ir.nodes) if (walk(node, IDENTITY)) break;
1369
+ return result;
1370
+ }
1233
1371
 
1234
1372
  // ../core/src/autoFoley.ts
1235
1373
  var V_MIN = 360;
@@ -1275,6 +1413,10 @@ function autoFoley(compiled, opts = {}) {
1275
1413
  os.push(num(compiled, t, id, "opacity", node.props.opacity ?? 1));
1276
1414
  }
1277
1415
  const speed = (i2) => i2 <= 0 ? 0 : Math.hypot(xs[i2] - xs[i2 - 1], ys[i2] - ys[i2 - 1]) * fps;
1416
+ const worldX = (frame) => {
1417
+ const m = nodeParentMatrix(compiled, id, frame / fps);
1418
+ return m ? m[0] * xs[frame] + m[2] * ys[frame] + m[4] : xs[frame];
1419
+ };
1278
1420
  let i = 1;
1279
1421
  while (i <= N) {
1280
1422
  if (speed(i) <= vMin) {
@@ -1297,19 +1439,20 @@ function autoFoley(compiled, opts = {}) {
1297
1439
  if (peakV > vMin && durS >= MIN_DUR && visible) {
1298
1440
  if (wantWhoosh) {
1299
1441
  const quickFlick = durS < 0.25;
1300
- cands.push({ t: peak / fps, sfx: quickFlick ? "swish" : "whoosh", gain: master * loud(peakV), pan: panOf(xs[peak]), rank: peakV });
1442
+ cands.push({ t: peak / fps, sfx: quickFlick ? "swish" : "whoosh", gain: master * loud(peakV), pan: panOf(worldX(peak)), rank: peakV });
1301
1443
  }
1302
1444
  const stopped = b >= N || speed(b + 1) < vStop && speed(Math.min(N, b + 2)) < vStop;
1303
- const landsOnScreen = xs[b] >= 0 && xs[b] <= W && os[b] > 0.1;
1445
+ const wxb = worldX(b);
1446
+ const landsOnScreen = wxb >= 0 && wxb <= W && os[b] > 0.1;
1304
1447
  if (wantImpact && peakV > vDecel && stopped && landsOnScreen && b < N) {
1305
- cands.push({ t: (b + 1) / fps, sfx: size > 220 ? "thud" : "knock", gain: master * loud(peakV), pan: panOf(xs[b]), rank: peakV * 1.1 });
1448
+ cands.push({ t: (b + 1) / fps, sfx: size > 220 ? "thud" : "knock", gain: master * loud(peakV), pan: panOf(worldX(b)), rank: peakV * 1.1 });
1306
1449
  }
1307
1450
  }
1308
1451
  }
1309
1452
  if (wantPop && ss[0] < 0.25) {
1310
1453
  for (let k = 1; k <= N; k++) {
1311
1454
  if (ss[k - 1] < 0.5 && ss[k] >= 0.5 && os[k] > 0.05) {
1312
- cands.push({ t: k / fps, sfx: "pop", gain: master * 0.7, pan: panOf(xs[k]), rank: 600 });
1455
+ cands.push({ t: k / fps, sfx: "pop", gain: master * 0.7, pan: panOf(worldX(k)), rank: 600 });
1313
1456
  break;
1314
1457
  }
1315
1458
  }
@@ -1378,13 +1521,14 @@ var SFX_DURATION = {
1378
1521
  camera: 0.18
1379
1522
  };
1380
1523
  var FILE_CUE_DURATION = 0.4;
1381
- function collectClipAudio(ir, duration, warnings) {
1524
+ function collectClipAudio(ir, duration, labelTimes, warnings) {
1382
1525
  const out = [];
1383
1526
  const walk = (nodes) => {
1384
1527
  for (const node of nodes) {
1385
1528
  if (node.type === "video") {
1386
1529
  const gain = node.props.volume ?? 1;
1387
- 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;
1388
1532
  if (gain <= 0) continue;
1389
1533
  if (start >= duration) {
1390
1534
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
@@ -1402,7 +1546,7 @@ function resolveAudioPlan(compiled) {
1402
1546
  const audio = compiled.ir.audio;
1403
1547
  const warnings = [];
1404
1548
  const duration = compiled.duration;
1405
- const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
1549
+ const clipAudio = collectClipAudio(compiled.ir, duration, compiled.labelTimes, warnings);
1406
1550
  const autoCues = audio?.autoFoley ? autoFoley(compiled, audio.autoFoley === true ? {} : audio.autoFoley) : [];
1407
1551
  const manualCues = [...audio?.cues ?? [], ...autoCues];
1408
1552
  if (!audio || !audio.bgm && manualCues.length === 0) {
@@ -2599,7 +2743,7 @@ ${stderr.slice(-2e3)}`))
2599
2743
  });
2600
2744
  }
2601
2745
  function neededSeconds(node, duration) {
2602
- const start = node.props.start ?? 0;
2746
+ const start = typeof node.props.start === "string" ? 0 : node.props.start ?? 0;
2603
2747
  const rate = node.props.rate ?? 1;
2604
2748
  const clipStart = node.props.clipStart ?? 0;
2605
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;