@cutcraft/kit 0.1.0 → 0.2.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.
@@ -1,5 +1,5 @@
1
- import { z } from "zod";
2
1
  import { GRAPHIC_KINDS, GRAPHIC_KIND_NAMES, graphicControls, hash, random } from "@cutcraft/graphics";
2
+ import { z } from "zod";
3
3
  import { Children, Fragment, createElement, isValidElement } from "react";
4
4
  // \0rolldown/runtime.js
5
5
  var __defProp = Object.defineProperty;
@@ -12,429 +12,151 @@ var __exportAll = (all, no_symbols) => {
12
12
  if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
13
13
  return target;
14
14
  };
15
- // src/jsx/define.ts
16
- /**
17
- * A vocabulary element is an ordinary React component that also carries its
18
- * kind, its Zod prop schema and its editing capabilities, so the resolver and
19
- * the editor can read the element tree without rendering it.
20
- */
21
- function defineElement(def) {
22
- const component = ((props) => {
23
- if (!def.render) return null;
24
- const { children, ...rest } = props;
25
- const parsed = def.schema.parse(rest);
26
- return def.render({
27
- ...parsed,
28
- children
29
- });
30
- });
31
- Object.defineProperties(component, {
32
- kind: {
33
- value: def.kind,
34
- enumerable: true
35
- },
36
- schema: {
37
- value: def.schema,
38
- enumerable: true
39
- },
40
- isVocabulary: {
41
- value: true,
42
- enumerable: true
43
- },
44
- capabilities: {
45
- value: def.capabilities,
46
- enumerable: true
47
- },
48
- displayName: {
49
- value: def.kind,
50
- enumerable: true
15
+ // src/patches/log.ts
16
+ var Log = class {
17
+ list = [];
18
+ reduced = /* @__PURE__ */ new Map();
19
+ v = 0;
20
+ isSource;
21
+ constructor(options) {
22
+ this.isSource = options.isSource ?? (() => false);
23
+ }
24
+ get entries() {
25
+ return this.list;
26
+ }
27
+ get net() {
28
+ return this.reduced;
29
+ }
30
+ get version() {
31
+ return this.v;
32
+ }
33
+ append(patch) {
34
+ this.list.push(patch);
35
+ reduceInto(this.reduced, patch, this.isSource);
36
+ this.v += 1;
37
+ }
38
+ mark() {
39
+ return this.list.length;
40
+ }
41
+ truncate(mark) {
42
+ if (mark >= this.list.length) return;
43
+ this.list.length = Math.max(0, mark);
44
+ this.reduced.clear();
45
+ for (const p of this.list) reduceInto(this.reduced, p, this.isSource);
46
+ this.v += 1;
47
+ }
48
+ clear() {
49
+ this.list.length = 0;
50
+ this.reduced.clear();
51
+ this.v += 1;
52
+ }
53
+ };
54
+ function createPatchLog(options = {}) {
55
+ return new Log(options);
56
+ }
57
+ function entry(net, id) {
58
+ let e = net.get(id);
59
+ if (!e) {
60
+ e = {
61
+ id,
62
+ presence: "present",
63
+ props: /* @__PURE__ */ new Map(),
64
+ bake: false
65
+ };
66
+ net.set(id, e);
67
+ }
68
+ return e;
69
+ }
70
+ /** Keep `net` reduced: last write wins per prop, structural ops collapse, removal of an insert vanishes. */
71
+ function reduceInto(net, patch, isSource = () => false) {
72
+ switch (patch.op) {
73
+ case "set": {
74
+ const e = entry(net, patch.id);
75
+ if (e.presence === "removed") throw new Error(`patch set on removed element ${patch.id}`);
76
+ e.props.set(patch.prop, patch.value);
77
+ return;
51
78
  }
52
- });
53
- return component;
79
+ case "text": {
80
+ const e = entry(net, patch.id);
81
+ if (e.presence === "removed") throw new Error(`patch text on removed element ${patch.id}`);
82
+ e.text = patch.text;
83
+ return;
84
+ }
85
+ case "move": {
86
+ const e = entry(net, patch.id);
87
+ if (e.presence === "removed") throw new Error(`patch move on removed element ${patch.id}`);
88
+ e.place = {
89
+ parent: patch.parent,
90
+ after: patch.after
91
+ };
92
+ return;
93
+ }
94
+ case "remove": {
95
+ if (net.get(patch.id)?.presence === "inserted" && !isSource(patch.id)) {
96
+ net.delete(patch.id);
97
+ return;
98
+ }
99
+ const r = entry(net, patch.id);
100
+ r.presence = "removed";
101
+ r.props.clear();
102
+ delete r.text;
103
+ delete r.place;
104
+ delete r.node;
105
+ return;
106
+ }
107
+ case "insert": {
108
+ const e = entry(net, patch.id);
109
+ e.presence = e.presence === "removed" && e.node !== void 0 && nodesEqual(e.node, patch.node) ? "present" : "inserted";
110
+ e.props.clear();
111
+ e.node = patch.node;
112
+ e.place = {
113
+ parent: patch.parent,
114
+ after: patch.after
115
+ };
116
+ return;
117
+ }
118
+ case "bake":
119
+ entry(net, patch.id).bake = true;
120
+ return;
121
+ }
54
122
  }
55
- function isVocabulary(type) {
56
- return typeof type === "function" && type.isVocabulary === true;
123
+ function nodesEqual(a, b) {
124
+ return a.id === b.id && a.kind === b.kind && stableJson(a.props) === stableJson(b.props) && a.text === b.text && a.children.length === b.children.length && a.children.every((c, i) => nodesEqual(c, b.children[i]));
57
125
  }
58
- function kindOf(type) {
59
- return isVocabulary(type) ? type.kind : null;
126
+ function stableJson(value) {
127
+ return JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a < b ? -1 : 1)) : v);
60
128
  }
61
- // src/jsx/enums.ts
62
- const TRACK_LAYOUTS = ["sequence", "free"];
63
- const TRACK_ROLES = [
64
- "general",
65
- "captions",
66
- "dialogue",
67
- "music",
68
- "effects",
69
- "ambience"
70
- ];
71
- const FIT_MODES = [
72
- "contain",
73
- "cover",
74
- "stretch",
75
- "none"
129
+ function asJson(value) {
130
+ if (value === void 0) return void 0;
131
+ return JSON.parse(JSON.stringify(value));
132
+ }
133
+ // src/jsx/capabilities.ts
134
+ const none = {
135
+ timed: false,
136
+ visual: false,
137
+ audio: false,
138
+ trimSource: false,
139
+ transformable: false,
140
+ container: false,
141
+ childKinds: [],
142
+ placementScope: "none"
143
+ };
144
+ const with_ = (over) => ({
145
+ ...none,
146
+ ...over
147
+ });
148
+ const CLIP_CHILDREN = [
149
+ "effect",
150
+ "colorGrade",
151
+ "keyframe",
152
+ "mask",
153
+ "speedKey",
154
+ "freeze"
76
155
  ];
77
- const BLEND_MODES = [
78
- "normal",
79
- "multiply",
80
- "screen",
81
- "overlay",
82
- "darken",
83
- "lighten",
84
- "color-dodge",
85
- "color-burn",
86
- "hard-light",
87
- "soft-light",
88
- "difference",
89
- "exclusion",
90
- "hue",
91
- "saturation",
92
- "color",
93
- "luminosity",
94
- "add",
95
- "linear-dodge"
96
- ];
97
- const EASINGS = [
98
- "linear",
99
- "ease",
100
- "ease-in",
101
- "ease-out",
102
- "ease-in-out",
103
- "hold",
104
- "bezier",
105
- "smoothstep",
106
- "smootherstep",
107
- "snappy",
108
- "smooth",
109
- "spring",
110
- "easeInQuad",
111
- "easeOutQuad",
112
- "easeInOutQuad",
113
- "easeInCubic",
114
- "easeOutCubic",
115
- "easeInOutCubic",
116
- "easeInQuart",
117
- "easeOutQuart",
118
- "easeInOutQuart",
119
- "easeInQuint",
120
- "easeOutQuint",
121
- "easeInOutQuint",
122
- "easeInSine",
123
- "easeOutSine",
124
- "easeInOutSine",
125
- "easeInExpo",
126
- "easeOutExpo",
127
- "easeInOutExpo",
128
- "easeInCirc",
129
- "easeOutCirc",
130
- "easeInOutCirc",
131
- "easeInBack",
132
- "easeOutBack",
133
- "easeInOutBack",
134
- "easeInBackStrong",
135
- "easeOutBackStrong",
136
- "easeInOutBackStrong",
137
- "easeInElastic",
138
- "easeOutElastic",
139
- "easeInOutElastic",
140
- "easeInElasticSoft",
141
- "easeOutElasticSoft",
142
- "easeInOutElasticSoft",
143
- "easeInBounce",
144
- "easeOutBounce",
145
- "easeInOutBounce",
146
- "easeInBounceSmall",
147
- "easeOutBounceSmall",
148
- "easeInOutBounceSmall"
149
- ];
150
- const MASK_EASINGS = [
151
- "linear",
152
- "ease-in",
153
- "ease-out",
154
- "ease-in-out"
155
- ];
156
- const TRANSITION_TYPES = [
157
- "crossfade",
158
- "dipToBlack",
159
- "dipToWhite",
160
- "wipe",
161
- "slide",
162
- "zoom",
163
- "push",
164
- "circleReveal",
165
- "blur",
166
- "whipPan",
167
- "radialWipe",
168
- "pixelate",
169
- "glitch",
170
- "blinds",
171
- "diamondReveal",
172
- "spin",
173
- "flip",
174
- "splitReveal",
175
- "flash",
176
- "filmBurn",
177
- "mosaic",
178
- "ripple",
179
- "pageTurn",
180
- "colorSplit"
181
- ];
182
- const VIDEO_EFFECT_TYPES = [
183
- "brightness",
184
- "contrast",
185
- "saturation",
186
- "hue",
187
- "temperature",
188
- "tint",
189
- "blur",
190
- "sharpen",
191
- "vignette",
192
- "grain",
193
- "shadow",
194
- "glow",
195
- "motion-blur",
196
- "radial-blur",
197
- "chromatic-aberration",
198
- "grayscale",
199
- "sepia",
200
- "invert",
201
- "levels",
202
- "hue-saturation"
203
- ];
204
- const AUDIO_EFFECT_TYPES = [
205
- "gain",
206
- "pan",
207
- "eq",
208
- "compressor",
209
- "reverb",
210
- "delay",
211
- "noiseReduction",
212
- "fadeIn",
213
- "fadeOut"
214
- ];
215
- const TEXT_ANIMATION_PRESETS = [
216
- "none",
217
- "typewriter",
218
- "fade",
219
- "slide-up",
220
- "slide-down",
221
- "slide-left",
222
- "slide-right",
223
- "scale",
224
- "blur",
225
- "bounce",
226
- "rotate",
227
- "pop",
228
- "flip",
229
- "split",
230
- "word-by-word",
231
- "rise",
232
- "drop",
233
- "elastic",
234
- "swing",
235
- "zoom-blur",
236
- "cascade",
237
- "wave",
238
- "shake",
239
- "rainbow",
240
- "glitch"
241
- ];
242
- const CAPTION_ANIMATIONS = [
243
- "none",
244
- "word-highlight",
245
- "word-by-word",
246
- "karaoke",
247
- "bounce",
248
- "typewriter"
249
- ];
250
- const GRAPHIC_ANIMATIONS = [
251
- "none",
252
- "fade",
253
- "slide-left",
254
- "slide-right",
255
- "slide-up",
256
- "slide-down",
257
- "scale",
258
- "rotate",
259
- "bounce",
260
- "pop",
261
- "draw",
262
- "wipe-left",
263
- "wipe-right",
264
- "wipe-up",
265
- "wipe-down",
266
- "reveal-center",
267
- "reveal-edges",
268
- "elastic",
269
- "flip-horizontal",
270
- "flip-vertical"
271
- ];
272
- const EMPHASIS_ANIMATIONS = [
273
- "none",
274
- "pulse",
275
- "shake",
276
- "bounce",
277
- "float",
278
- "spin",
279
- "flash",
280
- "heartbeat",
281
- "swing",
282
- "wobble",
283
- "jello",
284
- "rubber-band",
285
- "tada",
286
- "vibrate",
287
- "flicker",
288
- "glow",
289
- "breathe",
290
- "wave",
291
- "tilt",
292
- "zoom-pulse",
293
- "focus-zoom",
294
- "pan-left",
295
- "pan-right",
296
- "pan-up",
297
- "pan-down",
298
- "ken-burns"
299
- ];
300
- const SHAPE_TYPES = [
301
- "rectangle",
302
- "circle",
303
- "ellipse",
304
- "triangle",
305
- "arrow",
306
- "line",
307
- "polygon",
308
- "path",
309
- "star"
310
- ];
311
- const KEYFRAME_PROPERTIES = [
312
- "transform.position.x",
313
- "transform.position.y",
314
- "transform.position.z",
315
- "transform.scale.x",
316
- "transform.scale.y",
317
- "transform.rotation",
318
- "transform.rotation.x",
319
- "transform.rotation.y",
320
- "transform.perspective",
321
- "transform.opacity",
322
- "transform.anchor.x",
323
- "transform.anchor.y",
324
- "transform.borderRadius",
325
- "transform.crop.x",
326
- "transform.crop.y",
327
- "transform.crop.width",
328
- "transform.crop.height",
329
- "audio.volume",
330
- "audio.pan",
331
- "colorGrade.temperature",
332
- "colorGrade.tint",
333
- "path"
334
- ];
335
- /** `effect.<effectId>.<param>` is also a valid keyframe property; see `isKeyframeProperty`. */
336
- const KEYFRAME_PROPERTY_PATTERN = /^(effect\.[A-Za-z0-9_-]+\.[A-Za-z0-9_]+)$/;
337
- function isKeyframeProperty(value) {
338
- return KEYFRAME_PROPERTIES.includes(value) || KEYFRAME_PROPERTY_PATTERN.test(value);
339
- }
340
- const CAPTION_POSITIONS = [
341
- "top",
342
- "center",
343
- "bottom"
344
- ];
345
- const TEXT_ALIGNS = [
346
- "left",
347
- "center",
348
- "right",
349
- "justify"
350
- ];
351
- const TEXT_VALIGNS = [
352
- "top",
353
- "middle",
354
- "bottom"
355
- ];
356
- const TEXT_DECORATIONS = [
357
- "none",
358
- "underline",
359
- "line-through",
360
- "overline"
361
- ];
362
- const ANIMATION_UNITS = [
363
- "character",
364
- "word",
365
- "line"
366
- ];
367
- const MASK_TYPES = [
368
- "shape",
369
- "drawn",
370
- "matte"
371
- ];
372
- const MATTE_CHANNELS = [
373
- "alpha",
374
- "luminance",
375
- "bounds"
376
- ];
377
- const INTERPOLATION_QUALITIES = [
378
- "low",
379
- "medium",
380
- "high"
381
- ];
382
- const STABILIZE_CROP_MODES = ["auto", "none"];
383
- const SVG_COLOR_MODES = [
384
- "none",
385
- "tint",
386
- "replace"
387
- ];
388
- const AUTOMATION_LANES = ["volume", "pan"];
389
- const TRACK_FLAGS = [
390
- "locked",
391
- "hidden",
392
- "muted",
393
- "solo"
394
- ];
395
- const zTrackLayout = z.enum(TRACK_LAYOUTS);
396
- const zTrackRole = z.enum(TRACK_ROLES);
397
- const zFitMode = z.enum(FIT_MODES);
398
- const zBlendMode = z.enum(BLEND_MODES);
399
- const zEasing = z.enum(EASINGS);
400
- const zMaskEasing = z.enum(MASK_EASINGS);
401
- const zTransitionType = z.enum(TRANSITION_TYPES);
402
- const zVideoEffectType = z.enum(VIDEO_EFFECT_TYPES);
403
- const zAudioEffectType = z.enum(AUDIO_EFFECT_TYPES);
404
- const zTextAnimationPreset = z.enum(TEXT_ANIMATION_PRESETS);
405
- const zCaptionAnimation = z.enum(CAPTION_ANIMATIONS);
406
- const zGraphicAnimation = z.enum(GRAPHIC_ANIMATIONS);
407
- const zEmphasisAnimation = z.enum(EMPHASIS_ANIMATIONS);
408
- const zShapeType = z.enum(SHAPE_TYPES);
409
- const zKeyframeProperty = z.string().refine(isKeyframeProperty, "unknown keyframe property");
410
- // src/jsx/capabilities.ts
411
- const none = {
412
- timed: false,
413
- visual: false,
414
- audio: false,
415
- trimSource: false,
416
- transformable: false,
417
- container: false,
418
- childKinds: [],
419
- placementScope: "none"
420
- };
421
- const with_ = (over) => ({
422
- ...none,
423
- ...over
424
- });
425
- const CLIP_CHILDREN = [
426
- "effect",
427
- "audioEffect",
428
- "colorGrade",
429
- "keyframe",
430
- "mask",
431
- "speedKey",
432
- "freeze"
433
- ];
434
- const OVERLAY_CHILDREN = [
435
- "effect",
436
- "keyframe",
437
- "mask"
156
+ const OVERLAY_CHILDREN = [
157
+ "effect",
158
+ "keyframe",
159
+ "mask"
438
160
  ];
439
161
  /**
440
162
  * What each element kind can do. Ported from OpenReel's timeline-items table,
@@ -486,7 +208,7 @@ const KIND_CAPABILITIES = {
486
208
  audio: true,
487
209
  trimSource: true,
488
210
  container: true,
489
- childKinds: ["audioEffect", "keyframe"],
211
+ childKinds: ["keyframe"],
490
212
  placementScope: "track"
491
213
  }),
492
214
  gap: with_({
@@ -585,11 +307,10 @@ const KIND_CAPABILITIES = {
585
307
  visual: true,
586
308
  transformable: true,
587
309
  container: true,
588
- childKinds: ["keyframe"],
310
+ childKinds: ["effect", "keyframe"],
589
311
  placementScope: "track"
590
312
  }),
591
313
  effect: with_({ placementScope: "child" }),
592
- audioEffect: with_({ placementScope: "child" }),
593
314
  colorGrade: with_({ placementScope: "child" }),
594
315
  keyframe: with_({ placementScope: "child" }),
595
316
  speedKey: with_({ placementScope: "child" }),
@@ -598,308 +319,299 @@ const KIND_CAPABILITIES = {
598
319
  function capabilitiesOf(kind) {
599
320
  return KIND_CAPABILITIES[kind] ?? none;
600
321
  }
601
- // src/jsx/rendering.ts
322
+ // src/jsx/define.ts
602
323
  /**
603
- * What the player and the renderer actually draw and play, out of what the
604
- * schemas accept. The player reads these lists; the `not-rendered` lint
605
- * reports everything else, so a file that checks clean renders as written.
324
+ * A vocabulary element is an ordinary React component that also carries its
325
+ * kind, its Zod prop schema and its editing capabilities, so the resolver and
326
+ * the editor can read the element tree without rendering it.
606
327
  */
607
- const RENDERED_TRANSITIONS = [
608
- "crossfade",
609
- "dipToBlack",
610
- "dipToWhite",
611
- "wipe",
612
- "slide",
613
- "push"
614
- ];
615
- const RENDERED_EFFECTS = [
616
- "brightness",
617
- "contrast",
618
- "saturation",
619
- "hue",
620
- "blur",
621
- "grayscale",
622
- "sepia",
623
- "invert",
624
- "shadow",
625
- "temperature",
626
- "tint",
627
- "levels",
628
- "vignette",
629
- "grain"
630
- ];
631
- const RENDERED_AUDIO_EFFECTS = [];
632
- const RENDERED_TITLE_ANIMATIONS = [
633
- "none",
634
- "fade",
635
- "slide-up",
636
- "slide-down",
637
- "slide-left",
638
- "slide-right",
639
- "scale",
640
- "typewriter",
641
- "word-by-word"
642
- ];
643
- const RENDERED_CAPTION_ANIMATIONS = [
644
- "none",
645
- "word-highlight",
646
- "word-by-word",
647
- "karaoke"
648
- ];
649
- const RENDERED_MASK_TYPES = ["shape", "drawn"];
650
- const RENDERED_COLOR_GRADE = ["temperature", "tint"];
651
- /** Masks render on these kinds, one per element. */
652
- const MASKED_KINDS = ["clip", "nested"];
653
- const RENDERED_VALUES = [
654
- {
655
- kind: "transition",
656
- prop: "type",
657
- values: RENDERED_TRANSITIONS,
658
- reason: "draws as a crossfade"
659
- },
660
- {
661
- kind: "effect",
662
- prop: "type",
663
- values: RENDERED_EFFECTS,
664
- reason: "is ignored: the picture renders without it"
665
- },
666
- {
667
- kind: "audioEffect",
668
- prop: "type",
669
- values: RENDERED_AUDIO_EFFECTS,
670
- reason: "is ignored: the audio plays without it"
671
- },
672
- {
673
- kind: "title",
674
- prop: "animation.preset",
675
- values: RENDERED_TITLE_ANIMATIONS,
676
- reason: "shows as a fade"
677
- },
678
- {
679
- kind: "captions",
680
- prop: "animation",
681
- values: RENDERED_CAPTION_ANIMATIONS,
682
- reason: "shows as karaoke"
683
- },
684
- {
685
- kind: "mask",
686
- prop: "type",
687
- values: RENDERED_MASK_TYPES,
688
- reason: "is ignored: matte masks are not rendered, the element shows unmasked"
689
- },
690
- ...[
691
- "title",
692
- "shape",
693
- "svg",
694
- "sticker",
695
- "graphic",
696
- "layer"
697
- ].map((kind) => ({
698
- kind,
699
- prop: "blend",
700
- values: ["normal"],
701
- reason: "blends with the footage in the export only: the preview draws it unblended over the footage"
702
- }))
328
+ function defineElement(def) {
329
+ const component = ((props) => {
330
+ if (!def.render) return null;
331
+ const { children, ...rest } = props;
332
+ const parsed = def.schema.parse(rest);
333
+ return def.render({
334
+ ...parsed,
335
+ children
336
+ });
337
+ });
338
+ Object.defineProperties(component, {
339
+ kind: {
340
+ value: def.kind,
341
+ enumerable: true
342
+ },
343
+ schema: {
344
+ value: def.schema,
345
+ enumerable: true
346
+ },
347
+ isVocabulary: {
348
+ value: true,
349
+ enumerable: true
350
+ },
351
+ capabilities: {
352
+ value: def.capabilities,
353
+ enumerable: true
354
+ },
355
+ displayName: {
356
+ value: def.kind,
357
+ enumerable: true
358
+ }
359
+ });
360
+ return component;
361
+ }
362
+ function isVocabulary(type) {
363
+ return typeof type === "function" && type.isVocabulary === true;
364
+ }
365
+ function kindOf(type) {
366
+ return isVocabulary(type) ? type.kind : null;
367
+ }
368
+ // src/jsx/enums.ts
369
+ const TRACK_LAYOUTS = ["sequence", "free"];
370
+ const TRACK_ROLES = [
371
+ "general",
372
+ "captions",
373
+ "dialogue",
374
+ "music",
375
+ "effects",
376
+ "ambience"
703
377
  ];
704
- const THREE_D = "is ignored: 3D transforms are not rendered";
705
- const NOT_RENDERED = [
706
- {
707
- kind: "*",
708
- prop: "emphasis",
709
- reason: "is ignored: emphasis animations are not rendered"
710
- },
711
- {
712
- kind: "*",
713
- prop: "transform.rotate3d",
714
- reason: THREE_D
715
- },
716
- {
717
- kind: "*",
718
- prop: "transform.perspective",
719
- reason: THREE_D
720
- },
721
- {
722
- kind: "*",
723
- prop: "transform.transformStyle",
724
- reason: THREE_D
725
- },
726
- {
727
- kind: "keyframe",
728
- prop: "property",
729
- value: "transform.position.z",
730
- reason: THREE_D
731
- },
732
- {
733
- kind: "keyframe",
734
- prop: "property",
735
- value: "transform.rotation.x",
736
- reason: THREE_D
737
- },
738
- {
739
- kind: "keyframe",
740
- prop: "property",
741
- value: "transform.rotation.y",
742
- reason: THREE_D
743
- },
744
- {
745
- kind: "keyframe",
746
- prop: "property",
747
- value: "transform.perspective",
748
- reason: THREE_D
749
- },
750
- {
751
- kind: "keyframe",
752
- prop: "property",
753
- value: "path",
754
- reason: "is ignored: paths do not animate"
755
- },
756
- {
757
- kind: "keyframe",
758
- prop: "handles",
759
- reason: "is ignored: the segment follows its easing"
760
- },
761
- {
762
- kind: "keyframe",
763
- prop: "roving",
764
- reason: "is ignored: the key stays at its time"
765
- },
766
- {
767
- kind: "clip",
768
- prop: "chromaKey",
769
- reason: "is ignored: the green screen stays in the picture"
770
- },
771
- {
772
- kind: "clip",
773
- prop: "stabilize",
774
- reason: "is ignored: the footage renders unstabilised"
775
- },
776
- {
777
- kind: "clip",
778
- prop: "behindSubject",
779
- reason: "is ignored: the clip draws in its track order"
780
- },
781
- {
782
- kind: "title",
783
- prop: "behindSubject",
784
- reason: "is ignored: the title draws over the footage"
785
- },
786
- {
787
- kind: "title",
788
- prop: "text3d",
789
- reason: "is ignored: the title renders as flat text"
790
- },
791
- {
792
- kind: "clip",
793
- prop: "smoothSlowMo",
794
- reason: "is ignored: slow motion repeats frames"
795
- },
796
- {
797
- kind: "clip",
798
- prop: "interpolation",
799
- reason: "is ignored: slow motion repeats frames"
800
- },
801
- {
802
- kind: "clip",
803
- prop: "pitchCorrection",
804
- reason: "is ignored: sped-up or slowed audio changes pitch"
805
- },
806
- {
807
- kind: "audio",
808
- prop: "pitchCorrection",
809
- reason: "is ignored: sped-up or slowed audio changes pitch"
810
- },
811
- {
812
- kind: "clip",
813
- prop: "audioTrack",
814
- reason: "is ignored: the asset's first audio stream plays"
815
- },
816
- {
817
- kind: "audio",
818
- prop: "audioTrack",
819
- reason: "is ignored: the asset's first audio stream plays"
820
- },
821
- {
822
- kind: "speedKey",
823
- parent: "nested",
824
- reason: "is ignored inside <Nested>: the sequence plays at its constant speed"
825
- },
826
- {
827
- kind: "freeze",
828
- parent: "nested",
829
- reason: "is ignored inside <Nested>: the sequence plays at its constant speed"
830
- },
831
- {
832
- kind: "effect",
833
- prop: "params.gamma",
834
- reason: "is ignored: levels render their black and white points only"
835
- },
836
- {
837
- kind: "transition",
838
- prop: "params.direction",
839
- value: "diagonal",
840
- reason: "draws as a wipe to the left"
841
- },
842
- {
843
- kind: "transition",
844
- prop: "params.softness",
845
- reason: "is ignored: the wipe has a hard edge"
846
- },
847
- {
848
- kind: "adjustment",
849
- prop: "affects",
850
- reason: "is ignored: the adjustment filters every track below it"
851
- },
852
- {
853
- kind: "composition",
854
- prop: "background.mode",
855
- value: "blur",
856
- reason: "draws as the plain colour when an overlay sits beneath the footage"
857
- },
858
- {
859
- kind: "mask",
860
- prop: "matte",
861
- reason: "is ignored: matte masks are not rendered, the element shows unmasked"
862
- },
863
- {
864
- kind: "captions",
865
- prop: "animation",
866
- value: "bounce",
867
- reason: "has no motion: it shows as karaoke"
868
- },
869
- {
870
- kind: "captions",
871
- prop: "animation",
872
- value: "typewriter",
873
- reason: "types out only cues without word timings; cues with words show as karaoke"
874
- },
875
- ...[
876
- "wheels",
877
- "curves",
878
- "lut",
879
- "hsl"
880
- ].map((prop) => ({
881
- kind: "colorGrade",
882
- prop,
883
- reason: `is ignored: only ${RENDERED_COLOR_GRADE.join(" and ")} render`
884
- }))
378
+ const FIT_MODES = [
379
+ "contain",
380
+ "cover",
381
+ "stretch",
382
+ "none"
885
383
  ];
886
- const MASK_NOT_RENDERED = {
887
- /** On a kind outside MASKED_KINDS. */
888
- kind: "is ignored: masks render on clips and nested sequences only, the element shows unmasked",
889
- second: "is ignored: only the first <Mask> on an element renders"
890
- };
891
- // src/jsx/schemas/enums-local.ts
892
- const zInterpolation = z.enum(INTERPOLATION_QUALITIES);
893
- const zStabilizeCrop = z.enum(STABILIZE_CROP_MODES);
894
- const zCaptionPosition = z.enum(CAPTION_POSITIONS);
895
- const zTextAlign = z.enum(TEXT_ALIGNS);
896
- const zTextValign = z.enum(TEXT_VALIGNS);
897
- const zTextDecoration = z.enum(TEXT_DECORATIONS);
898
- const zAnimationUnit = z.enum(ANIMATION_UNITS);
899
- const zMaskType = z.enum(MASK_TYPES);
900
- const zMatteChannel = z.enum(MATTE_CHANNELS);
901
- const zSvgColorMode = z.enum(SVG_COLOR_MODES);
902
- z.enum(AUTOMATION_LANES);
384
+ const BLEND_MODES = [
385
+ "normal",
386
+ "multiply",
387
+ "screen",
388
+ "overlay",
389
+ "darken",
390
+ "lighten",
391
+ "color-dodge",
392
+ "color-burn",
393
+ "hard-light",
394
+ "soft-light",
395
+ "difference",
396
+ "exclusion",
397
+ "hue",
398
+ "saturation",
399
+ "color",
400
+ "luminosity",
401
+ "add",
402
+ "linear-dodge"
403
+ ];
404
+ const EASINGS = [
405
+ "linear",
406
+ "ease",
407
+ "ease-in",
408
+ "ease-out",
409
+ "ease-in-out",
410
+ "hold",
411
+ "bezier",
412
+ "smoothstep",
413
+ "smootherstep",
414
+ "snappy",
415
+ "smooth",
416
+ "spring",
417
+ "easeInQuad",
418
+ "easeOutQuad",
419
+ "easeInOutQuad",
420
+ "easeInCubic",
421
+ "easeOutCubic",
422
+ "easeInOutCubic",
423
+ "easeInQuart",
424
+ "easeOutQuart",
425
+ "easeInOutQuart",
426
+ "easeInQuint",
427
+ "easeOutQuint",
428
+ "easeInOutQuint",
429
+ "easeInSine",
430
+ "easeOutSine",
431
+ "easeInOutSine",
432
+ "easeInExpo",
433
+ "easeOutExpo",
434
+ "easeInOutExpo",
435
+ "easeInCirc",
436
+ "easeOutCirc",
437
+ "easeInOutCirc",
438
+ "easeInBack",
439
+ "easeOutBack",
440
+ "easeInOutBack",
441
+ "easeInBackStrong",
442
+ "easeOutBackStrong",
443
+ "easeInOutBackStrong",
444
+ "easeInElastic",
445
+ "easeOutElastic",
446
+ "easeInOutElastic",
447
+ "easeInElasticSoft",
448
+ "easeOutElasticSoft",
449
+ "easeInOutElasticSoft",
450
+ "easeInBounce",
451
+ "easeOutBounce",
452
+ "easeInOutBounce",
453
+ "easeInBounceSmall",
454
+ "easeOutBounceSmall",
455
+ "easeInOutBounceSmall"
456
+ ];
457
+ const MASK_EASINGS = [
458
+ "linear",
459
+ "ease-in",
460
+ "ease-out",
461
+ "ease-in-out"
462
+ ];
463
+ const TRANSITION_TYPES = [
464
+ "crossfade",
465
+ "dipToBlack",
466
+ "dipToWhite",
467
+ "wipe",
468
+ "slide",
469
+ "push"
470
+ ];
471
+ const VIDEO_EFFECT_TYPES = [
472
+ "brightness",
473
+ "contrast",
474
+ "saturation",
475
+ "hue",
476
+ "temperature",
477
+ "tint",
478
+ "blur",
479
+ "vignette",
480
+ "grain",
481
+ "shadow",
482
+ "grayscale",
483
+ "sepia",
484
+ "invert",
485
+ "levels"
486
+ ];
487
+ const TEXT_ANIMATION_PRESETS = [
488
+ "none",
489
+ "typewriter",
490
+ "fade",
491
+ "slide-up",
492
+ "slide-down",
493
+ "slide-left",
494
+ "slide-right",
495
+ "scale",
496
+ "word-by-word"
497
+ ];
498
+ const CAPTION_ANIMATIONS = [
499
+ "none",
500
+ "word-highlight",
501
+ "word-by-word",
502
+ "karaoke",
503
+ "typewriter"
504
+ ];
505
+ const GRAPHIC_ANIMATIONS = [
506
+ "none",
507
+ "fade",
508
+ "slide-left",
509
+ "slide-right",
510
+ "slide-up",
511
+ "slide-down",
512
+ "scale",
513
+ "rotate",
514
+ "bounce",
515
+ "pop",
516
+ "draw",
517
+ "wipe-left",
518
+ "wipe-right",
519
+ "wipe-up",
520
+ "wipe-down",
521
+ "reveal-center",
522
+ "reveal-edges",
523
+ "elastic",
524
+ "flip-horizontal",
525
+ "flip-vertical"
526
+ ];
527
+ const SHAPE_TYPES = [
528
+ "rectangle",
529
+ "circle",
530
+ "ellipse",
531
+ "triangle",
532
+ "arrow",
533
+ "line",
534
+ "polygon",
535
+ "path",
536
+ "star"
537
+ ];
538
+ const KEYFRAME_PROPERTIES = [
539
+ "transform.position.x",
540
+ "transform.position.y",
541
+ "transform.scale.x",
542
+ "transform.scale.y",
543
+ "transform.rotation",
544
+ "transform.opacity",
545
+ "transform.anchor.x",
546
+ "transform.anchor.y",
547
+ "transform.borderRadius",
548
+ "transform.crop.x",
549
+ "transform.crop.y",
550
+ "transform.crop.width",
551
+ "transform.crop.height",
552
+ "audio.volume",
553
+ "audio.pan",
554
+ "colorGrade.temperature",
555
+ "colorGrade.tint"
556
+ ];
557
+ /** `effect.<effectId>.<param>` is also a valid keyframe property; see `isKeyframeProperty`. */
558
+ const KEYFRAME_PROPERTY_PATTERN = /^(effect\.[A-Za-z0-9_-]+\.[A-Za-z0-9_]+)$/;
559
+ function isKeyframeProperty(value) {
560
+ return KEYFRAME_PROPERTIES.includes(value) || KEYFRAME_PROPERTY_PATTERN.test(value);
561
+ }
562
+ const CAPTION_POSITIONS = [
563
+ "top",
564
+ "center",
565
+ "bottom"
566
+ ];
567
+ const TEXT_ALIGNS = [
568
+ "left",
569
+ "center",
570
+ "right",
571
+ "justify"
572
+ ];
573
+ const TEXT_VALIGNS = [
574
+ "top",
575
+ "middle",
576
+ "bottom"
577
+ ];
578
+ const TEXT_DECORATIONS = [
579
+ "none",
580
+ "underline",
581
+ "line-through",
582
+ "overline"
583
+ ];
584
+ const ANIMATION_UNITS = [
585
+ "character",
586
+ "word",
587
+ "line"
588
+ ];
589
+ const MASK_TYPES = ["shape", "drawn"];
590
+ const SVG_COLOR_MODES = [
591
+ "none",
592
+ "tint",
593
+ "replace"
594
+ ];
595
+ const AUTOMATION_LANES = ["volume", "pan"];
596
+ const TRACK_FLAGS = [
597
+ "locked",
598
+ "hidden",
599
+ "muted",
600
+ "solo"
601
+ ];
602
+ const zTrackLayout = z.enum(TRACK_LAYOUTS);
603
+ const zTrackRole = z.enum(TRACK_ROLES);
604
+ const zFitMode = z.enum(FIT_MODES);
605
+ const zBlendMode = z.enum(BLEND_MODES);
606
+ const zEasing = z.enum(EASINGS);
607
+ const zMaskEasing = z.enum(MASK_EASINGS);
608
+ const zTransitionType = z.enum(TRANSITION_TYPES);
609
+ const zVideoEffectType = z.enum(VIDEO_EFFECT_TYPES);
610
+ const zTextAnimationPreset = z.enum(TEXT_ANIMATION_PRESETS);
611
+ const zCaptionAnimation = z.enum(CAPTION_ANIMATIONS);
612
+ const zGraphicAnimation = z.enum(GRAPHIC_ANIMATIONS);
613
+ const zShapeType = z.enum(SHAPE_TYPES);
614
+ const zKeyframeProperty = z.string().refine(isKeyframeProperty, "unknown keyframe property");
903
615
  // src/jsx/schemas/common.ts
904
616
  const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
905
617
  const TIME_REF_PATTERN = /^(word|shot|silence):\d+(\.end)?$|^marker:[A-Za-z_][A-Za-z0-9_-]*$/;
@@ -936,7 +648,7 @@ const vec2Schema = z.strictObject({
936
648
  x: z.number(),
937
649
  y: z.number()
938
650
  });
939
- const vec3Schema = z.strictObject({
651
+ z.strictObject({
940
652
  x: z.number(),
941
653
  y: z.number(),
942
654
  z: z.number()
@@ -953,27 +665,13 @@ const transformSchema = z.strictObject({
953
665
  y: z.number().min(0).max(1),
954
666
  width: z.number().min(0).max(1),
955
667
  height: z.number().min(0).max(1)
956
- }).optional(),
957
- rotate3d: vec3Schema.optional(),
958
- perspective: z.number().min(100).max(5e3).optional(),
959
- transformStyle: z.enum(["flat", "preserve-3d"]).optional()
668
+ }).optional()
960
669
  });
961
670
  const visualProps = {
962
671
  transform: transformSchema.optional(),
963
672
  fit: zFitMode.optional(),
964
673
  blend: zBlendMode.optional(),
965
- blendOpacity: z.number().min(0).max(1).optional(),
966
- emphasis: z.union([zEmphasisAnimation, z.strictObject({
967
- type: zEmphasisAnimation,
968
- speed: z.number().positive().optional(),
969
- intensity: z.number().min(0).optional(),
970
- loop: z.boolean().optional(),
971
- focusPoint: vec2Schema.optional(),
972
- zoomScale: z.number().positive().optional(),
973
- holdDuration: z.number().nonnegative().optional(),
974
- startTime: z.number().nonnegative().optional(),
975
- animationDuration: z.number().positive().optional()
976
- })]).optional()
674
+ blendOpacity: z.number().min(0).max(1).optional()
977
675
  };
978
676
  const automationPointsSchema = z.array(z.tuple([z.number().nonnegative(), z.number()]));
979
677
  const audioProps = {
@@ -1025,6 +723,15 @@ const entranceProps = {
1025
723
  enter: graphicAnimationSchema.optional(),
1026
724
  exit: graphicAnimationSchema.optional()
1027
725
  };
726
+ // src/jsx/schemas/enums-local.ts
727
+ const zCaptionPosition = z.enum(CAPTION_POSITIONS);
728
+ const zTextAlign = z.enum(TEXT_ALIGNS);
729
+ const zTextValign = z.enum(TEXT_VALIGNS);
730
+ const zTextDecoration = z.enum(TEXT_DECORATIONS);
731
+ const zAnimationUnit = z.enum(ANIMATION_UNITS);
732
+ const zMaskType = z.enum(MASK_TYPES);
733
+ const zSvgColorMode = z.enum(SVG_COLOR_MODES);
734
+ z.enum(AUTOMATION_LANES);
1028
735
  // src/jsx/schemas/captions.ts
1029
736
  const captionStyleSchema = z.strictObject({
1030
737
  font: z.string().optional(),
@@ -1053,7 +760,9 @@ const captionsSchema = z.strictObject({
1053
760
  /** Left out: as many as fit one line at the caption size on this frame (at most 42). */
1054
761
  maxChars: z.number().int().positive().optional(),
1055
762
  maxDuration: z.number().positive().default(5),
1056
- maxGap: z.number().nonnegative().default(.8)
763
+ maxGap: z.number().nonnegative().default(.8),
764
+ /** Words a generated cue always ends after, as `word:N` refs on `src`: `breakAfter={["word:5", "word:12"]}`. */
765
+ breakAfter: z.array(z.string().regex(/^word:\d+$/, "a word ref such as \"word:12\"")).optional()
1057
766
  });
1058
767
  const captionSchema = z.strictObject({
1059
768
  id: idSchema,
@@ -1077,48 +786,13 @@ const paramValue = z.union([
1077
786
  const paramsSchema = z.record(z.string(), paramValue);
1078
787
  const effectSchema = z.strictObject({
1079
788
  id: idSchema,
1080
- type: zVideoEffectType,
1081
- params: paramsSchema.default({}),
1082
- enabled: z.boolean().default(true)
1083
- });
1084
- const audioEffectSchema = z.strictObject({
1085
- id: idSchema,
1086
- type: zAudioEffectType,
789
+ /** A built-in effect, or one the file declares with `defineEffect()`. */
790
+ type: z.union([zVideoEffectType, z.string().min(1)]),
1087
791
  params: paramsSchema.default({}),
1088
792
  enabled: z.boolean().default(true)
1089
793
  });
1090
- const rgb = z.strictObject({
1091
- r: z.number().min(-1).max(1),
1092
- g: z.number().min(-1).max(1),
1093
- b: z.number().min(-1).max(1)
1094
- });
1095
- const curve = z.array(vec2Schema).min(2);
1096
- const eightBands = z.array(z.number()).length(8);
1097
794
  const colorGradeSchema = z.strictObject({
1098
795
  id: idSchema,
1099
- wheels: z.strictObject({
1100
- shadows: rgb.optional(),
1101
- midtones: rgb.optional(),
1102
- highlights: rgb.optional(),
1103
- shadowsLift: z.number().min(-1).max(1).optional(),
1104
- midtonesGamma: z.number().min(.1).max(4).optional(),
1105
- highlightsGain: z.number().min(0).max(4).optional()
1106
- }).optional(),
1107
- curves: z.strictObject({
1108
- rgb: curve.optional(),
1109
- red: curve.optional(),
1110
- green: curve.optional(),
1111
- blue: curve.optional()
1112
- }).optional(),
1113
- lut: z.strictObject({
1114
- src: z.string(),
1115
- intensity: z.number().min(0).max(1).default(1)
1116
- }).optional(),
1117
- hsl: z.strictObject({
1118
- hue: eightBands.optional(),
1119
- saturation: eightBands.optional(),
1120
- luminance: eightBands.optional()
1121
- }).optional(),
1122
796
  temperature: z.number().min(-100).max(100).optional(),
1123
797
  tint: z.number().min(-100).max(100).optional()
1124
798
  });
@@ -1128,12 +802,7 @@ const keyframeSchema = z.strictObject({
1128
802
  /** Element-relative seconds. */
1129
803
  at: z.number().nonnegative(),
1130
804
  value: z.unknown(),
1131
- easing: zEasing.default("linear"),
1132
- handles: z.strictObject({
1133
- in: vec2Schema,
1134
- out: vec2Schema
1135
- }).optional(),
1136
- roving: z.boolean().optional()
805
+ easing: zEasing.default("linear")
1137
806
  });
1138
807
  const speedKeySchema = z.strictObject({
1139
808
  id: idSchema,
@@ -1182,11 +851,7 @@ const maskSchema = z.strictObject({
1182
851
  inverted: z.boolean().default(false),
1183
852
  expansion: z.number().min(-100).max(100).default(0),
1184
853
  opacity: z.number().min(0).max(1).default(1),
1185
- easing: zMaskEasing.default("linear"),
1186
- matte: z.strictObject({
1187
- source: idSchema,
1188
- channel: zMatteChannel.default("bounds")
1189
- }).optional()
854
+ easing: zMaskEasing.default("linear")
1190
855
  });
1191
856
  // src/theme/builtins.ts
1192
857
  /**
@@ -1617,7 +1282,16 @@ const compositionSchema = z.strictObject({
1617
1282
  gradient: gradientSchema.optional()
1618
1283
  }).optional(),
1619
1284
  /** The video's look: palette, fonts, radius, motion style and element presets. Elements use it through `$tokens` and `style`. */
1620
- theme: themeSchema.optional()
1285
+ theme: themeSchema.optional(),
1286
+ /**
1287
+ * A film camera's blur on everything that moves, in the export: each frame averages `samples` moments across the
1288
+ * time its shutter is open, `shutterAngle` degrees of the frame (180 is half the frame, the film default). Drawn by
1289
+ * the browser export, which a file with it uses on its own; the preview stays sharp.
1290
+ */
1291
+ motionBlur: z.strictObject({
1292
+ shutterAngle: z.number().min(1).max(360).default(180),
1293
+ samples: z.number().int().min(2).max(64).default(8)
1294
+ }).optional()
1621
1295
  });
1622
1296
  const trackSchema = z.strictObject({
1623
1297
  id: idSchema,
@@ -1641,26 +1315,12 @@ const gapSchema = z.strictObject({
1641
1315
  duration: z.number().positive()
1642
1316
  });
1643
1317
  // src/jsx/schemas/media.ts
1644
- const chromaKeySchema = z.strictObject({
1645
- enabled: z.boolean().default(true),
1646
- color: colorSchema.default("#00ff00"),
1647
- tolerance: z.number().min(0).max(1).default(.3),
1648
- edgeSoftness: z.number().min(0).max(1).default(.1),
1649
- spillSuppression: z.number().min(0).max(1).default(.5)
1650
- });
1651
- const stabilizeSchema = z.strictObject({
1652
- enabled: z.boolean().default(true),
1653
- strength: z.number().min(0).max(1).default(.5),
1654
- crop: zStabilizeCrop.default("auto")
1655
- });
1656
1318
  const sourceProps = {
1657
1319
  src: assetRefSchema,
1658
1320
  in: timeRefSchema.optional(),
1659
1321
  out: timeRefSchema.optional(),
1660
1322
  speed: z.number().min(.1).max(20).default(1),
1661
- reverse: z.boolean().default(false),
1662
- pitchCorrection: z.boolean().default(false),
1663
- audioTrack: z.number().int().nonnegative().default(0)
1323
+ reverse: z.boolean().default(false)
1664
1324
  };
1665
1325
  const clipSchema = z.strictObject({
1666
1326
  ...timedProps,
@@ -1675,12 +1335,7 @@ const clipSchema = z.strictObject({
1675
1335
  ]), z.strictObject({
1676
1336
  x: z.number().min(0).max(1),
1677
1337
  y: z.number().min(0).max(1).optional()
1678
- })]).default("center") }).optional(),
1679
- smoothSlowMo: z.boolean().default(false),
1680
- interpolation: zInterpolation.optional(),
1681
- stabilize: stabilizeSchema.optional(),
1682
- chromaKey: chromaKeySchema.optional(),
1683
- behindSubject: z.boolean().default(false)
1338
+ })]).default("center") }).optional()
1684
1339
  });
1685
1340
  /** `<Audio>` is `<Clip>` for audio-only assets: the same schema, no visual props accepted. */
1686
1341
  const audioSchema = z.strictObject({
@@ -1726,18 +1381,6 @@ const textAnimationSchema = z.strictObject({
1726
1381
  })
1727
1382
  ])).optional()
1728
1383
  });
1729
- const text3dSchema = z.strictObject({
1730
- enabled: z.boolean().default(true),
1731
- depth: z.number().nonnegative().default(10),
1732
- bevelThickness: z.number().nonnegative().optional(),
1733
- bevelSize: z.number().nonnegative().optional(),
1734
- bevelSegments: z.number().int().nonnegative().optional(),
1735
- frontColor: colorSchema.optional(),
1736
- sideColor: colorSchema.optional(),
1737
- metalness: z.number().min(0).max(1).optional(),
1738
- roughness: z.number().min(0).max(1).optional(),
1739
- material: z.enum(["basic", "physical"]).optional()
1740
- });
1741
1384
  const titleSchema = z.strictObject({
1742
1385
  ...timedProps,
1743
1386
  ...visualProps,
@@ -1763,9 +1406,7 @@ const titleSchema = z.strictObject({
1763
1406
  lineHeight: z.number().min(.1).max(10).default(1.2),
1764
1407
  letterSpacing: z.number().min(-100).max(100).default(0),
1765
1408
  decoration: zTextDecoration.optional(),
1766
- animation: textAnimationSchema.optional(),
1767
- text3d: text3dSchema.optional(),
1768
- behindSubject: z.boolean().default(false)
1409
+ animation: textAnimationSchema.optional()
1769
1410
  });
1770
1411
  const fillSchema = z.union([colorSchema, z.strictObject({
1771
1412
  type: z.enum([
@@ -1868,7 +1509,6 @@ const stickerSchema = z.strictObject({
1868
1509
  });
1869
1510
  const adjustmentSchema = z.strictObject({
1870
1511
  ...timedProps,
1871
- affects: z.union([z.literal("all"), z.array(idSchema).min(1)]).default("all"),
1872
1512
  opacity: z.number().min(0).max(1).default(1),
1873
1513
  blend: zBlendMode.default("normal"),
1874
1514
  enabled: z.boolean().default(true),
@@ -1889,6 +1529,41 @@ const layerSchema = z.strictObject({
1889
1529
  transform: transformSchema.optional(),
1890
1530
  blend: zBlendMode.optional()
1891
1531
  });
1532
+ const vec3Schema = z.tuple([
1533
+ z.number(),
1534
+ z.number(),
1535
+ z.number()
1536
+ ]);
1537
+ /**
1538
+ * `<Three>`: a Layer that holds a three.js scene. `setup` takes `{ scene, camera, renderer, THREE, width, height }`
1539
+ * once and may return `update(frame)`, called on every frame with the layer's time; JSX children are React Three
1540
+ * Fiber. The camera and background are literal props, so the inspector edits them.
1541
+ */
1542
+ const threeSchema = layerSchema.extend({
1543
+ engine: z.literal("three"),
1544
+ camera: z.strictObject({
1545
+ position: vec3Schema.optional(),
1546
+ lookAt: vec3Schema.optional(),
1547
+ fov: z.number().min(1).max(179).optional(),
1548
+ near: z.number().positive().optional(),
1549
+ far: z.number().positive().optional(),
1550
+ zoom: z.number().positive().optional(),
1551
+ orthographic: z.boolean().optional()
1552
+ }).optional(),
1553
+ background: colorSchema.optional(),
1554
+ setup: z.custom((v) => typeof v === "function", "setup must be a function").optional()
1555
+ });
1556
+ /** One step of a Motion sequence: a selector inside the tag, its keyframes, and options (`duration`, `at`, `ease`…). */
1557
+ const motionStepSchema = z.union([z.tuple([z.string(), z.record(z.string(), z.unknown())]), z.tuple([
1558
+ z.string(),
1559
+ z.record(z.string(), z.unknown()),
1560
+ z.record(z.string(), z.unknown())
1561
+ ])]);
1562
+ /** `<Motion>`: a Layer whose children are animated by a Motion sequence on the video's clock. */
1563
+ const motionSchema = layerSchema.extend({
1564
+ engine: z.literal("motion"),
1565
+ sequence: z.array(motionStepSchema).default([])
1566
+ });
1892
1567
  // src/jsx/schemas/transition.ts
1893
1568
  const dir4 = z.enum([
1894
1569
  "left",
@@ -1896,11 +1571,7 @@ const dir4 = z.enum([
1896
1571
  "up",
1897
1572
  "down"
1898
1573
  ]);
1899
- const center = vec2Schema.default({
1900
- x: .5,
1901
- y: .5
1902
- });
1903
- /** Per-type parameter schemas with OpenReel's engine defaults, so defaults validate. */
1574
+ /** Per-type parameter schemas, with defaults that validate. */
1904
1575
  const TRANSITION_PARAMS = {
1905
1576
  crossfade: z.strictObject({ curve: z.enum([
1906
1577
  "linear",
@@ -1910,80 +1581,25 @@ const TRANSITION_PARAMS = {
1910
1581
  "ease-in-out"
1911
1582
  ]).default("ease") }),
1912
1583
  dipToBlack: z.strictObject({ holdDuration: z.number().min(0).max(30).default(.1) }),
1913
- dipToWhite: z.strictObject({ holdDuration: z.number().min(0).max(30).default(.1) }),
1914
- wipe: z.strictObject({
1915
- direction: z.enum([
1916
- "left",
1917
- "right",
1918
- "up",
1919
- "down",
1920
- "diagonal"
1921
- ]).default("left"),
1922
- softness: z.number().min(0).max(1).default(0)
1923
- }),
1924
- slide: z.strictObject({
1925
- direction: dir4.default("left"),
1926
- pushOut: z.boolean().default(false)
1927
- }),
1928
- zoom: z.strictObject({
1929
- scale: z.number().min(.01).max(100).default(2),
1930
- center
1931
- }),
1932
- push: z.strictObject({ direction: dir4.default("left") }),
1933
- circleReveal: z.strictObject({ center }),
1934
- blur: z.strictObject({ intensity: z.number().min(0).max(10).default(1) }),
1935
- whipPan: z.strictObject({
1936
- direction: dir4.default("left"),
1937
- blurIntensity: z.number().min(0).max(100).default(1)
1938
- }),
1939
- radialWipe: z.strictObject({
1940
- startAngle: z.number().min(-360).max(360).default(-90),
1941
- clockwise: z.boolean().default(true)
1942
- }),
1943
- pixelate: z.strictObject({ maxPixelSize: z.number().min(1).max(1e3).default(48) }),
1944
- glitch: z.strictObject({
1945
- intensity: z.number().min(0).max(10).default(.08),
1946
- slices: z.number().int().min(1).max(100).default(12)
1947
- }),
1948
- blinds: z.strictObject({
1949
- count: z.number().int().min(1).max(100).default(8),
1950
- direction: z.enum(["vertical", "horizontal"]).default("vertical")
1951
- }),
1952
- diamondReveal: z.strictObject({ center }),
1953
- spin: z.strictObject({ rotations: z.number().min(-20).max(20).default(1) }),
1954
- flip: z.strictObject({ axis: z.enum(["horizontal", "vertical"]).default("horizontal") }),
1955
- splitReveal: z.strictObject({ orientation: z.enum(["horizontal", "vertical"]).default("horizontal") }),
1956
- flash: z.strictObject({ intensity: z.number().min(0).max(10).default(1) }),
1957
- filmBurn: z.strictObject({
1958
- intensity: z.number().min(0).max(10).default(1),
1959
- warmth: z.number().min(0).max(1).default(.75)
1960
- }),
1961
- mosaic: z.strictObject({
1962
- tiles: z.number().int().min(1).max(100).default(8),
1963
- randomness: z.number().min(0).max(1).default(.85)
1964
- }),
1965
- ripple: z.strictObject({
1966
- amplitude: z.number().min(0).max(1).default(.04),
1967
- waves: z.number().min(0).max(100).default(3)
1968
- }),
1969
- pageTurn: z.strictObject({
1970
- direction: z.enum(["left", "right"]).default("left"),
1971
- shadow: z.number().min(0).max(1).default(.55)
1584
+ dipToWhite: z.strictObject({ holdDuration: z.number().min(0).max(30).default(.1) }),
1585
+ wipe: z.strictObject({ direction: dir4.default("left") }),
1586
+ slide: z.strictObject({
1587
+ direction: dir4.default("left"),
1588
+ pushOut: z.boolean().default(false)
1972
1589
  }),
1973
- colorSplit: z.strictObject({
1974
- maxOffset: z.number().min(0).max(1e3).default(18),
1975
- angle: z.number().min(-360).max(360).default(0)
1976
- })
1590
+ push: z.strictObject({ direction: dir4.default("left") })
1977
1591
  };
1978
1592
  const transitionSchema = z.strictObject({
1979
1593
  id: idSchema,
1980
- type: zTransitionType.default("crossfade"),
1594
+ /** A built-in transition, or one the file declares with `defineTransition()`. */
1595
+ type: z.union([zTransitionType, z.string().min(1)]).default("crossfade"),
1981
1596
  duration: z.number().min(.001).max(30).default(.5),
1982
1597
  params: z.record(z.string(), z.unknown()).default({}),
1983
1598
  /** Edge form: a transition at one edge of a single clip. Omit for a transition between two siblings. */
1984
1599
  on: idSchema.optional(),
1985
1600
  edge: z.enum(["in", "out"]).optional()
1986
1601
  }).refine((v) => v.on === void 0 === (v.edge === void 0), { message: "on and edge go together" }).superRefine((v, ctx) => {
1602
+ if (!isBuiltInTransition(v.type)) return;
1987
1603
  const result = TRANSITION_PARAMS[v.type].safeParse(v.params);
1988
1604
  if (!result.success) ctx.addIssue({
1989
1605
  code: "custom",
@@ -1991,6 +1607,9 @@ const transitionSchema = z.strictObject({
1991
1607
  message: `invalid params for ${v.type}: ${result.error.message}`
1992
1608
  });
1993
1609
  });
1610
+ function isBuiltInTransition(type) {
1611
+ return TRANSITION_TYPES.includes(type);
1612
+ }
1994
1613
  function defaultTransitionParams(type) {
1995
1614
  return TRANSITION_PARAMS[type].parse({});
1996
1615
  }
@@ -2010,6 +1629,10 @@ const Title = el("title", titleSchema);
2010
1629
  const Shape = el("shape", shapeSchema);
2011
1630
  const Svg = el("svg", svgSchema);
2012
1631
  const Sticker = el("sticker", stickerSchema);
1632
+ /**
1633
+ * @deprecated `<Graphic kind>` will be removed. Draw it with `<Motion>`, `<Three>` or a `<Layer>`; the showcase examples
1634
+ * show each (decision 0020).
1635
+ */
2013
1636
  const Graphic = el("graphic", graphicSchema);
2014
1637
  const Captions = el("captions", captionsSchema);
2015
1638
  const Caption = el("caption", captionSchema);
@@ -2021,8 +1644,41 @@ const Multicam = el("multicam", multicamSchema);
2021
1644
  const Angle = el("angle", angleSchema);
2022
1645
  const Cut = el("cut", cutSchema);
2023
1646
  const Layer = el("layer", layerSchema);
1647
+ /**
1648
+ * A Layer drawn by a library: kind `layer` to the resolver, the timeline and the editor, with props of its own and
1649
+ * an `engine` the Player looks up among the installed layer engines (`installLayerEngine`).
1650
+ */
1651
+ const engineLayer = (engine, schema) => {
1652
+ const component = defineElement({
1653
+ kind: "layer",
1654
+ schema,
1655
+ capabilities: capabilitiesOf("layer")
1656
+ });
1657
+ Object.defineProperty(component, "engine", {
1658
+ value: engine,
1659
+ enumerable: true
1660
+ });
1661
+ return component;
1662
+ };
1663
+ /** A three.js scene on the timeline: `setup` for plain three.js, or React Three Fiber children. */
1664
+ const Three = engineLayer("three", threeSchema.omit({ engine: true }));
1665
+ /** Children animated by a Motion `sequence` on the video's clock. */
1666
+ const Motion = engineLayer("motion", motionSchema.omit({ engine: true }));
1667
+ /** The engine Layers by tag, and the schema each is checked against once collected (with its `engine`). */
1668
+ const ENGINE_LAYERS = {
1669
+ Three,
1670
+ Motion
1671
+ };
1672
+ const ENGINE_SCHEMAS = {
1673
+ three: threeSchema,
1674
+ motion: motionSchema
1675
+ };
1676
+ /** The engine a vocabulary component draws with, if it is an engine Layer. */
1677
+ function engineOf(type) {
1678
+ const engine = type?.engine;
1679
+ return typeof engine === "string" ? engine : null;
1680
+ }
2024
1681
  const Effect = el("effect", effectSchema);
2025
- const AudioEffect = el("audioEffect", audioEffectSchema);
2026
1682
  const ColorGrade = el("colorGrade", colorGradeSchema);
2027
1683
  const Keyframe = el("keyframe", keyframeSchema);
2028
1684
  const SpeedKey = el("speedKey", speedKeySchema);
@@ -2050,277 +1706,186 @@ const ELEMENTS = {
2050
1706
  Cut,
2051
1707
  Layer,
2052
1708
  Effect,
2053
- AudioEffect,
2054
1709
  ColorGrade,
2055
1710
  Keyframe,
2056
1711
  SpeedKey,
2057
1712
  Freeze
2058
1713
  };
2059
1714
  const SCHEMAS_BY_KIND = Object.fromEntries(Object.values(ELEMENTS).map((component) => [component.kind, component.schema]));
2060
- // src/jsx/use-t.ts
2061
- /**
2062
- * The installed clock lives on `globalThis`, not in this module: a cut file loaded by a tool may carry its own
2063
- * copy of the kit (a private module graph, a second install), and its `useT()` must still find the player's clock.
2064
- */
2065
- const slot = globalThis[Symbol.for("@cutcraft/kit/layer-clock")] ??= { impl: null };
2066
- /** The layer's clock: progress, seconds elapsed and remaining, its duration, the composition time. */
2067
- function useLayer() {
2068
- const impl = slot.impl;
2069
- if (!impl) throw new Error("useLayer() needs a Player. The kit does not render; install one with installLayerClock().");
2070
- return impl();
2071
- }
2072
- /** Normalised time (0..1) inside a `<Layer>`; `useLayer().t`. */
2073
- function useT() {
2074
- const impl = slot.impl;
2075
- if (!impl) throw new Error("useT() needs a Player. The kit does not render; install one with installLayerClock().");
2076
- return impl().t;
2077
- }
2078
- function installLayerClock(fn) {
2079
- slot.impl = fn;
2080
- }
2081
- // src/resolve/speed.ts
2082
- /** CSS cubic-bezier(x1, y1, x2, y2) solved for y at a given x by bisection. */
2083
- function bezier(x1, y1, x2, y2) {
2084
- const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
2085
- const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
2086
- const sampleX = (u) => ((ax * u + bx) * u + cx) * u;
2087
- const sampleY = (u) => ((ay * u + by) * u + cy) * u;
2088
- return (x) => {
2089
- if (x <= 0) return 0;
2090
- if (x >= 1) return 1;
2091
- let lo = 0, hi = 1, u = x;
2092
- for (let i = 0; i < 24; i++) {
2093
- const sx = sampleX(u);
2094
- if (Math.abs(sx - x) < 1e-6) break;
2095
- if (sx < x) lo = u;
2096
- else hi = u;
2097
- u = (lo + hi) / 2;
2098
- }
2099
- return sampleY(u);
2100
- };
2101
- }
2102
- const inOut = (f) => (x) => x < .5 ? f(2 * x) / 2 : 1 - f(2 - 2 * x) / 2;
2103
- const out = (f) => (x) => 1 - f(1 - x);
2104
- const pow = (n) => (x) => Math.pow(x, n);
2105
- const sineIn = (x) => 1 - Math.cos(x * Math.PI / 2);
2106
- const expoIn = (x) => x === 0 ? 0 : Math.pow(2, 10 * x - 10);
2107
- const circIn = (x) => 1 - Math.sqrt(1 - x * x);
2108
- const backIn = (s) => (x) => (s + 1) * x * x * x - s * x * x;
2109
- const elasticIn = (p) => (x) => x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * (2 * Math.PI) / p);
2110
- const bounceFull = (x) => {
2111
- const n1 = 7.5625, d1 = 2.75;
2112
- if (x < 1 / d1) return n1 * x * x;
2113
- if (x < 2 / d1) {
2114
- x -= 1.5 / d1;
2115
- return n1 * x * x + .75;
2116
- }
2117
- if (x < 2.5 / d1) {
2118
- x -= 2.25 / d1;
2119
- return n1 * x * x + .9375;
2120
- }
2121
- x -= 2.625 / d1;
2122
- return n1 * x * x + .984375;
1715
+ // src/jsx/shaders.ts
1716
+ const STORE_KEY$1 = Symbol.for("cutcraft.shaders.store");
1717
+ const store$1 = globalThis[STORE_KEY$1] ??= {
1718
+ scope: null,
1719
+ global: /* @__PURE__ */ new Map()
2123
1720
  };
2124
- /** A bounce blended toward linear by `1 - h`: `h = 1` is the full bounce, smaller values damp it. */
2125
- const bounceOut = (h) => (x) => x + (bounceFull(x) - x) * h;
1721
+ /** The shaders a module loaded by `loadModule` declared, stored on its exports under this key. */
1722
+ const MODULE_SHADERS = Symbol.for("cutcraft.shaders");
1723
+ const NAME = /^[a-z][a-zA-Z0-9-]*$/;
1724
+ /** Names a param cannot take: the inputs every shader gets, and GLSL ES 3.00's built-in functions, which it would hide. */
1725
+ const RESERVED = /* @__PURE__ */ new Set([
1726
+ ...[
1727
+ "image",
1728
+ "from",
1729
+ "to",
1730
+ "resolution",
1731
+ "time",
1732
+ "progress",
1733
+ "position",
1734
+ "uv",
1735
+ "main"
1736
+ ],
1737
+ ...[
1738
+ "radians",
1739
+ "degrees",
1740
+ "sin",
1741
+ "cos",
1742
+ "tan",
1743
+ "asin",
1744
+ "acos",
1745
+ "atan",
1746
+ "sinh",
1747
+ "cosh",
1748
+ "tanh",
1749
+ "asinh",
1750
+ "acosh"
1751
+ ],
1752
+ ...[
1753
+ "atanh",
1754
+ "pow",
1755
+ "exp",
1756
+ "log",
1757
+ "exp2",
1758
+ "log2",
1759
+ "sqrt",
1760
+ "inversesqrt",
1761
+ "abs",
1762
+ "sign",
1763
+ "floor",
1764
+ "trunc",
1765
+ "round"
1766
+ ],
1767
+ ...[
1768
+ "roundEven",
1769
+ "ceil",
1770
+ "fract",
1771
+ "mod",
1772
+ "modf",
1773
+ "min",
1774
+ "max",
1775
+ "clamp",
1776
+ "mix",
1777
+ "step",
1778
+ "smoothstep",
1779
+ "isnan"
1780
+ ],
1781
+ ...[
1782
+ "isinf",
1783
+ "length",
1784
+ "distance",
1785
+ "dot",
1786
+ "cross",
1787
+ "normalize",
1788
+ "faceforward",
1789
+ "reflect",
1790
+ "refract"
1791
+ ],
1792
+ ...[
1793
+ "matrixCompMult",
1794
+ "outerProduct",
1795
+ "transpose",
1796
+ "determinant",
1797
+ "inverse",
1798
+ "lessThan",
1799
+ "lessThanEqual"
1800
+ ],
1801
+ ...[
1802
+ "greaterThan",
1803
+ "greaterThanEqual",
1804
+ "equal",
1805
+ "notEqual",
1806
+ "any",
1807
+ "all",
1808
+ "not",
1809
+ "texture",
1810
+ "textureSize"
1811
+ ],
1812
+ ...[
1813
+ "textureLod",
1814
+ "texelFetch",
1815
+ "dFdx",
1816
+ "dFdy",
1817
+ "fwidth"
1818
+ ]
1819
+ ]);
1820
+ function declare$1(kind, options) {
1821
+ const { name, shader } = options;
1822
+ const builtIn = kind === "effect" ? VIDEO_EFFECT_TYPES : TRANSITION_TYPES;
1823
+ if (!NAME.test(name)) throw new Error(`${kind} "${name}": a name is lowercase letters, digits and dashes`);
1824
+ if (builtIn.includes(name)) throw new Error(`${kind} "${name}" is built in; give yours another name`);
1825
+ const fn = kind === "effect" ? "effect" : "transition";
1826
+ if (!new RegExp(`vec4\\s+${fn}\\s*\\(\\s*vec2\\s+\\w+\\s*\\)`).test(shader)) throw new Error(`${kind} "${name}": the shader defines vec4 ${fn}(vec2 uv)`);
1827
+ for (const param of Object.keys(options.params?.shape ?? {})) if (RESERVED.has(param)) throw new Error(`${kind} "${name}": param "${param}" is a name every shader already uses (a GLSL function or input); call it something else`);
1828
+ (store$1.scope ?? store$1.global).set(`${kind}:${name}`, {
1829
+ kind,
1830
+ name,
1831
+ params: options.params ?? z.object({}),
1832
+ shader
1833
+ });
1834
+ return name;
1835
+ }
2126
1836
  /**
2127
- * Where a spring released at rest from `from` toward `to` is after `time` seconds: the closed-form
2128
- * damped oscillator, under-, critically or over-damped. Defaults are mass 1, damping 10, stiffness 100.
1837
+ * Declares an effect drawn by a GLSL function, and returns its name for `<Effect type>`. Call it at the top of the file.
1838
+ *
1839
+ * const glitch = defineEffect({
1840
+ * name: "glitch",
1841
+ * params: z.object({ amount: z.number().min(0).max(1).default(0.3) }),
1842
+ * shader: `vec4 effect(vec2 uv) {
1843
+ * float shift = amount * 0.02 * sin(time * 40.0 + uv.y * 80.0);
1844
+ * return vec4(texture(image, uv + vec2(shift, 0)).r, texture(image, uv).g, texture(image, uv - vec2(shift, 0)).b,
1845
+ * texture(image, uv).a);
1846
+ * }`,
1847
+ * });
1848
+ * <Clip id="city" src="asset:city"><Effect id="g" type={glitch} params={{ amount: 0.5 }} /></Clip>
2129
1849
  */
2130
- function springAt(time, from, to, { mass = 1, damping = 10, stiffness = 100 } = {}) {
2131
- if (time <= 0) return from;
2132
- const w0 = Math.sqrt(stiffness / mass), zeta = damping / (2 * Math.sqrt(stiffness * mass)), x0 = from - to;
2133
- if (zeta < 1) {
2134
- const wd = w0 * Math.sqrt(1 - zeta * zeta);
2135
- return to + Math.exp(-zeta * w0 * time) * (x0 * Math.cos(wd * time) + zeta * w0 * x0 / wd * Math.sin(wd * time));
2136
- }
2137
- if (zeta === 1) return to + Math.exp(-w0 * time) * (x0 + w0 * x0 * time);
2138
- const r1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1)), r2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));
2139
- return to + -r2 * x0 / (r1 - r2) * Math.exp(r1 * time) + r1 * x0 / (r1 - r2) * Math.exp(r2 * time);
2140
- }
2141
- /** The default spring squeezed into 0..1: it settles (to about 0.1%) at x = 1, overshooting about 16% on the way. */
2142
- const SPRING_SETTLE = Math.log(1e3) / 5;
2143
- const springEase = (x) => x >= 1 ? 1 : springAt(x * SPRING_SETTLE, 0, 1);
2144
- const quadIn = pow(2);
2145
- const cubicIn = pow(3);
2146
- const quartIn = pow(4);
2147
- const quintIn = pow(5);
2148
- const back = backIn(1.70158);
2149
- const backStrong = backIn(2.6);
2150
- const elastic = elasticIn(3);
2151
- const elasticSoft = elasticIn(4.5);
2152
- const bounce = bounceOut(1);
2153
- const bounceSmall = bounceOut(.5);
2154
- const EASING_FN = {
2155
- linear: (x) => x,
2156
- ease: bezier(.25, .1, .25, 1),
2157
- "ease-in": bezier(.42, 0, 1, 1),
2158
- "ease-out": bezier(0, 0, .58, 1),
2159
- "ease-in-out": bezier(.42, 0, .58, 1),
2160
- hold: (x) => x >= 1 ? 1 : 0,
2161
- bezier: bezier(.25, .1, .25, 1),
2162
- smoothstep: (x) => x * x * (3 - 2 * x),
2163
- smootherstep: (x) => x * x * x * (x * (x * 6 - 15) + 10),
2164
- snappy: bezier(.2, .9, .3, 1),
2165
- smooth: inOut(sineIn),
2166
- spring: springEase,
2167
- easeInQuad: quadIn,
2168
- easeOutQuad: out(quadIn),
2169
- easeInOutQuad: inOut(quadIn),
2170
- easeInCubic: cubicIn,
2171
- easeOutCubic: out(cubicIn),
2172
- easeInOutCubic: inOut(cubicIn),
2173
- easeInQuart: quartIn,
2174
- easeOutQuart: out(quartIn),
2175
- easeInOutQuart: inOut(quartIn),
2176
- easeInQuint: quintIn,
2177
- easeOutQuint: out(quintIn),
2178
- easeInOutQuint: inOut(quintIn),
2179
- easeInSine: sineIn,
2180
- easeOutSine: out(sineIn),
2181
- easeInOutSine: inOut(sineIn),
2182
- easeInExpo: expoIn,
2183
- easeOutExpo: out(expoIn),
2184
- easeInOutExpo: inOut(expoIn),
2185
- easeInCirc: circIn,
2186
- easeOutCirc: out(circIn),
2187
- easeInOutCirc: inOut(circIn),
2188
- easeInBack: back,
2189
- easeOutBack: out(back),
2190
- easeInOutBack: inOut(back),
2191
- easeInBackStrong: backStrong,
2192
- easeOutBackStrong: out(backStrong),
2193
- easeInOutBackStrong: inOut(backStrong),
2194
- easeInElastic: elastic,
2195
- easeOutElastic: out(elastic),
2196
- easeInOutElastic: inOut(elastic),
2197
- easeInElasticSoft: elasticSoft,
2198
- easeOutElasticSoft: out(elasticSoft),
2199
- easeInOutElasticSoft: inOut(elasticSoft),
2200
- easeInBounce: out(bounce),
2201
- easeOutBounce: bounce,
2202
- easeInOutBounce: inOut(out(bounce)),
2203
- easeInBounceSmall: out(bounceSmall),
2204
- easeOutBounceSmall: bounceSmall,
2205
- easeInOutBounceSmall: inOut(out(bounceSmall))
2206
- };
2207
- const INTEGRATION_STEPS = 64;
2208
- /** Speed at clip time `u` under keys sorted by `at`: holds outside the keys, eases between them. */
2209
- function rampSpeed(keys, u) {
2210
- const first = keys[0];
2211
- const last = keys[keys.length - 1];
2212
- if (u <= first.at) return first.speed;
2213
- if (u >= last.at) return last.speed;
2214
- for (let i = 0; i < keys.length - 1; i++) {
2215
- const a = keys[i], b = keys[i + 1];
2216
- if (u < a.at || u >= b.at) continue;
2217
- const span = b.at - a.at;
2218
- const fn = EASING_FN[a.easing] ?? EASING_FN.linear;
2219
- const k = span <= 0 ? 1 : fn(Math.min(1, Math.max(0, (u - a.at) / span)));
2220
- return a.speed + (b.speed - a.speed) * k;
2221
- }
2222
- return last.speed;
2223
- }
2224
- /** Source seconds played in `rel` clip seconds: ∫₀ʳᵉˡ speed by the trapezium rule, a fixed step count per key segment. */
2225
- function rampAdvance(keys, rel) {
2226
- if (rel <= 0) return 0;
2227
- const bounds = [
2228
- 0,
2229
- ...keys.map((k) => k.at).filter((at) => at > 0 && at < rel),
2230
- rel
2231
- ];
2232
- let total = 0;
2233
- for (let i = 0; i < bounds.length - 1; i++) {
2234
- const a = bounds[i], b = bounds[i + 1];
2235
- if (b <= a) continue;
2236
- const h = (b - a) / INTEGRATION_STEPS;
2237
- let sum = (rampSpeed(keys, a) + rampSpeed(keys, b)) / 2;
2238
- for (let s = 1; s < INTEGRATION_STEPS; s++) sum += rampSpeed(keys, a + s * h);
2239
- total += sum * h;
2240
- }
2241
- return total;
2242
- }
2243
- /** Clip seconds that play `advance` source seconds: the inverse of `rampAdvance`, by bisection (speed ≥ 0.1, so it is monotonic). */
2244
- function rampInverse(keys, advance) {
2245
- if (advance <= 0) return 0;
2246
- let lo = 0, hi = advance / .1;
2247
- for (let i = 0; i < 60 && hi - lo > 1e-9; i++) {
2248
- const mid = (lo + hi) / 2;
2249
- if (rampAdvance(keys, mid) < advance) lo = mid;
2250
- else hi = mid;
2251
- }
2252
- return (lo + hi) / 2;
2253
- }
2254
- /** Element-relative `rel` with the holds before it taken out, or the held source second when `rel` is inside a hold. Holds sorted by `start`. */
2255
- function freezeShift(holds, rel) {
2256
- let shift = 0;
2257
- for (const h of holds) {
2258
- if (rel < h.start) break;
2259
- if (rel < h.start + h.duration) return { held: h.at };
2260
- shift += h.duration;
2261
- }
2262
- return { rel: rel - shift };
2263
- }
2264
- /** Element-relative time of clip time `u`: the inverse of `freezeShift` outside the holds. A time at a hold's start plays before it. */
2265
- function freezeUnshift(holds, u) {
2266
- let rel = u;
2267
- for (const h of holds) if (h.start < rel - 1e-9) rel += h.duration;
2268
- return rel;
2269
- }
2270
- /** Total seconds the holds add to a clip. */
2271
- function holdTotal(holds) {
2272
- return holds.reduce((n, h) => n + h.duration, 0);
2273
- }
2274
- /** A clip's `<SpeedKey>` children, sorted by `at`. */
2275
- function speedKeysOf(children) {
2276
- return children.filter((c) => c.kind === "speedKey").map((c) => ({
2277
- at: Number(c.props.at ?? 0),
2278
- speed: Number(c.props.speed ?? 1),
2279
- easing: c.props.easing
2280
- })).sort((a, b) => a.at - b.at);
2281
- }
2282
- /** A clip's `<Freeze>` children, sorted by `start`. */
2283
- function freezeHoldsOf(children) {
2284
- return children.filter((c) => c.kind === "freeze").map((c) => ({
2285
- at: Number(c.props.at ?? 0),
2286
- start: Number(c.props.start ?? 0),
2287
- duration: Number(c.props.duration ?? 0)
2288
- })).sort((a, b) => a.start - b.start);
1850
+ function defineEffect(options) {
1851
+ return declare$1("effect", options);
2289
1852
  }
2290
- // src/jsx/animate.ts
2291
- /**
2292
- * Maps `input` from `inputRange` onto `outputRange`, piecewise: `interpolate(elapsed, [0, 0.5, 2], [0, 1, 0.8])`.
2293
- * The input range must increase. Beyond the ends the value extends linearly, unless clamped; easing applies only inside.
1853
+ /**
1854
+ * Declares a transition drawn by a GLSL function of the two pictures, and returns its name for `<Transition type>`.
1855
+ *
1856
+ * const sweep = defineTransition({
1857
+ * name: "sweep",
1858
+ * shader: `vec4 transition(vec2 uv) { return uv.x < progress ? texture(to, uv) : texture(from, uv); }`,
1859
+ * });
2294
1860
  */
2295
- function interpolate(input, inputRange, outputRange, options = {}) {
2296
- if (inputRange.length < 2 || inputRange.length !== outputRange.length) throw new Error("interpolate() wants input and output ranges of the same length, at least two");
2297
- for (let i = 1; i < inputRange.length; i++) if (!(inputRange[i] > inputRange[i - 1])) throw new Error(`interpolate() wants an increasing input range, got [${inputRange.join(", ")}]`);
2298
- let i = 1;
2299
- while (i < inputRange.length - 1 && input > inputRange[i]) i++;
2300
- const a = inputRange[i - 1], b = inputRange[i];
2301
- let x = (input - a) / (b - a);
2302
- if (x < 0 && options.extrapolateLeft === "clamp") x = 0;
2303
- if (x > 1 && options.extrapolateRight === "clamp") x = 1;
2304
- const k = x >= 0 && x <= 1 && options.easing ? EASING_FN[options.easing](x) : x;
2305
- const lo = outputRange[i - 1];
2306
- return lo + (outputRange[i] - lo) * k;
1861
+ function defineTransition(options) {
1862
+ return declare$1("transition", options);
2307
1863
  }
2308
- /** A physical spring from `from` (default 0) to `to` (default 1) at `time` seconds: mass 1, damping 10, stiffness 100 unless given. */
2309
- function spring({ time, from = 0, to = 1, overshootClamping = false, ...config }) {
2310
- const v = springAt(time, from, to, config);
2311
- return overshootClamping ? to >= from ? Math.min(v, to) : Math.max(v, to) : v;
1864
+ /** Runs `load` and returns the shaders declared while it ran: how `loadModule` keeps each file's apart. */
1865
+ function collectShaders(load) {
1866
+ const outer = store$1.scope;
1867
+ const scope = /* @__PURE__ */ new Map();
1868
+ store$1.scope = scope;
1869
+ try {
1870
+ load();
1871
+ } finally {
1872
+ store$1.scope = outer;
1873
+ }
1874
+ return [...scope.values()];
2312
1875
  }
2313
- /** A number in [0, 1) fixed by `seed`: the same seed gives the same number every frame, in the preview and the export. */
2314
- function random$1(seed) {
2315
- return random(hash(String(seed)))();
1876
+ /** The shaders a module declared: those `loadModule` recorded for it, or else every one declared outside a load. */
1877
+ function shadersOf(mod) {
1878
+ return mod[MODULE_SHADERS] ?? [...store$1.global.values()];
2316
1879
  }
2317
- // src/manifest/types.ts
2318
- const EMPTY_MANIFEST = { assets: {} };
2319
- function assetIdOf(ref) {
2320
- return ref.startsWith("asset:") ? ref.slice(6) : ref;
1880
+ /** The shader a file declares under this name for an `<Effect>` or `<Transition>`, if any. */
1881
+ function shaderFor(shaders, kind, type) {
1882
+ return shaders.find((s) => s.kind === kind && s.name === type);
2321
1883
  }
2322
- function lookupAsset(manifest, ref) {
2323
- return manifest.assets[assetIdOf(ref)] ?? null;
1884
+ /** Whether a file draws anything with a shader it declares: what only a renderer with WebGL draws. */
1885
+ function usesShaders(state) {
1886
+ if (state.shaders.length === 0) return false;
1887
+ for (const p of state.items.values()) if ((p.kind === "effect" || p.kind === "transition") && shaderFor(state.shaders, p.kind, p.props.type)) return true;
1888
+ return false;
2324
1889
  }
2325
1890
  // src/manifest/timeref.ts
2326
1891
  const REF = /^(word|shot|silence):(\d+)(\.end)?$/;
@@ -2385,347 +1950,432 @@ function resolveTimeRef(ref, asset, markers, ctx) {
2385
1950
  }
2386
1951
  }
2387
1952
  }
2388
- // src/resolve/tree.ts
2389
- /** Prop keys under this prefix address a Layer's handles rather than the Layer's own props. */
2390
- const INTERIOR_PREFIX = "interior.";
2391
- function walk(node, visit, depth = 0) {
2392
- visit(node, depth);
2393
- for (const child of node.children) walk(child, visit, depth + 1);
1953
+ // src/manifest/types.ts
1954
+ const EMPTY_MANIFEST = { assets: {} };
1955
+ function assetIdOf(ref) {
1956
+ return ref.startsWith("asset:") ? ref.slice(6) : ref;
2394
1957
  }
2395
- function findParentOf(tree, id) {
2396
- const node = tree.byId.get(id);
2397
- if (!node || node.parent === null) return null;
2398
- return tree.byId.get(node.parent) ?? null;
1958
+ function lookupAsset(manifest, ref) {
1959
+ return manifest.assets[assetIdOf(ref)] ?? null;
1960
+ }
1961
+ // src/theme/theme.ts
1962
+ /** Kinds whose entrance and exit a motion style fills. */
1963
+ const MOTION_KINDS = /* @__PURE__ */ new Set([
1964
+ "title",
1965
+ "shape",
1966
+ "svg",
1967
+ "sticker",
1968
+ "graphic"
1969
+ ]);
1970
+ /** Props whose strings are content, never tokens. */
1971
+ const CONTENT_PROPS = /* @__PURE__ */ new Set([
1972
+ "text",
1973
+ "content",
1974
+ "src",
1975
+ "label",
1976
+ "name",
1977
+ "category",
1978
+ "theme"
1979
+ ]);
1980
+ const byInput = /* @__PURE__ */ new WeakMap();
1981
+ let undeclared = null;
1982
+ /** The theme a composition's raw `theme` prop describes. Memoised per object; an invalid theme falls back to its base (the schema reports why). */
1983
+ function resolveTheme(raw) {
1984
+ if (raw === void 0 || raw === null || typeof raw !== "object") return undeclared ??= build(void 0, false);
1985
+ const hit = byInput.get(raw);
1986
+ if (hit) return hit;
1987
+ const parsed = themeSchema.safeParse(raw);
1988
+ const theme = build(parsed.success ? parsed.data : baseOnly(raw), true);
1989
+ byInput.set(raw, theme);
1990
+ return theme;
1991
+ }
1992
+ /** The value of `$name`, or undefined. */
1993
+ function themeToken(theme, name) {
1994
+ return Object.hasOwn(theme.tokens, name) ? theme.tokens[name] : void 0;
2399
1995
  }
2400
- // src/resolve/collect.ts
2401
- const NO_BINDINGS = Object.freeze({});
2402
1996
  /**
2403
- * Walk the element tree the file's default export returned and index every
2404
- * vocabulary element by id. Pure helper components are expanded by calling
2405
- * them; a `<Layer>` interior is not entered.
1997
+ * An element's raw props with its theme applied, before the schema parses them:
1998
+ * the preset its `style` names (or the kind's `default` preset) fills the props
1999
+ * the element leaves out, the motion style fills `enter`/`exit` (a transition's
2000
+ * type and duration), and whole-string `$token` values become theme values. The
2001
+ * element's own props always win. Returns the input object when nothing was themed.
2406
2002
  */
2407
- function collect(element, options = {}) {
2408
- const walker = {
2409
- byId: /* @__PURE__ */ new Map(),
2410
- errors: [],
2411
- index: options.index,
2412
- helperUses: /* @__PURE__ */ new Map()
2003
+ function applyTheme(kind, raw, theme) {
2004
+ const named = typeof raw.style === "string" ? raw.style : null;
2005
+ if (!theme.declared && named === null && !hasToken(raw)) return {
2006
+ props: raw,
2007
+ from: EMPTY,
2008
+ issues: []
2413
2009
  };
2414
- const roots = collectChildren(element, null, walker, null);
2415
- const root = roots.find((n) => n.kind === "composition") ?? roots[0];
2416
- if (!root) {
2417
- const empty = {
2418
- id: "composition",
2419
- kind: "composition",
2420
- props: {},
2421
- children: [],
2422
- parent: null,
2423
- editable: "literal",
2424
- lockedProps: [],
2425
- boundProps: {}
2010
+ const out = { ...raw };
2011
+ const from = {};
2012
+ const issues = [];
2013
+ const presets = theme.styles[kind];
2014
+ const presetName = named ?? (theme.declared ? "default" : null);
2015
+ if (presetName !== null && kind !== "composition") {
2016
+ const preset = presets?.[presetName];
2017
+ if (preset) {
2018
+ if (kind === "captions") {
2019
+ const own = typeof raw.style === "object" && raw.style !== null ? raw.style : {};
2020
+ out.style = {
2021
+ ...preset,
2022
+ ...own
2023
+ };
2024
+ from.style = `style:${presetName}`;
2025
+ } else for (const [key, value] of Object.entries(preset)) if (raw[key] === void 0) {
2026
+ out[key] = value;
2027
+ from[key] = `style:${presetName}`;
2028
+ }
2029
+ } else if (named !== null) {
2030
+ const known = Object.keys(presets ?? {});
2031
+ issues.push({
2032
+ code: "UNKNOWN_STYLE",
2033
+ prop: "style",
2034
+ message: `no ${kind} style "${named}" in the theme${known.length ? ` (${known.join(", ")})` : ""}`
2035
+ });
2036
+ if (kind === "captions") delete out.style;
2037
+ }
2038
+ }
2039
+ const motion = theme.declared ? theme.motion : null;
2040
+ if (motion && MOTION_KINDS.has(kind) && !(kind === "title" && out.animation !== void 0)) {
2041
+ if (out.enter === void 0 && motion.enter) {
2042
+ out.enter = motion.enter;
2043
+ from.enter = "motion";
2044
+ }
2045
+ if (out.exit === void 0 && motion.exit) {
2046
+ out.exit = motion.exit;
2047
+ from.exit = "motion";
2048
+ }
2049
+ }
2050
+ if (motion && kind === "transition" && motion.transition) {
2051
+ if (out.type === void 0 && motion.transition.type) {
2052
+ out.type = motion.transition.type;
2053
+ from.type = "motion";
2054
+ }
2055
+ if (out.duration === void 0 && motion.transition.duration !== void 0) {
2056
+ out.duration = motion.transition.duration;
2057
+ from.duration = "motion";
2058
+ }
2059
+ }
2060
+ if (kind === "composition" && theme.declared && out.background === void 0) {
2061
+ out.background = {
2062
+ mode: "color",
2063
+ color: theme.palette.background
2426
2064
  };
2427
- walker.errors.push({
2428
- code: "SCHEMA",
2429
- id: "composition",
2430
- message: "the file must render a <Composition>"
2065
+ from.background = "theme";
2066
+ }
2067
+ for (const key of Object.keys(out)) {
2068
+ if (CONTENT_PROPS.has(key)) continue;
2069
+ out[key] = substitute(out[key], key, theme, from, issues);
2070
+ }
2071
+ if (Object.keys(from).length === 0 && issues.length === 0) return {
2072
+ props: raw,
2073
+ from: EMPTY,
2074
+ issues
2075
+ };
2076
+ return {
2077
+ props: out,
2078
+ from: Object.keys(from).length ? from : EMPTY,
2079
+ issues
2080
+ };
2081
+ }
2082
+ const EMPTY = Object.freeze({});
2083
+ function substitute(value, path, theme, from, issues) {
2084
+ if (typeof value === "string") {
2085
+ const m = TOKEN_PATTERN.exec(value);
2086
+ if (!m) return value;
2087
+ const name = m[1];
2088
+ const resolved = themeToken(theme, name);
2089
+ if (resolved === void 0) {
2090
+ issues.push({
2091
+ code: "UNKNOWN_TOKEN",
2092
+ prop: path,
2093
+ message: `unknown theme token ${value}`
2094
+ });
2095
+ return value;
2096
+ }
2097
+ from[path] = value;
2098
+ return resolved;
2099
+ }
2100
+ if (Array.isArray(value)) {
2101
+ let changed = null;
2102
+ value.forEach((v, i) => {
2103
+ const s = substitute(v, `${path}.${i}`, theme, from, issues);
2104
+ if (s !== v) (changed ??= [...value])[i] = s;
2431
2105
  });
2432
- return {
2433
- root: empty,
2434
- byId: walker.byId,
2435
- errors: walker.errors
2106
+ return changed ?? value;
2107
+ }
2108
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
2109
+ let changed = null;
2110
+ for (const [k, v] of Object.entries(value)) {
2111
+ const s = substitute(v, `${path}.${k}`, theme, from, issues);
2112
+ if (s !== v) (changed ??= { ...value })[k] = s;
2113
+ }
2114
+ return changed ?? value;
2115
+ }
2116
+ return value;
2117
+ }
2118
+ function hasToken(value) {
2119
+ if (typeof value === "string") return value.startsWith("$");
2120
+ if (Array.isArray(value)) return value.some(hasToken);
2121
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
2122
+ for (const [k, v] of Object.entries(value)) if (!CONTENT_PROPS.has(k) && hasToken(v)) return true;
2123
+ }
2124
+ return false;
2125
+ }
2126
+ /** For an invalid theme: keep its `extends` when that alone is valid, so the rest of the file still themes sensibly. */
2127
+ function baseOnly(raw) {
2128
+ const ext = raw.extends;
2129
+ const parsed = themeSchema.safeParse(typeof ext === "string" ? { extends: ext } : {});
2130
+ return parsed.success ? parsed.data : void 0;
2131
+ }
2132
+ function build(input, declared) {
2133
+ const base = input?.extends ?? "midnight";
2134
+ const def = THEMES[base];
2135
+ const palette = {
2136
+ ...def.palette,
2137
+ ...input?.palette
2138
+ };
2139
+ const fonts = {
2140
+ ...def.fonts,
2141
+ ...input?.fonts
2142
+ };
2143
+ const radius = input?.radius ?? def.radius;
2144
+ const m = input?.motion ?? def.motion;
2145
+ const style = typeof m === "string" ? MOTIONS[m] : m;
2146
+ const motion = style.enter || style.exit || style.transition ? style : null;
2147
+ const styles = {};
2148
+ for (const [kind, presets] of Object.entries(def.styles)) styles[kind] = { ...presets };
2149
+ for (const [kind, presets] of Object.entries(input?.styles ?? {})) {
2150
+ const merged = styles[kind] ??= {};
2151
+ for (const [name, preset] of Object.entries(presets ?? {})) merged[name] = {
2152
+ ...merged[name],
2153
+ ...preset
2436
2154
  };
2437
2155
  }
2438
- const multi = new Set([...walker.helperUses].filter(([, n]) => n > 1).map(([name]) => name));
2439
- if (multi.size > 0) {
2440
- for (const node of walker.byId.values()) if (node.origin && multi.has(node.origin.helper) && node.editable === "literal") node.editable = "generated";
2441
- }
2442
2156
  return {
2443
- root,
2444
- byId: walker.byId,
2445
- errors: walker.errors
2157
+ declared,
2158
+ base,
2159
+ palette,
2160
+ fonts,
2161
+ radius,
2162
+ motion,
2163
+ styles,
2164
+ tokens: {
2165
+ ...palette,
2166
+ ...fonts,
2167
+ radius
2168
+ }
2446
2169
  };
2447
2170
  }
2448
- function collectModule(mod, overrides = {}, options = {}) {
2449
- const element = mod.default({
2450
- ...mod.defaults ?? {},
2451
- ...overrides
2452
- });
2453
- if (!element) return collect(Fragment({ children: null }), options);
2454
- return collect(element, options);
2455
- }
2456
- function collectChildren(node, parent, walker, origin) {
2457
- const out = [];
2458
- Children.forEach(node, (child) => {
2459
- if (!isValidElement(child)) return;
2460
- const element = child;
2461
- const type = element.type;
2462
- if (type === Fragment) {
2463
- out.push(...collectChildren(element.props.children, parent, walker, origin));
2464
- return;
2465
- }
2466
- if (isVocabulary(type)) {
2467
- if (type.kind === "layer") {
2468
- const node = makeNode(element, type.kind, parent, walker, origin, [], true);
2469
- const interior = interiorOf(element.props.children);
2470
- if (interior) {
2471
- const withInterior = {
2472
- ...node,
2473
- interior
2474
- };
2475
- walker.byId.set(node.id, withInterior);
2476
- out.push(withInterior);
2477
- } else out.push(node);
2478
- return;
2479
- }
2480
- const placeholder = makeNode(element, type.kind, parent, walker, origin, [], false);
2481
- const children = collectChildren(element.props.children, placeholder.id, walker, origin);
2482
- const node = {
2483
- ...placeholder,
2484
- children
2485
- };
2486
- walker.byId.set(node.id, node);
2487
- out.push(node);
2488
- return;
2489
- }
2490
- if (typeof type === "function") {
2491
- const helper = type;
2492
- const name = helper.name || "anonymous";
2493
- const uses = (walker.helperUses.get(name) ?? 0) + 1;
2494
- walker.helperUses.set(name, uses);
2495
- let produced;
2496
- try {
2497
- produced = helper(element.props);
2498
- } catch (error) {
2499
- walker.errors.push({
2500
- code: "SCHEMA",
2501
- id: parent ?? "composition",
2502
- message: `helper ${name} threw while expanding: ${String(error)}`
2503
- });
2504
- return;
2505
- }
2506
- const childOrigin = origin ?? {
2507
- helper: name,
2508
- callerId: parent,
2509
- callIndex: uses - 1
2510
- };
2511
- out.push(...collectChildren(produced, parent, walker, childOrigin));
2512
- return;
2171
+ // src/resolve/speed.ts
2172
+ /** CSS cubic-bezier(x1, y1, x2, y2) solved for y at a given x by bisection. */
2173
+ function bezier(x1, y1, x2, y2) {
2174
+ const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
2175
+ const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
2176
+ const sampleX = (u) => ((ax * u + bx) * u + cx) * u;
2177
+ const sampleY = (u) => ((ay * u + by) * u + cy) * u;
2178
+ return (x) => {
2179
+ if (x <= 0) return 0;
2180
+ if (x >= 1) return 1;
2181
+ let lo = 0, hi = 1, u = x;
2182
+ for (let i = 0; i < 24; i++) {
2183
+ const sx = sampleX(u);
2184
+ if (Math.abs(sx - x) < 1e-6) break;
2185
+ if (sx < x) lo = u;
2186
+ else hi = u;
2187
+ u = (lo + hi) / 2;
2513
2188
  }
2514
- out.push(...collectChildren(element.props.children, parent, walker, origin));
2515
- });
2516
- return out;
2517
- }
2518
- function makeNode(element, kind, parent, walker, origin, children, opaqueInterior) {
2519
- const { children: rawChildren, ...props } = element.props;
2520
- const idValue = props.id ?? (kind === "composition" ? "composition" : void 0);
2521
- const id = typeof idValue === "string" && idValue.length > 0 ? idValue : `${kind}-${walker.byId.size + 1}`;
2522
- if (typeof idValue !== "string" || idValue.length === 0) walker.errors.push({
2523
- code: "SCHEMA",
2524
- id,
2525
- prop: "id",
2526
- message: `<${kind}> needs a literal id`
2527
- });
2528
- if (walker.byId.has(id)) walker.errors.push({
2529
- code: "DUPLICATE_ID",
2530
- id,
2531
- message: `id "${id}" is used more than once`
2532
- });
2533
- const text = textOf(rawChildren);
2534
- const indexed = walker.index?.elements.get(id);
2535
- let editable = "literal";
2536
- if (walker.index && (!indexed || indexed.generated)) editable = "generated";
2537
- const lockedProps = indexed?.lockedProps ?? [];
2538
- const boundProps = indexed?.boundProps ?? NO_BINDINGS;
2539
- const node = {
2540
- id,
2541
- kind,
2542
- props,
2543
- children,
2544
- parent,
2545
- editable,
2546
- lockedProps,
2547
- boundProps,
2548
- ...text !== void 0 ? { text } : {},
2549
- ...origin ? { origin } : {}
2189
+ return sampleY(u);
2550
2190
  };
2551
- if (opaqueInterior) {
2552
- const opaque = {
2553
- ...node,
2554
- editable: editable === "generated" ? "generated" : "literal"
2555
- };
2556
- walker.byId.set(id, opaque);
2557
- return opaque;
2558
- }
2559
- walker.byId.set(id, node);
2560
- return node;
2561
2191
  }
2562
- function textOf(children) {
2563
- const parts = [];
2564
- let onlyText = true;
2565
- Children.forEach(children, (child) => {
2566
- if (typeof child === "string" || typeof child === "number") parts.push(String(child));
2567
- else if (child !== null && child !== void 0 && typeof child !== "boolean") onlyText = false;
2568
- });
2569
- if (!onlyText || parts.length === 0) return void 0;
2570
- return parts.join("");
2192
+ const inOut = (f) => (x) => x < .5 ? f(2 * x) / 2 : 1 - f(2 - 2 * x) / 2;
2193
+ const out = (f) => (x) => 1 - f(1 - x);
2194
+ const pow = (n) => (x) => x ** n;
2195
+ const sineIn = (x) => 1 - Math.cos(x * Math.PI / 2);
2196
+ const expoIn = (x) => x === 0 ? 0 : 2 ** (10 * x - 10);
2197
+ const circIn = (x) => 1 - Math.sqrt(1 - x * x);
2198
+ const backIn = (s) => (x) => (s + 1) * x * x * x - s * x * x;
2199
+ const elasticIn = (p) => (x) => x === 0 ? 0 : x === 1 ? 1 : -(2 ** (10 * x - 10)) * Math.sin((x * 10 - 10.75) * (2 * Math.PI) / p);
2200
+ const bounceFull = (x) => {
2201
+ const n1 = 7.5625, d1 = 2.75;
2202
+ if (x < 1 / d1) return n1 * x * x;
2203
+ if (x < 2 / d1) {
2204
+ x -= 1.5 / d1;
2205
+ return n1 * x * x + .75;
2206
+ }
2207
+ if (x < 2.5 / d1) {
2208
+ x -= 2.25 / d1;
2209
+ return n1 * x * x + .9375;
2210
+ }
2211
+ x -= 2.625 / d1;
2212
+ return n1 * x * x + .984375;
2213
+ };
2214
+ /** A bounce blended toward linear by `1 - h`: `h = 1` is the full bounce, smaller values damp it. */
2215
+ const bounceOut = (h) => (x) => x + (bounceFull(x) - x) * h;
2216
+ /**
2217
+ * Where a spring released at rest from `from` toward `to` is after `time` seconds: the closed-form
2218
+ * damped oscillator, under-, critically or over-damped. Defaults are mass 1, damping 10, stiffness 100.
2219
+ */
2220
+ function springAt(time, from, to, { mass = 1, damping = 10, stiffness = 100 } = {}) {
2221
+ if (time <= 0) return from;
2222
+ const w0 = Math.sqrt(stiffness / mass), zeta = damping / (2 * Math.sqrt(stiffness * mass)), x0 = from - to;
2223
+ if (zeta < 1) {
2224
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
2225
+ return to + Math.exp(-zeta * w0 * time) * (x0 * Math.cos(wd * time) + zeta * w0 * x0 / wd * Math.sin(wd * time));
2226
+ }
2227
+ if (zeta === 1) return to + Math.exp(-w0 * time) * (x0 + w0 * x0 * time);
2228
+ const r1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1)), r2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));
2229
+ return to + -r2 * x0 / (r1 - r2) * Math.exp(r1 * time) + r1 * x0 / (r1 - r2) * Math.exp(r2 * time);
2571
2230
  }
2572
- /** A Layer's handles: exactly one child element whose type is a function component (not a vocabulary element). */
2573
- function interiorOf(children) {
2574
- const only = Children.toArray(children).filter((c) => c !== null && c !== void 0 && typeof c !== "boolean" && c !== "");
2575
- const child = only.length === 1 ? only[0] : null;
2576
- if (!isValidElement(child) || typeof child.type !== "function" || isVocabulary(child.type)) return null;
2577
- const { children: _children, ...props } = child.props;
2578
- return {
2579
- component: child.type.name || "anonymous",
2580
- props
2581
- };
2231
+ /** The default spring squeezed into 0..1: it settles (to about 0.1%) at x = 1, overshooting about 16% on the way. */
2232
+ const SPRING_SETTLE = Math.log(1e3) / 5;
2233
+ const springEase = (x) => x >= 1 ? 1 : springAt(x * SPRING_SETTLE, 0, 1);
2234
+ const quadIn = pow(2);
2235
+ const cubicIn = pow(3);
2236
+ const quartIn = pow(4);
2237
+ const quintIn = pow(5);
2238
+ const back = backIn(1.70158);
2239
+ const backStrong = backIn(2.6);
2240
+ const elastic = elasticIn(3);
2241
+ const elasticSoft = elasticIn(4.5);
2242
+ const bounce = bounceOut(1);
2243
+ const bounceSmall = bounceOut(.5);
2244
+ const EASING_FN = {
2245
+ linear: (x) => x,
2246
+ ease: bezier(.25, .1, .25, 1),
2247
+ "ease-in": bezier(.42, 0, 1, 1),
2248
+ "ease-out": bezier(0, 0, .58, 1),
2249
+ "ease-in-out": bezier(.42, 0, .58, 1),
2250
+ hold: (x) => x >= 1 ? 1 : 0,
2251
+ bezier: bezier(.25, .1, .25, 1),
2252
+ smoothstep: (x) => x * x * (3 - 2 * x),
2253
+ smootherstep: (x) => x * x * x * (x * (x * 6 - 15) + 10),
2254
+ snappy: bezier(.2, .9, .3, 1),
2255
+ smooth: inOut(sineIn),
2256
+ spring: springEase,
2257
+ easeInQuad: quadIn,
2258
+ easeOutQuad: out(quadIn),
2259
+ easeInOutQuad: inOut(quadIn),
2260
+ easeInCubic: cubicIn,
2261
+ easeOutCubic: out(cubicIn),
2262
+ easeInOutCubic: inOut(cubicIn),
2263
+ easeInQuart: quartIn,
2264
+ easeOutQuart: out(quartIn),
2265
+ easeInOutQuart: inOut(quartIn),
2266
+ easeInQuint: quintIn,
2267
+ easeOutQuint: out(quintIn),
2268
+ easeInOutQuint: inOut(quintIn),
2269
+ easeInSine: sineIn,
2270
+ easeOutSine: out(sineIn),
2271
+ easeInOutSine: inOut(sineIn),
2272
+ easeInExpo: expoIn,
2273
+ easeOutExpo: out(expoIn),
2274
+ easeInOutExpo: inOut(expoIn),
2275
+ easeInCirc: circIn,
2276
+ easeOutCirc: out(circIn),
2277
+ easeInOutCirc: inOut(circIn),
2278
+ easeInBack: back,
2279
+ easeOutBack: out(back),
2280
+ easeInOutBack: inOut(back),
2281
+ easeInBackStrong: backStrong,
2282
+ easeOutBackStrong: out(backStrong),
2283
+ easeInOutBackStrong: inOut(backStrong),
2284
+ easeInElastic: elastic,
2285
+ easeOutElastic: out(elastic),
2286
+ easeInOutElastic: inOut(elastic),
2287
+ easeInElasticSoft: elasticSoft,
2288
+ easeOutElasticSoft: out(elasticSoft),
2289
+ easeInOutElasticSoft: inOut(elasticSoft),
2290
+ easeInBounce: out(bounce),
2291
+ easeOutBounce: bounce,
2292
+ easeInOutBounce: inOut(out(bounce)),
2293
+ easeInBounceSmall: out(bounceSmall),
2294
+ easeOutBounceSmall: bounceSmall,
2295
+ easeInOutBounceSmall: inOut(out(bounceSmall))
2296
+ };
2297
+ const INTEGRATION_STEPS = 64;
2298
+ /** Speed at clip time `u` under keys sorted by `at`: holds outside the keys, eases between them. */
2299
+ function rampSpeed(keys, u) {
2300
+ const first = keys[0];
2301
+ const last = keys[keys.length - 1];
2302
+ if (u <= first.at) return first.speed;
2303
+ if (u >= last.at) return last.speed;
2304
+ for (let i = 0; i < keys.length - 1; i++) {
2305
+ const a = keys[i], b = keys[i + 1];
2306
+ if (u < a.at || u >= b.at) continue;
2307
+ const span = b.at - a.at;
2308
+ const fn = EASING_FN[a.easing] ?? EASING_FN.linear;
2309
+ const k = span <= 0 ? 1 : fn(Math.min(1, Math.max(0, (u - a.at) / span)));
2310
+ return a.speed + (b.speed - a.speed) * k;
2311
+ }
2312
+ return last.speed;
2582
2313
  }
2583
- // src/resolve/errors.ts
2584
- function isResolveError(value) {
2585
- return typeof value === "object" && value !== null && "code" in value && "message" in value && "id" in value;
2314
+ /** Source seconds played in `rel` clip seconds: ∫₀ʳᵉˡ speed by the trapezium rule, a fixed step count per key segment. */
2315
+ function rampAdvance(keys, rel) {
2316
+ if (rel <= 0) return 0;
2317
+ const bounds = [
2318
+ 0,
2319
+ ...keys.map((k) => k.at).filter((at) => at > 0 && at < rel),
2320
+ rel
2321
+ ];
2322
+ let total = 0;
2323
+ for (let i = 0; i < bounds.length - 1; i++) {
2324
+ const a = bounds[i], b = bounds[i + 1];
2325
+ if (b <= a) continue;
2326
+ const h = (b - a) / INTEGRATION_STEPS;
2327
+ let sum = (rampSpeed(keys, a) + rampSpeed(keys, b)) / 2;
2328
+ for (let s = 1; s < INTEGRATION_STEPS; s++) sum += rampSpeed(keys, a + s * h);
2329
+ total += sum * h;
2330
+ }
2331
+ return total;
2586
2332
  }
2587
- // src/resolve/overlay.ts
2588
- /**
2589
- * The collected tree with the patch log's net effect applied: removed nodes
2590
- * dropped, inserted nodes placed, moved nodes relocated, props and text
2591
- * overlaid. Returns the same tree when the log is empty.
2592
- */
2593
- function overlay(tree, log) {
2594
- if (!log || log.net.size === 0) return tree;
2595
- const net = log.net;
2596
- if (![...net.values()].some((n) => n.presence !== "present" || n.place !== void 0 || n.bake)) return overlayProps(tree, net);
2597
- const byId = /* @__PURE__ */ new Map();
2598
- const additions = /* @__PURE__ */ new Map();
2599
- const movedAway = /* @__PURE__ */ new Set();
2600
- for (const [id, n] of net) if (n.presence === "inserted" && n.node && n.place) {
2601
- if (tree.byId.has(id)) movedAway.add(id);
2602
- push(additions, n.place.parent, {
2603
- after: n.place.after,
2604
- node: n.node
2605
- });
2606
- } else if (n.presence === "present" && n.place) {
2607
- const original = tree.byId.get(id);
2608
- if (original && original.parent !== n.place.parent) {
2609
- movedAway.add(id);
2610
- push(additions, n.place.parent, {
2611
- after: n.place.after,
2612
- node: original
2613
- });
2614
- } else if (original) {
2615
- movedAway.add(id);
2616
- push(additions, n.place.parent, {
2617
- after: n.place.after,
2618
- node: original
2619
- });
2620
- }
2333
+ /** Clip seconds that play `advance` source seconds: the inverse of `rampAdvance`, by bisection (speed ≥ 0.1, so it is monotonic). */
2334
+ function rampInverse(keys, advance) {
2335
+ if (advance <= 0) return 0;
2336
+ let lo = 0, hi = advance / .1;
2337
+ for (let i = 0; i < 60 && hi - lo > 1e-9; i++) {
2338
+ const mid = (lo + hi) / 2;
2339
+ if (rampAdvance(keys, mid) < advance) lo = mid;
2340
+ else hi = mid;
2621
2341
  }
2622
- const rebuild = (node, parent) => {
2623
- const n = net.get(node.id);
2624
- if (n?.presence === "removed") return null;
2625
- let props = node.props;
2626
- let interior = node.interior;
2627
- let text = node.text;
2628
- if (n) {
2629
- if (n.props.size > 0) ({props, interior} = overlaidProps(node, n));
2630
- if (n.text !== void 0) text = n.text;
2631
- }
2632
- const kept = [];
2633
- for (const child of node.children) {
2634
- if (movedAway.has(child.id)) continue;
2635
- const built = rebuild(child, node.id);
2636
- if (built) kept.push(built);
2637
- }
2638
- const children = insertAdditions(kept, additions.get(node.id) ?? [], node.id, rebuild);
2639
- const out = {
2640
- ...node,
2641
- props,
2642
- children,
2643
- parent,
2644
- ...text !== void 0 ? { text } : {},
2645
- ...interior ? { interior } : {}
2646
- };
2647
- byId.set(out.id, out);
2648
- return out;
2649
- };
2650
- return {
2651
- root: rebuild(tree.root, null) ?? tree.root,
2652
- byId,
2653
- errors: tree.errors
2654
- };
2342
+ return (lo + hi) / 2;
2655
2343
  }
2656
- /**
2657
- * The common case during a drag: only props or text changed. Rebuild the
2658
- * patched nodes and their ancestors; every other node keeps its identity,
2659
- * and with it its props object, so parsed props stay cached downstream.
2660
- */
2661
- function overlayProps(tree, net) {
2662
- const dirty = /* @__PURE__ */ new Set();
2663
- for (const [id, n] of net) {
2664
- if (n.props.size === 0 && n.text === void 0) continue;
2665
- for (let node = tree.byId.get(id); node; node = node.parent === null ? void 0 : tree.byId.get(node.parent)) dirty.add(node.id);
2344
+ /** Element-relative `rel` with the holds before it taken out, or the held source second when `rel` is inside a hold. Holds sorted by `start`. */
2345
+ function freezeShift(holds, rel) {
2346
+ let shift = 0;
2347
+ for (const h of holds) {
2348
+ if (rel < h.start) break;
2349
+ if (rel < h.start + h.duration) return { held: h.at };
2350
+ shift += h.duration;
2666
2351
  }
2667
- if (dirty.size === 0) return tree;
2668
- const byId = new Map(tree.byId);
2669
- const rebuild = (node) => {
2670
- if (!dirty.has(node.id)) return node;
2671
- const n = net.get(node.id);
2672
- let props = node.props;
2673
- let interior = node.interior;
2674
- let text = node.text;
2675
- if (n && n.props.size > 0) ({props, interior} = overlaidProps(node, n));
2676
- if (n && n.text !== void 0) text = n.text;
2677
- const out = {
2678
- ...node,
2679
- props,
2680
- children: node.children.map(rebuild),
2681
- ...text !== void 0 ? { text } : {},
2682
- ...interior ? { interior } : {}
2683
- };
2684
- byId.set(out.id, out);
2685
- return out;
2686
- };
2687
- return {
2688
- root: rebuild(tree.root),
2689
- byId,
2690
- errors: tree.errors
2691
- };
2352
+ return { rel: rel - shift };
2692
2353
  }
2693
- function push(map, key, value) {
2694
- const list = map.get(key);
2695
- if (list) list.push(value);
2696
- else map.set(key, [value]);
2354
+ /** Element-relative time of clip time `u`: the inverse of `freezeShift` outside the holds. A time at a hold's start plays before it. */
2355
+ function freezeUnshift(holds, u) {
2356
+ let rel = u;
2357
+ for (const h of holds) if (h.start < rel - 1e-9) rel += h.duration;
2358
+ return rel;
2697
2359
  }
2698
- function insertAdditions(children, additions, parent, rebuild) {
2699
- if (additions.length === 0) return children;
2700
- const result = [...children];
2701
- for (const { after, node } of additions) {
2702
- const built = rebuild({
2703
- ...node,
2704
- parent
2705
- }, parent);
2706
- if (!built) continue;
2707
- const index = after === null ? -1 : result.findIndex((c) => c.id === after);
2708
- result.splice(index + 1, 0, built);
2709
- }
2710
- return result;
2360
+ /** Total seconds the holds add to a clip. */
2361
+ function holdTotal(holds) {
2362
+ return holds.reduce((n, h) => n + h.duration, 0);
2711
2363
  }
2712
- /** Apply a net's prop writes: `interior.<name>` keys go to the Layer's handles, the rest to the node's own props. */
2713
- function overlaidProps(node, n) {
2714
- const props = { ...node.props };
2715
- let handles = null;
2716
- for (const [k, v] of n.props) {
2717
- const target = k.startsWith("interior.") ? handles ??= { ...node.interior?.props ?? {} } : props;
2718
- const key = k.startsWith("interior.") ? k.slice(9) : k;
2719
- if (v === void 0) delete target[key];
2720
- else target[key] = v;
2721
- }
2722
- return {
2723
- props,
2724
- interior: handles && node.interior ? {
2725
- component: node.interior.component,
2726
- props: handles
2727
- } : node.interior
2728
- };
2364
+ /** A clip's `<SpeedKey>` children, sorted by `at`. */
2365
+ function speedKeysOf(children) {
2366
+ return children.filter((c) => c.kind === "speedKey").map((c) => ({
2367
+ at: Number(c.props.at ?? 0),
2368
+ speed: Number(c.props.speed ?? 1),
2369
+ easing: c.props.easing
2370
+ })).sort((a, b) => a.at - b.at);
2371
+ }
2372
+ /** A clip's `<Freeze>` children, sorted by `start`. */
2373
+ function freezeHoldsOf(children) {
2374
+ return children.filter((c) => c.kind === "freeze").map((c) => ({
2375
+ at: Number(c.props.at ?? 0),
2376
+ start: Number(c.props.start ?? 0),
2377
+ duration: Number(c.props.duration ?? 0)
2378
+ })).sort((a, b) => a.start - b.start);
2729
2379
  }
2730
2380
  // src/resolve/captions.ts
2731
2381
  const DEFAULT_CUE_OPTIONS = {
@@ -2737,7 +2387,7 @@ const SENTENCE_END = /[.!?…]["')\]]?$/;
2737
2387
  /** Composition time of an asset second, through the first play that covers it; null when it is not on the timeline. */
2738
2388
  function onTimeline(sec, plays) {
2739
2389
  const EPS = 1e-6;
2740
- for (const p of plays) if (sec >= p.in - EPS && sec <= p.out + EPS) return ms(playTime(p, sec));
2390
+ for (const p of plays) if (sec >= p.in - EPS && sec <= p.out + EPS) return ms$1(playTime(p, sec));
2741
2391
  return null;
2742
2392
  }
2743
2393
  /** The asset's words at the times they play, in timeline order; words outside every play are left out. */
@@ -2760,6 +2410,8 @@ function timelineWords(asset, plays) {
2760
2410
  * with a capital.
2761
2411
  */
2762
2412
  function planCues(words, options = DEFAULT_CUE_OPTIONS) {
2413
+ /** A cue ends after this word, whatever the limits and the merging below would do. */
2414
+ const forced = (w) => w !== void 0 && options.breakAfter?.has(w.i) === true;
2763
2415
  const lines = [];
2764
2416
  let current = [];
2765
2417
  for (const w of words) {
@@ -2775,12 +2427,16 @@ function planCues(words, options = DEFAULT_CUE_OPTIONS) {
2775
2427
  }
2776
2428
  }
2777
2429
  current.push(w);
2430
+ if (forced(w)) {
2431
+ lines.push(current);
2432
+ current = [];
2433
+ }
2778
2434
  }
2779
2435
  if (current.length > 0) lines.push(current);
2780
2436
  const merged = [];
2781
2437
  for (const line of lines) {
2782
2438
  const prev = merged[merged.length - 1], last = prev?.[prev.length - 1], first = line[0];
2783
- if (!(prev && last && span(line) < MIN_CUE && last.play === first.play && !SENTENCE_END.test(last.text) && first.start - last.end <= options.maxGap)) {
2439
+ if (!(prev && last && !forced(last) && span(line) < MIN_CUE && last.play === first.play && !SENTENCE_END.test(last.text) && first.start - last.end <= options.maxGap)) {
2784
2440
  merged.push([...line]);
2785
2441
  continue;
2786
2442
  }
@@ -2796,14 +2452,14 @@ function planCues(words, options = DEFAULT_CUE_OPTIONS) {
2796
2452
  for (let n = merged.length - 2; n >= 0; n--) {
2797
2453
  const line = merged[n], next = merged[n + 1];
2798
2454
  const last = line[line.length - 1], first = next[0];
2799
- if (span(line) >= MIN_CUE || SENTENCE_END.test(last.text) || last.play !== first.play || first.start - last.end > options.maxGap) continue;
2455
+ if (span(line) >= MIN_CUE || forced(last) || SENTENCE_END.test(last.text) || last.play !== first.play || first.start - last.end > options.maxGap) continue;
2800
2456
  if (chars(line) + 1 + chars(next) <= 2 * options.maxChars) merged.splice(n, 2, [...line, ...next]);
2801
2457
  }
2802
2458
  return merged.map((line, n) => {
2803
2459
  const first = line[0], last = line[line.length - 1];
2804
2460
  const next = merged[n + 1]?.[0];
2805
2461
  const end = last.end - first.start < MIN_CUE && next ? Math.min(first.start + MIN_CUE, next.start) : last.end;
2806
- const opens = n === 0 || SENTENCE_END.test((merged[n - 1]?.[merged[n - 1].length - 1]).text);
2462
+ const opens = n === 0 || SENTENCE_END.test(merged[n - 1].at(-1).text);
2807
2463
  const cueWords = line.map((w, i) => ({
2808
2464
  text: i === 0 && opens ? capitalise(w.text) : w.text,
2809
2465
  start: w.start,
@@ -2811,7 +2467,7 @@ function planCues(words, options = DEFAULT_CUE_OPTIONS) {
2811
2467
  }));
2812
2468
  return {
2813
2469
  start: first.start,
2814
- end: ms(end),
2470
+ end: ms$1(end),
2815
2471
  text: cueWords.map((w) => w.text).join(" "),
2816
2472
  words: cueWords
2817
2473
  };
@@ -2934,12 +2590,12 @@ function sameAsset(a, b) {
2934
2590
  return assetIdOf(a) === assetIdOf(b);
2935
2591
  }
2936
2592
  /** Composition times are written into the file, so they are kept to the millisecond. */
2937
- function ms(x) {
2593
+ function ms$1(x) {
2938
2594
  return Math.round(x * 1e3) / 1e3 + 0;
2939
2595
  }
2940
2596
  function wordOn(w, play) {
2941
- const start = ms(playTime(play, Math.max(w.start, play.in)));
2942
- const end = ms(playTime(play, Math.min(w.end, play.out)));
2597
+ const start = ms$1(playTime(play, Math.max(w.start, play.in)));
2598
+ const end = ms$1(playTime(play, Math.min(w.end, play.out)));
2943
2599
  return {
2944
2600
  i: w.i,
2945
2601
  text: w.text,
@@ -2949,220 +2605,165 @@ function wordOn(w, play) {
2949
2605
  ...w.speaker !== void 0 ? { speaker: w.speaker } : {}
2950
2606
  };
2951
2607
  }
2952
- /** Composition time at which asset second `sec` plays in `play`: through the speed ramp, then past the holds before it. */
2953
- function playTime(play, sec) {
2954
- const advance = Math.max(0, sec - play.in);
2955
- const rel = play.keys ? rampInverse(play.keys, advance) : advance / play.speed;
2956
- return play.start + (play.holds ? freezeUnshift(play.holds, rel) : rel);
2957
- }
2958
- // src/theme/theme.ts
2959
- /** Kinds whose entrance and exit a motion style fills. */
2960
- const MOTION_KINDS = /* @__PURE__ */ new Set([
2961
- "title",
2962
- "shape",
2963
- "svg",
2964
- "sticker",
2965
- "graphic"
2966
- ]);
2967
- /** Props whose strings are content, never tokens. */
2968
- const CONTENT_PROPS = /* @__PURE__ */ new Set([
2969
- "text",
2970
- "content",
2971
- "src",
2972
- "label",
2973
- "name",
2974
- "category",
2975
- "theme"
2976
- ]);
2977
- const byInput = /* @__PURE__ */ new WeakMap();
2978
- let undeclared = null;
2979
- /** The theme a composition's raw `theme` prop describes. Memoised per object; an invalid theme falls back to its base (the schema reports why). */
2980
- function resolveTheme(raw) {
2981
- if (raw === void 0 || raw === null || typeof raw !== "object") return undeclared ??= build(void 0, false);
2982
- const hit = byInput.get(raw);
2983
- if (hit) return hit;
2984
- const parsed = themeSchema.safeParse(raw);
2985
- const theme = build(parsed.success ? parsed.data : baseOnly(raw), true);
2986
- byInput.set(raw, theme);
2987
- return theme;
2988
- }
2989
- /** The value of `$name`, or undefined. */
2990
- function themeToken(theme, name) {
2991
- return Object.hasOwn(theme.tokens, name) ? theme.tokens[name] : void 0;
2608
+ /** Composition time at which asset second `sec` plays in `play`: through the speed ramp, then past the holds before it. */
2609
+ function playTime(play, sec) {
2610
+ const advance = Math.max(0, sec - play.in);
2611
+ const rel = play.keys ? rampInverse(play.keys, advance) : advance / play.speed;
2612
+ return play.start + (play.holds ? freezeUnshift(play.holds, rel) : rel);
2613
+ }
2614
+ // src/resolve/tree.ts
2615
+ /** Prop keys under this prefix address a Layer's handles rather than the Layer's own props. */
2616
+ const INTERIOR_PREFIX = "interior.";
2617
+ function walk(node, visit, depth = 0) {
2618
+ visit(node, depth);
2619
+ for (const child of node.children) walk(child, visit, depth + 1);
2620
+ }
2621
+ function findParentOf(tree, id) {
2622
+ const node = tree.byId.get(id);
2623
+ if (!node || node.parent === null) return null;
2624
+ return tree.byId.get(node.parent) ?? null;
2992
2625
  }
2626
+ // src/resolve/overlay.ts
2993
2627
  /**
2994
- * An element's raw props with its theme applied, before the schema parses them:
2995
- * the preset its `style` names (or the kind's `default` preset) fills the props
2996
- * the element leaves out, the motion style fills `enter`/`exit` (a transition's
2997
- * type and duration), and whole-string `$token` values become theme values. The
2998
- * element's own props always win. Returns the input object when nothing was themed.
2628
+ * The collected tree with the patch log's net effect applied: removed nodes
2629
+ * dropped, inserted nodes placed, moved nodes relocated, props and text
2630
+ * overlaid. Returns the same tree when the log is empty.
2999
2631
  */
3000
- function applyTheme(kind, raw, theme) {
3001
- const named = typeof raw.style === "string" ? raw.style : null;
3002
- if (!theme.declared && named === null && !hasToken(raw)) return {
3003
- props: raw,
3004
- from: EMPTY,
3005
- issues: []
3006
- };
3007
- const out = { ...raw };
3008
- const from = {};
3009
- const issues = [];
3010
- const presets = theme.styles[kind];
3011
- const presetName = named ?? (theme.declared ? "default" : null);
3012
- if (presetName !== null && kind !== "composition") {
3013
- const preset = presets?.[presetName];
3014
- if (preset) {
3015
- if (kind === "captions") {
3016
- const own = typeof raw.style === "object" && raw.style !== null ? raw.style : {};
3017
- out.style = {
3018
- ...preset,
3019
- ...own
3020
- };
3021
- from.style = `style:${presetName}`;
3022
- } else for (const [key, value] of Object.entries(preset)) if (raw[key] === void 0) {
3023
- out[key] = value;
3024
- from[key] = `style:${presetName}`;
3025
- }
3026
- } else if (named !== null) {
3027
- const known = Object.keys(presets ?? {});
3028
- issues.push({
3029
- code: "UNKNOWN_STYLE",
3030
- prop: "style",
3031
- message: `no ${kind} style "${named}" in the theme${known.length ? ` (${known.join(", ")})` : ""}`
2632
+ function overlay(tree, log) {
2633
+ if (!log || log.net.size === 0) return tree;
2634
+ const net = log.net;
2635
+ if (![...net.values()].some((n) => n.presence !== "present" || n.place !== void 0 || n.bake)) return overlayProps(tree, net);
2636
+ const byId = /* @__PURE__ */ new Map();
2637
+ const additions = /* @__PURE__ */ new Map();
2638
+ const movedAway = /* @__PURE__ */ new Set();
2639
+ for (const [id, n] of net) if (n.presence === "inserted" && n.node && n.place) {
2640
+ if (tree.byId.has(id)) movedAway.add(id);
2641
+ push(additions, n.place.parent, {
2642
+ after: n.place.after,
2643
+ node: n.node
2644
+ });
2645
+ } else if (n.presence === "present" && n.place) {
2646
+ const original = tree.byId.get(id);
2647
+ if (original && original.parent !== n.place.parent) {
2648
+ movedAway.add(id);
2649
+ push(additions, n.place.parent, {
2650
+ after: n.place.after,
2651
+ node: original
2652
+ });
2653
+ } else if (original) {
2654
+ movedAway.add(id);
2655
+ push(additions, n.place.parent, {
2656
+ after: n.place.after,
2657
+ node: original
3032
2658
  });
3033
- if (kind === "captions") delete out.style;
3034
- }
3035
- }
3036
- const motion = theme.declared ? theme.motion : null;
3037
- if (motion && MOTION_KINDS.has(kind) && !(kind === "title" && out.animation !== void 0)) {
3038
- if (out.enter === void 0 && motion.enter) {
3039
- out.enter = motion.enter;
3040
- from.enter = "motion";
3041
- }
3042
- if (out.exit === void 0 && motion.exit) {
3043
- out.exit = motion.exit;
3044
- from.exit = "motion";
3045
2659
  }
3046
2660
  }
3047
- if (motion && kind === "transition" && motion.transition) {
3048
- if (out.type === void 0 && motion.transition.type) {
3049
- out.type = motion.transition.type;
3050
- from.type = "motion";
2661
+ const rebuild = (node, parent) => {
2662
+ const n = net.get(node.id);
2663
+ if (n?.presence === "removed") return null;
2664
+ let props = node.props;
2665
+ let interior = node.interior;
2666
+ let text = node.text;
2667
+ if (n) {
2668
+ if (n.props.size > 0) ({props, interior} = overlaidProps(node, n));
2669
+ if (n.text !== void 0) text = n.text;
3051
2670
  }
3052
- if (out.duration === void 0 && motion.transition.duration !== void 0) {
3053
- out.duration = motion.transition.duration;
3054
- from.duration = "motion";
2671
+ const kept = [];
2672
+ for (const child of node.children) {
2673
+ if (movedAway.has(child.id)) continue;
2674
+ const built = rebuild(child, node.id);
2675
+ if (built) kept.push(built);
3055
2676
  }
3056
- }
3057
- if (kind === "composition" && theme.declared && out.background === void 0) {
3058
- out.background = {
3059
- mode: "color",
3060
- color: theme.palette.background
2677
+ const children = insertAdditions(kept, additions.get(node.id) ?? [], node.id, rebuild);
2678
+ const out = {
2679
+ ...node,
2680
+ props,
2681
+ children,
2682
+ parent,
2683
+ ...text !== void 0 ? { text } : {},
2684
+ ...interior ? { interior } : {}
3061
2685
  };
3062
- from.background = "theme";
3063
- }
3064
- for (const key of Object.keys(out)) {
3065
- if (CONTENT_PROPS.has(key)) continue;
3066
- out[key] = substitute(out[key], key, theme, from, issues);
3067
- }
3068
- if (Object.keys(from).length === 0 && issues.length === 0) return {
3069
- props: raw,
3070
- from: EMPTY,
3071
- issues
2686
+ byId.set(out.id, out);
2687
+ return out;
3072
2688
  };
3073
2689
  return {
3074
- props: out,
3075
- from: Object.keys(from).length ? from : EMPTY,
3076
- issues
2690
+ root: rebuild(tree.root, null) ?? tree.root,
2691
+ byId,
2692
+ errors: tree.errors
3077
2693
  };
3078
2694
  }
3079
- const EMPTY = Object.freeze({});
3080
- function substitute(value, path, theme, from, issues) {
3081
- if (typeof value === "string") {
3082
- const m = TOKEN_PATTERN.exec(value);
3083
- if (!m) return value;
3084
- const name = m[1];
3085
- const resolved = themeToken(theme, name);
3086
- if (resolved === void 0) {
3087
- issues.push({
3088
- code: "UNKNOWN_TOKEN",
3089
- prop: path,
3090
- message: `unknown theme token ${value}`
3091
- });
3092
- return value;
3093
- }
3094
- from[path] = value;
3095
- return resolved;
3096
- }
3097
- if (Array.isArray(value)) {
3098
- let changed = null;
3099
- value.forEach((v, i) => {
3100
- const s = substitute(v, `${path}.${i}`, theme, from, issues);
3101
- if (s !== v) (changed ??= [...value])[i] = s;
3102
- });
3103
- return changed ?? value;
3104
- }
3105
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
3106
- let changed = null;
3107
- for (const [k, v] of Object.entries(value)) {
3108
- const s = substitute(v, `${path}.${k}`, theme, from, issues);
3109
- if (s !== v) (changed ??= { ...value })[k] = s;
3110
- }
3111
- return changed ?? value;
3112
- }
3113
- return value;
3114
- }
3115
- function hasToken(value) {
3116
- if (typeof value === "string") return value.startsWith("$");
3117
- if (Array.isArray(value)) return value.some(hasToken);
3118
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
3119
- for (const [k, v] of Object.entries(value)) if (!CONTENT_PROPS.has(k) && hasToken(v)) return true;
2695
+ /**
2696
+ * The common case during a drag: only props or text changed. Rebuild the
2697
+ * patched nodes and their ancestors; every other node keeps its identity,
2698
+ * and with it its props object, so parsed props stay cached downstream.
2699
+ */
2700
+ function overlayProps(tree, net) {
2701
+ const dirty = /* @__PURE__ */ new Set();
2702
+ for (const [id, n] of net) {
2703
+ if (n.props.size === 0 && n.text === void 0) continue;
2704
+ for (let node = tree.byId.get(id); node; node = node.parent === null ? void 0 : tree.byId.get(node.parent)) dirty.add(node.id);
3120
2705
  }
3121
- return false;
3122
- }
3123
- /** For an invalid theme: keep its `extends` when that alone is valid, so the rest of the file still themes sensibly. */
3124
- function baseOnly(raw) {
3125
- const ext = raw.extends;
3126
- const parsed = themeSchema.safeParse(typeof ext === "string" ? { extends: ext } : {});
3127
- return parsed.success ? parsed.data : void 0;
3128
- }
3129
- function build(input, declared) {
3130
- const base = input?.extends ?? "midnight";
3131
- const def = THEMES[base];
3132
- const palette = {
3133
- ...def.palette,
3134
- ...input?.palette
2706
+ if (dirty.size === 0) return tree;
2707
+ const byId = new Map(tree.byId);
2708
+ const rebuild = (node) => {
2709
+ if (!dirty.has(node.id)) return node;
2710
+ const n = net.get(node.id);
2711
+ let props = node.props;
2712
+ let interior = node.interior;
2713
+ let text = node.text;
2714
+ if (n && n.props.size > 0) ({props, interior} = overlaidProps(node, n));
2715
+ if (n && n.text !== void 0) text = n.text;
2716
+ const out = {
2717
+ ...node,
2718
+ props,
2719
+ children: node.children.map(rebuild),
2720
+ ...text !== void 0 ? { text } : {},
2721
+ ...interior ? { interior } : {}
2722
+ };
2723
+ byId.set(out.id, out);
2724
+ return out;
3135
2725
  };
3136
- const fonts = {
3137
- ...def.fonts,
3138
- ...input?.fonts
2726
+ return {
2727
+ root: rebuild(tree.root),
2728
+ byId,
2729
+ errors: tree.errors
3139
2730
  };
3140
- const radius = input?.radius ?? def.radius;
3141
- const m = input?.motion ?? def.motion;
3142
- const style = typeof m === "string" ? MOTIONS[m] : m;
3143
- const motion = style.enter || style.exit || style.transition ? style : null;
3144
- const styles = {};
3145
- for (const [kind, presets] of Object.entries(def.styles)) styles[kind] = { ...presets };
3146
- for (const [kind, presets] of Object.entries(input?.styles ?? {})) {
3147
- const merged = styles[kind] ??= {};
3148
- for (const [name, preset] of Object.entries(presets ?? {})) merged[name] = {
3149
- ...merged[name],
3150
- ...preset
3151
- };
2731
+ }
2732
+ function push(map, key, value) {
2733
+ const list = map.get(key);
2734
+ if (list) list.push(value);
2735
+ else map.set(key, [value]);
2736
+ }
2737
+ function insertAdditions(children, additions, parent, rebuild) {
2738
+ if (additions.length === 0) return children;
2739
+ const result = [...children];
2740
+ for (const { after, node } of additions) {
2741
+ const built = rebuild({
2742
+ ...node,
2743
+ parent
2744
+ }, parent);
2745
+ if (!built) continue;
2746
+ const index = after === null ? -1 : result.findIndex((c) => c.id === after);
2747
+ result.splice(index + 1, 0, built);
2748
+ }
2749
+ return result;
2750
+ }
2751
+ /** Apply a net's prop writes: `interior.<name>` keys go to the Layer's handles, the rest to the node's own props. */
2752
+ function overlaidProps(node, n) {
2753
+ const props = { ...node.props };
2754
+ let handles = null;
2755
+ for (const [k, v] of n.props) {
2756
+ const target = k.startsWith("interior.") ? handles ??= { ...node.interior?.props ?? {} } : props;
2757
+ const key = k.startsWith("interior.") ? k.slice(9) : k;
2758
+ if (v === void 0) delete target[key];
2759
+ else target[key] = v;
3152
2760
  }
3153
2761
  return {
3154
- declared,
3155
- base,
3156
- palette,
3157
- fonts,
3158
- radius,
3159
- motion,
3160
- styles,
3161
- tokens: {
3162
- ...palette,
3163
- ...fonts,
3164
- radius
3165
- }
2762
+ props,
2763
+ interior: handles && node.interior ? {
2764
+ component: node.interior.component,
2765
+ props: handles
2766
+ } : node.interior
3166
2767
  };
3167
2768
  }
3168
2769
  // src/resolve/resolve.ts
@@ -3194,6 +2795,7 @@ function resolve(tree, manifest, log) {
3194
2795
  */
3195
2796
  function compute(original, manifest, log, previous) {
3196
2797
  const tree = overlay(original, log);
2798
+ fileShaders = original.shaders ?? [];
3197
2799
  const errors = [...tree.errors];
3198
2800
  const root = tree.root;
3199
2801
  const theme = resolveTheme(root.props.theme);
@@ -3386,7 +2988,9 @@ function compute(original, manifest, log, previous) {
3386
2988
  tree,
3387
2989
  manifest,
3388
2990
  log,
3389
- theme
2991
+ theme,
2992
+ fonts: original.fonts ?? [],
2993
+ shaders: original.shaders ?? []
3390
2994
  };
3391
2995
  }
3392
2996
  /**
@@ -3399,6 +3003,40 @@ function compute(original, manifest, log, previous) {
3399
3003
  const parsedByProps = /* @__PURE__ */ new WeakMap();
3400
3004
  /** Which parsed props the theme supplied, keyed by the parsed `data`. */
3401
3005
  const themedByData = /* @__PURE__ */ new WeakMap();
3006
+ /** The shaders of the file being resolved (`defineEffect`, `defineTransition`); set for the length of one `compute`. */
3007
+ let fileShaders = [];
3008
+ /**
3009
+ * An `<Effect>` or `<Transition>` of a type the file declares: its params checked and filled by the declared schema.
3010
+ * An unknown type is an error naming the built-in ones; null for a built-in type or any other element.
3011
+ */
3012
+ function declaredShader(node, data, own) {
3013
+ if (node.kind !== "effect" && node.kind !== "transition") return null;
3014
+ const type = String(data.type);
3015
+ const builtIn = node.kind === "effect" ? VIDEO_EFFECT_TYPES : TRANSITION_TYPES;
3016
+ if (builtIn.includes(type)) return null;
3017
+ const def = shaderFor(fileShaders, node.kind, type);
3018
+ if (!def) {
3019
+ own.push({
3020
+ code: "SCHEMA",
3021
+ id: node.id,
3022
+ prop: "type",
3023
+ message: `unknown ${node.kind} "${type}": use one of ${builtIn.join(", ")}, or declare it with ${node.kind === "effect" ? "defineEffect" : "defineTransition"}()`
3024
+ });
3025
+ return null;
3026
+ }
3027
+ const result = def.params.safeParse(data.params ?? {});
3028
+ if (result.success) return {
3029
+ ...data,
3030
+ params: result.data
3031
+ };
3032
+ for (const issue of result.error.issues) own.push({
3033
+ code: "SCHEMA",
3034
+ id: node.id,
3035
+ prop: ["params", ...issue.path.map(String)].join("."),
3036
+ message: issue.message
3037
+ });
3038
+ return null;
3039
+ }
3402
3040
  function parseProps(node, errors, theme) {
3403
3041
  const cached = parsedByProps.get(node.props);
3404
3042
  if (cached && cached.theme === theme) {
@@ -3406,7 +3044,7 @@ function parseProps(node, errors, theme) {
3406
3044
  return cached.data;
3407
3045
  }
3408
3046
  const themed = applyTheme(node.kind, node.props, theme);
3409
- if (cached && cached.unthemed && themed.props === node.props) {
3047
+ if (cached?.unthemed && themed.props === node.props) {
3410
3048
  parsedByProps.set(node.props, {
3411
3049
  ...cached,
3412
3050
  theme
@@ -3420,14 +3058,17 @@ function parseProps(node, errors, theme) {
3420
3058
  prop: issue.prop,
3421
3059
  message: issue.message
3422
3060
  }));
3423
- const schema = SCHEMAS_BY_KIND[node.kind];
3061
+ const engine = node.kind === "layer" && typeof node.props.engine === "string" ? node.props.engine : null;
3062
+ const schema = engine ? ENGINE_SCHEMAS[engine] : SCHEMAS_BY_KIND[node.kind];
3424
3063
  let data;
3425
3064
  if (!schema) data = { ...themed.props };
3426
3065
  else {
3427
3066
  const result = schema.safeParse(themed.props);
3428
3067
  if (result.success) {
3429
3068
  data = result.data;
3430
- if (node.kind === "transition") data = {
3069
+ const declared = declaredShader(node, data, own);
3070
+ if (declared) data = declared;
3071
+ else if (node.kind === "transition" && isBuiltInTransition(String(data.type))) data = {
3431
3072
  ...data,
3432
3073
  params: {
3433
3074
  ...defaultTransitionParams(data.type),
@@ -3612,14 +3253,19 @@ function framed(props, asset, comp) {
3612
3253
  });
3613
3254
  return out;
3614
3255
  }
3256
+ /** The seconds a time reference resolved to, or `fallback` with its error recorded. */
3257
+ function secondsOr(p, resolved, fallback) {
3258
+ if (typeof resolved === "number") return resolved;
3259
+ p.errors.push(resolved);
3260
+ return fallback;
3261
+ }
3615
3262
  function sourceRange(p, asset, markers) {
3616
3263
  const { node, props } = p;
3617
3264
  const speed = Number(props.speed ?? 1);
3618
- const inT = resolveTimeRef(props.in ?? 0, asset, markers, {
3265
+ const inSec = secondsOr(p, resolveTimeRef(props.in ?? 0, asset, markers, {
3619
3266
  id: node.id,
3620
3267
  prop: "in"
3621
- });
3622
- const inSec = typeof inT === "number" ? inT : (p.errors.push(inT), 0);
3268
+ }), 0);
3623
3269
  const dur = props.duration;
3624
3270
  const keys = speedKeysOf(node.children);
3625
3271
  const holds = holdTotal(freezeHoldsOf(node.children));
@@ -3632,13 +3278,11 @@ function sourceRange(p, asset, markers) {
3632
3278
  prop: "duration",
3633
3279
  message: "give either out or duration, not both"
3634
3280
  });
3635
- if (props.out !== void 0) {
3636
- const o = resolveTimeRef(props.out, asset, markers, {
3637
- id: node.id,
3638
- prop: "out"
3639
- });
3640
- outSec = typeof o === "number" ? o : (p.errors.push(o), inSec);
3641
- } else if (typeof dur === "number") {
3281
+ if (props.out !== void 0) outSec = secondsOr(p, resolveTimeRef(props.out, asset, markers, {
3282
+ id: node.id,
3283
+ prop: "out"
3284
+ }), inSec);
3285
+ else if (typeof dur === "number") {
3642
3286
  played = dur;
3643
3287
  outSec = inSec + (keys.length > 0 ? rampAdvance(keys, dur) : dur * speed);
3644
3288
  } else if (dur === "fill" || typeof dur === "object" && dur !== null) {
@@ -4095,7 +3739,8 @@ function placeCaptions(root, pending, tracks, manifest, markers) {
4095
3739
  const options = {
4096
3740
  maxChars: Number(cp.props.maxChars ?? lineChars(root, cp.props.style)),
4097
3741
  maxDuration: Number(cp.props.maxDuration ?? 5),
4098
- maxGap: Number(cp.props.maxGap ?? .8)
3742
+ maxGap: Number(cp.props.maxGap ?? .8),
3743
+ ...Array.isArray(cp.props.breakAfter) ? { breakAfter: new Set(cp.props.breakAfter.map((ref) => Number(ref.slice(5)))) } : {}
4099
3744
  };
4100
3745
  nodes = cueNodes(node, planCues(timelineWords(asset, plays), options), (id) => pending.has(id));
4101
3746
  cuesByCaptions.set(node, {
@@ -4160,172 +3805,6 @@ function captionTime(ref, asset, plays, markers, ctx) {
4160
3805
  };
4161
3806
  return at;
4162
3807
  }
4163
- // src/resolve/scene.ts
4164
- /**
4165
- * Resolve a `<Nested of={Scene}>` interior: the scene is called with the
4166
- * nested element's props (minus `of` and `id`) and what it returns is
4167
- * resolved inside a composition of the outer size. The player renders the
4168
- * result; `nested/extract` writes it into the file.
4169
- */
4170
- function resolveScene(of, props, outer, manifest) {
4171
- const { of: _of, id: _id, ...rest } = props;
4172
- const inner = of(rest);
4173
- return resolve(collect(createElement(Composition, {
4174
- fps: outer.fps,
4175
- width: outer.width,
4176
- height: outer.height
4177
- }, inner)), manifest);
4178
- }
4179
- // src/theme/fonts.ts
4180
- const BUNDLED_FONTS = [
4181
- {
4182
- family: "Inter",
4183
- id: "inter",
4184
- italic: true
4185
- },
4186
- {
4187
- family: "Space Grotesk",
4188
- id: "space-grotesk",
4189
- italic: false
4190
- },
4191
- {
4192
- family: "Playfair Display",
4193
- id: "playfair-display",
4194
- italic: true
4195
- },
4196
- {
4197
- family: "DM Sans",
4198
- id: "dm-sans",
4199
- italic: true
4200
- },
4201
- {
4202
- family: "JetBrains Mono",
4203
- id: "jetbrains-mono",
4204
- italic: true
4205
- }
4206
- ];
4207
- const BUNDLED_FONT_WEIGHTS = [400, 700];
4208
- function isBundledFont(family) {
4209
- return BUNDLED_FONTS.some((f) => f.family === family);
4210
- }
4211
- // src/patches/log.ts
4212
- var Log = class {
4213
- list = [];
4214
- reduced = /* @__PURE__ */ new Map();
4215
- v = 0;
4216
- isSource;
4217
- constructor(options) {
4218
- this.isSource = options.isSource ?? (() => false);
4219
- }
4220
- get entries() {
4221
- return this.list;
4222
- }
4223
- get net() {
4224
- return this.reduced;
4225
- }
4226
- get version() {
4227
- return this.v;
4228
- }
4229
- append(patch) {
4230
- this.list.push(patch);
4231
- reduceInto(this.reduced, patch, this.isSource);
4232
- this.v += 1;
4233
- }
4234
- mark() {
4235
- return this.list.length;
4236
- }
4237
- truncate(mark) {
4238
- if (mark >= this.list.length) return;
4239
- this.list.length = Math.max(0, mark);
4240
- this.reduced.clear();
4241
- for (const p of this.list) reduceInto(this.reduced, p, this.isSource);
4242
- this.v += 1;
4243
- }
4244
- clear() {
4245
- this.list.length = 0;
4246
- this.reduced.clear();
4247
- this.v += 1;
4248
- }
4249
- };
4250
- function createPatchLog(options = {}) {
4251
- return new Log(options);
4252
- }
4253
- function entry(net, id) {
4254
- let e = net.get(id);
4255
- if (!e) {
4256
- e = {
4257
- id,
4258
- presence: "present",
4259
- props: /* @__PURE__ */ new Map(),
4260
- bake: false
4261
- };
4262
- net.set(id, e);
4263
- }
4264
- return e;
4265
- }
4266
- /** Keep `net` reduced: last write wins per prop, structural ops collapse, removal of an insert vanishes. */
4267
- function reduceInto(net, patch, isSource = () => false) {
4268
- switch (patch.op) {
4269
- case "set": {
4270
- const e = entry(net, patch.id);
4271
- if (e.presence === "removed") throw new Error(`patch set on removed element ${patch.id}`);
4272
- e.props.set(patch.prop, patch.value);
4273
- return;
4274
- }
4275
- case "text": {
4276
- const e = entry(net, patch.id);
4277
- if (e.presence === "removed") throw new Error(`patch text on removed element ${patch.id}`);
4278
- e.text = patch.text;
4279
- return;
4280
- }
4281
- case "move": {
4282
- const e = entry(net, patch.id);
4283
- if (e.presence === "removed") throw new Error(`patch move on removed element ${patch.id}`);
4284
- e.place = {
4285
- parent: patch.parent,
4286
- after: patch.after
4287
- };
4288
- return;
4289
- }
4290
- case "remove": {
4291
- if (net.get(patch.id)?.presence === "inserted" && !isSource(patch.id)) {
4292
- net.delete(patch.id);
4293
- return;
4294
- }
4295
- const r = entry(net, patch.id);
4296
- r.presence = "removed";
4297
- r.props.clear();
4298
- delete r.text;
4299
- delete r.place;
4300
- delete r.node;
4301
- return;
4302
- }
4303
- case "insert": {
4304
- const e = entry(net, patch.id);
4305
- e.presence = e.presence === "removed" && e.node !== void 0 && nodesEqual(e.node, patch.node) ? "present" : "inserted";
4306
- e.props.clear();
4307
- e.node = patch.node;
4308
- e.place = {
4309
- parent: patch.parent,
4310
- after: patch.after
4311
- };
4312
- return;
4313
- }
4314
- case "bake":
4315
- entry(net, patch.id).bake = true;
4316
- return;
4317
- }
4318
- }
4319
- function nodesEqual(a, b) {
4320
- return a.id === b.id && a.kind === b.kind && stableJson(a.props) === stableJson(b.props) && a.text === b.text && a.children.length === b.children.length && a.children.every((c, i) => nodesEqual(c, b.children[i]));
4321
- }
4322
- function stableJson(value) {
4323
- return JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a < b ? -1 : 1)) : v);
4324
- }
4325
- function asJson(value) {
4326
- if (value === void 0) return void 0;
4327
- return JSON.parse(JSON.stringify(value));
4328
- }
4329
3808
  // src/actions/types/base.ts
4330
3809
  const ok = {
4331
3810
  valid: true,
@@ -4366,7 +3845,6 @@ function nodeOfSpec(spec, parent) {
4366
3845
  const AUTO_GROUP_TYPES = /* @__PURE__ */ new Set([
4367
3846
  "transform/update",
4368
3847
  "effect/update",
4369
- "audioEffect/update",
4370
3848
  "audio/setVolume",
4371
3849
  "audio/setFade",
4372
3850
  "audio/setDuck",
@@ -4379,8 +3857,6 @@ const AUTO_GROUP_TYPES = /* @__PURE__ */ new Set([
4379
3857
  "clip/slide",
4380
3858
  "clip/roll",
4381
3859
  "clip/setSpeed",
4382
- "clip/setChromaKey",
4383
- "clip/setStabilization",
4384
3860
  "keyframe/move",
4385
3861
  "keyframe/update",
4386
3862
  "composition/updateSettings",
@@ -4414,33 +3890,12 @@ function targetIdOf(action) {
4414
3890
  const value = params[key];
4415
3891
  if (typeof value === "string" && value.length > 0) return value;
4416
3892
  }
4417
- return null;
4418
- }
4419
- function describeAction(action) {
4420
- const [ns, name] = action.type.split("/");
4421
- const words = name.replace(/([A-Z])/g, " $1").toLowerCase();
4422
- return `${words.charAt(0).toUpperCase()}${words.slice(1)} ${ns}`;
4423
- }
4424
- // src/actions/registry.ts
4425
- const registry = /* @__PURE__ */ new Map();
4426
- function registerHandler(handler) {
4427
- if (registry.has(handler.type)) throw new Error(`handler for ${handler.type} registered twice`);
4428
- registry.set(handler.type, handler);
4429
- }
4430
- function getHandler(type) {
4431
- return registry.get(type);
4432
- }
4433
- function listHandlers() {
4434
- return [...registry.keys()].sort();
4435
- }
4436
- function inverseOf(action, before) {
4437
- const handler = getHandler(action.type);
4438
- if (!handler) throw new Error(`no handler for ${action.type}`);
4439
- const inv = handler.invert(action, before);
4440
- return Array.isArray(inv) ? {
4441
- type: "batch/apply",
4442
- params: { actions: inv }
4443
- } : inv;
3893
+ return null;
3894
+ }
3895
+ function describeAction(action) {
3896
+ const [ns, name] = action.type.split("/");
3897
+ const words = name.replace(/([A-Z])/g, " $1").toLowerCase();
3898
+ return `${words.charAt(0).toUpperCase()}${words.slice(1)} ${ns}`;
4444
3899
  }
4445
3900
  // src/actions/history.ts
4446
3901
  /**
@@ -4564,6 +4019,27 @@ var ActionHistory = class {
4564
4019
  this.groupDepth = 0;
4565
4020
  }
4566
4021
  };
4022
+ // src/actions/registry.ts
4023
+ const registry = /* @__PURE__ */ new Map();
4024
+ function registerHandler(handler) {
4025
+ if (registry.has(handler.type)) throw new Error(`handler for ${handler.type} registered twice`);
4026
+ registry.set(handler.type, handler);
4027
+ }
4028
+ function getHandler(type) {
4029
+ return registry.get(type);
4030
+ }
4031
+ function listHandlers() {
4032
+ return [...registry.keys()].sort();
4033
+ }
4034
+ function inverseOf(action, before) {
4035
+ const handler = getHandler(action.type);
4036
+ if (!handler) throw new Error(`no handler for ${action.type}`);
4037
+ const inv = handler.invert(action, before);
4038
+ return Array.isArray(inv) ? {
4039
+ type: "batch/apply",
4040
+ params: { actions: inv }
4041
+ } : inv;
4042
+ }
4567
4043
  // src/actions/executor.ts
4568
4044
  /**
4569
4045
  * Runs actions against `{ tree, patches }`: validate on the state before,
@@ -4700,221 +4176,905 @@ var NotImplemented = class extends Error {
4700
4176
  this.name = "NotImplemented";
4701
4177
  }
4702
4178
  };
4703
- function requireElement(state, id, path = "id") {
4704
- if (typeof id !== "string") return {
4705
- code: "INVALID_PARAMS",
4706
- message: `${path} must be a string`,
4707
- path
4708
- };
4709
- return state.items.get(id) ?? {
4710
- code: "ELEMENT_NOT_FOUND",
4711
- message: `element "${id}" does not exist`,
4712
- path
4179
+ function requireElement(state, id, path = "id") {
4180
+ if (typeof id !== "string") return {
4181
+ code: "INVALID_PARAMS",
4182
+ message: `${path} must be a string`,
4183
+ path
4184
+ };
4185
+ return state.items.get(id) ?? {
4186
+ code: "ELEMENT_NOT_FOUND",
4187
+ message: `element "${id}" does not exist`,
4188
+ path
4189
+ };
4190
+ }
4191
+ function requireUnlocked(state, item) {
4192
+ const track = item.track ? state.tracks.find((t) => t.id === item.track) : void 0;
4193
+ if (track?.locked) return {
4194
+ code: "TRACK_LOCKED",
4195
+ message: `track "${track.id}" is locked`
4196
+ };
4197
+ return null;
4198
+ }
4199
+ function requireNotOpaque(item) {
4200
+ return item.editable === "opaque" ? {
4201
+ code: "OPAQUE_TARGET",
4202
+ message: `"${item.id}" is model-only code`
4203
+ } : null;
4204
+ }
4205
+ function requireLiteralProp(item, prop) {
4206
+ return item.lockedProps.includes(prop) ? {
4207
+ code: "LOCKED_PROP",
4208
+ message: `"${item.id}.${prop}" is an expression; bake it first`,
4209
+ path: prop
4210
+ } : null;
4211
+ }
4212
+ function requireLayout(state, item, layout) {
4213
+ const track = state.tracks.find((t) => t.id === item.track);
4214
+ if (!track || track.layout !== layout) return {
4215
+ code: "UNSUPPORTED_ON_LAYOUT",
4216
+ message: `needs a ${layout} track`
4217
+ };
4218
+ return null;
4219
+ }
4220
+ function collectErrors(...errors) {
4221
+ return invalid(...errors.filter((e) => Boolean(e) && "code" in e && "message" in e));
4222
+ }
4223
+ function isError(value) {
4224
+ return "code" in value && "message" in value && !("kind" in value);
4225
+ }
4226
+ /** Standard checks for a prop write on an element. */
4227
+ function validateTarget(state, id, props = []) {
4228
+ const item = requireElement(state, id);
4229
+ if (isError(item)) return invalid(item);
4230
+ const errors = [];
4231
+ const locked = requireUnlocked(state, item);
4232
+ if (locked) errors.push(locked);
4233
+ const opaque = requireNotOpaque(item);
4234
+ if (opaque) errors.push(opaque);
4235
+ for (const p of props) {
4236
+ const e = requireLiteralProp(item, p);
4237
+ if (e) errors.push(e);
4238
+ }
4239
+ return invalid(...errors);
4240
+ }
4241
+ function rawNode(state, id) {
4242
+ return state.tree.byId.get(id);
4243
+ }
4244
+ function rawProp(state, id, prop) {
4245
+ const node = rawNode(state, id);
4246
+ if (prop.startsWith("interior.")) return asJson(node?.interior?.props[prop.slice(9)]);
4247
+ return asJson(node?.props[prop]);
4248
+ }
4249
+ function siblingAfter(state, id) {
4250
+ const node = rawNode(state, id);
4251
+ const parent = node?.parent ? rawNode(state, node.parent) : void 0;
4252
+ if (!node || !parent) return null;
4253
+ const index = parent.children.findIndex((c) => c.id === id);
4254
+ return index > 0 ? parent.children[index - 1].id : null;
4255
+ }
4256
+ function placementProps(place) {
4257
+ if ("start" in place) return {
4258
+ start: place.start,
4259
+ anchor: void 0,
4260
+ offset: void 0,
4261
+ at: void 0
4262
+ };
4263
+ if ("anchor" in place) return {
4264
+ anchor: place.anchor,
4265
+ offset: place.offset,
4266
+ start: void 0
4267
+ };
4268
+ return {
4269
+ start: void 0,
4270
+ anchor: void 0,
4271
+ offset: void 0,
4272
+ at: void 0
4273
+ };
4274
+ }
4275
+ function placementOf(state, id) {
4276
+ const node = rawNode(state, id);
4277
+ if (!node) return { after: null };
4278
+ if (typeof node.props.anchor === "string") return typeof node.props.offset === "number" ? {
4279
+ anchor: node.props.anchor,
4280
+ offset: node.props.offset
4281
+ } : { anchor: node.props.anchor };
4282
+ if (typeof node.props.start === "number") return { start: node.props.start };
4283
+ return { after: siblingAfter(state, id) };
4284
+ }
4285
+ /** `base`, or `base-2`, `base-3`, … until it names nothing in the tree, the resolved items or `taken`. */
4286
+ function uniqueId(state, base, taken = /* @__PURE__ */ new Set()) {
4287
+ let id = base;
4288
+ for (let n = 2; state.tree.byId.has(id) || state.items.has(id) || taken.has(id); n++) id = `${base}-${n}`;
4289
+ return id;
4290
+ }
4291
+ function setProps(log, id, props) {
4292
+ for (const [prop, value] of Object.entries(props)) log.append({
4293
+ op: "set",
4294
+ id,
4295
+ prop,
4296
+ value
4297
+ });
4298
+ }
4299
+ function insertNode(log, parent, after, spec) {
4300
+ log.append({
4301
+ op: "insert",
4302
+ id: spec.id,
4303
+ parent,
4304
+ after,
4305
+ node: nodeOfSpec(spec, parent)
4306
+ });
4307
+ }
4308
+ function placeNode(log, state, parent, place, spec, afterOverride) {
4309
+ const after = afterOverride !== void 0 ? afterOverride : "after" in place ? place.after : lastChildId(state, parent);
4310
+ const props = {
4311
+ ...spec.props,
4312
+ ...placementProps(place)
4313
+ };
4314
+ const cleaned = Object.fromEntries(Object.entries(props).filter(([, v]) => v !== void 0));
4315
+ insertNode(log, parent, after, {
4316
+ ...spec,
4317
+ props: cleaned
4318
+ });
4319
+ }
4320
+ function lastChildId(state, parent) {
4321
+ const node = rawNode(state, parent);
4322
+ const last = node?.children[node.children.length - 1];
4323
+ return last ? last.id : null;
4324
+ }
4325
+ function childIndex(state, parent, childId) {
4326
+ const node = rawNode(state, parent);
4327
+ return node ? node.children.findIndex((c) => c.id === childId) : -1;
4328
+ }
4329
+ function childAt(state, parent, index) {
4330
+ const child = rawNode(state, parent)?.children[index];
4331
+ return child ? child.id : null;
4332
+ }
4333
+ /** An action that sets literal props on one element; the inverse sets the prior values. */
4334
+ function makePropHandler(spec) {
4335
+ const target = spec.target ?? ((params) => params.id);
4336
+ return {
4337
+ type: spec.type,
4338
+ validate(action, state) {
4339
+ const id = target(action.params);
4340
+ const base = validateTarget(state, id, Object.keys(spec.write(action.params, state)));
4341
+ if (!base.valid) return base;
4342
+ const item = state.items.get(id);
4343
+ if (spec.kinds && item && !spec.kinds.includes(item.kind)) return invalid({
4344
+ code: "INVALID_PARAMS",
4345
+ message: `${spec.type} does not apply to <${item.kind}>`
4346
+ });
4347
+ return spec.validate ? spec.validate(action, state) : ok;
4348
+ },
4349
+ apply(action, log, ctx) {
4350
+ setProps(log, target(action.params), spec.write(action.params, ctx.before));
4351
+ },
4352
+ invert(action, before) {
4353
+ const id = target(action.params);
4354
+ const prior = {};
4355
+ for (const prop of Object.keys(spec.write(action.params, before))) prior[prop] = rawProp(before, id, prop);
4356
+ return {
4357
+ type: "source/setProps",
4358
+ params: {
4359
+ id,
4360
+ props: prior
4361
+ }
4362
+ };
4363
+ },
4364
+ fold: () => []
4365
+ };
4366
+ }
4367
+ /** Register a handler and return it, for one-line namespace files. */
4368
+ function define(handler) {
4369
+ registerHandler(handler);
4370
+ return handler;
4371
+ }
4372
+ function stub(type, validate, invert) {
4373
+ return define({
4374
+ type,
4375
+ validate,
4376
+ apply: () => {
4377
+ throw new NotImplemented(type);
4378
+ },
4379
+ invert,
4380
+ fold: () => []
4381
+ });
4382
+ }
4383
+ // src/jsx/animate.ts
4384
+ /**
4385
+ * Maps `input` from `inputRange` onto `outputRange`, piecewise: `interpolate(elapsed, [0, 0.5, 2], [0, 1, 0.8])`.
4386
+ * The input range must increase. Beyond the ends the value extends linearly, unless clamped; easing applies only inside.
4387
+ */
4388
+ function interpolate(input, inputRange, outputRange, options = {}) {
4389
+ if (inputRange.length < 2 || inputRange.length !== outputRange.length) throw new Error("interpolate() wants input and output ranges of the same length, at least two");
4390
+ for (let i = 1; i < inputRange.length; i++) if (!(inputRange[i] > inputRange[i - 1])) throw new Error(`interpolate() wants an increasing input range, got [${inputRange.join(", ")}]`);
4391
+ let i = 1;
4392
+ while (i < inputRange.length - 1 && input > inputRange[i]) i++;
4393
+ const a = inputRange[i - 1], b = inputRange[i];
4394
+ let x = (input - a) / (b - a);
4395
+ if (x < 0 && options.extrapolateLeft === "clamp") x = 0;
4396
+ if (x > 1 && options.extrapolateRight === "clamp") x = 1;
4397
+ const k = x >= 0 && x <= 1 && options.easing ? EASING_FN[options.easing](x) : x;
4398
+ const lo = outputRange[i - 1];
4399
+ return lo + (outputRange[i] - lo) * k;
4400
+ }
4401
+ /** A physical spring from `from` (default 0) to `to` (default 1) at `time` seconds: mass 1, damping 10, stiffness 100 unless given. */
4402
+ function spring({ time, from = 0, to = 1, overshootClamping = false, ...config }) {
4403
+ const v = springAt(time, from, to, config);
4404
+ return overshootClamping ? to >= from ? Math.min(v, to) : Math.max(v, to) : v;
4405
+ }
4406
+ /** A number in [0, 1) fixed by `seed`: the same seed gives the same number every frame, in the preview and the export. */
4407
+ function random$1(seed) {
4408
+ return random(hash(String(seed)))();
4409
+ }
4410
+ // src/jsx/engines.ts
4411
+ /** On `globalThis`, like the layer clock: a file loaded with its own copy of the kit still finds the host's engines. */
4412
+ const engines = globalThis[Symbol.for("@cutcraft/kit/layer-engines")] ??= /* @__PURE__ */ new Map();
4413
+ /** Install (or with `null`, remove) the engine that draws `<Layer engine={name}>` tags such as `<Three>`. */
4414
+ function installLayerEngine(name, engine) {
4415
+ if (engine) engines.set(name, engine);
4416
+ else engines.delete(name);
4417
+ }
4418
+ function layerEngine(name) {
4419
+ return engines.get(name) ?? null;
4420
+ }
4421
+ /**
4422
+ * Frames an engine is still preparing (a three.js renderer being created, say). The browser export waits for none
4423
+ * before it captures a frame; the player ignores them. On `globalThis` for the same reason as the engines.
4424
+ */
4425
+ const pending = globalThis[Symbol.for("@cutcraft/kit/layer-frames-pending")] ??= { count: 0 };
4426
+ /** Hold the frame until the returned release is called (once; later calls do nothing). */
4427
+ function holdLayerFrame() {
4428
+ pending.count++;
4429
+ let released = false;
4430
+ return () => {
4431
+ if (released) return;
4432
+ released = true;
4433
+ pending.count--;
4434
+ };
4435
+ }
4436
+ /** How many holds are open: 0 when every engine has drawn the frame it was given. */
4437
+ function layerFramesPending() {
4438
+ return pending.count;
4439
+ }
4440
+ // src/jsx/rendering.ts
4441
+ /**
4442
+ * Where the renderers draw less than a file can say. Every type and enum value the schemas accept is drawn (a type the
4443
+ * player and the export do not draw is not offered); these are the remaining limits, which the `not-rendered` lint
4444
+ * reports, so a file that checks clean renders as written.
4445
+ */
4446
+ /** Masks render on these kinds, one per element. */
4447
+ const MASKED_KINDS = ["clip", "nested"];
4448
+ const RENDERED_VALUES = [...[
4449
+ "title",
4450
+ "shape",
4451
+ "svg",
4452
+ "sticker",
4453
+ "graphic",
4454
+ "layer"
4455
+ ].map((kind) => ({
4456
+ kind,
4457
+ prop: "blend",
4458
+ values: ["normal"],
4459
+ reason: "is drawn unblended over the footage, in the preview and the export"
4460
+ }))];
4461
+ const NOT_RENDERED = [
4462
+ {
4463
+ kind: "speedKey",
4464
+ parent: "nested",
4465
+ reason: "is ignored inside <Nested>: the sequence plays at its constant speed"
4466
+ },
4467
+ {
4468
+ kind: "freeze",
4469
+ parent: "nested",
4470
+ reason: "is ignored inside <Nested>: the sequence plays at its constant speed"
4471
+ },
4472
+ {
4473
+ kind: "effect",
4474
+ prop: "params.gamma",
4475
+ reason: "is ignored: levels render their black and white points only"
4476
+ },
4477
+ {
4478
+ kind: "composition",
4479
+ prop: "background.mode",
4480
+ value: "blur",
4481
+ reason: "draws as the plain colour when an overlay sits beneath the footage"
4482
+ },
4483
+ {
4484
+ kind: "captions",
4485
+ prop: "animation",
4486
+ value: "typewriter",
4487
+ reason: "types out only cues without word timings; cues with words show as karaoke"
4488
+ }
4489
+ ];
4490
+ const MASK_NOT_RENDERED = {
4491
+ /** On a kind outside MASKED_KINDS. */
4492
+ kind: "is ignored: masks render on clips and nested sequences only, the element shows unmasked",
4493
+ second: "is ignored: only the first <Mask> on an element renders"
4494
+ };
4495
+ /** The composition's motion blur (`<Composition motionBlur>`), or null when it has none. */
4496
+ function motionBlurOf(state) {
4497
+ const blur = state.items.get(state.composition.id)?.props.motionBlur;
4498
+ if (!blur) return null;
4499
+ return {
4500
+ shutterAngle: blur.shutterAngle ?? 180,
4501
+ samples: blur.samples ?? 8
4713
4502
  };
4714
4503
  }
4715
- function requireUnlocked(state, item) {
4716
- const track = item.track ? state.tracks.find((t) => t.id === item.track) : void 0;
4717
- if (track?.locked) return {
4718
- code: "TRACK_LOCKED",
4719
- message: `track "${track.id}" is locked`
4720
- };
4721
- return null;
4504
+ /**
4505
+ * The composition seconds a frame at `t` averages: `samples` moments spread evenly across the open shutter, which
4506
+ * opens at `t` and stays open `shutterAngle / 360` of a frame. Never past the composition's end.
4507
+ */
4508
+ function shutterTimes(t, fps, blur, duration) {
4509
+ const open = blur.shutterAngle / 360 / fps;
4510
+ const last = Math.max(0, duration - 1e-6);
4511
+ return Array.from({ length: blur.samples }, (_, k) => Math.min(last, t + open * k / blur.samples));
4722
4512
  }
4723
- function requireNotOpaque(item) {
4724
- return item.editable === "opaque" ? {
4725
- code: "OPAQUE_TARGET",
4726
- message: `"${item.id}" is model-only code`
4727
- } : null;
4513
+ // src/jsx/use-t.ts
4514
+ /**
4515
+ * The installed clock lives on `globalThis`, not in this module: a cut file loaded by a tool may carry its own
4516
+ * copy of the kit (a private module graph, a second install), and its `useT()` must still find the player's clock.
4517
+ */
4518
+ const slot = globalThis[Symbol.for("@cutcraft/kit/layer-clock")] ??= { impl: null };
4519
+ /** The layer's clock: progress, seconds elapsed and remaining, its duration, the composition time. */
4520
+ function useLayer() {
4521
+ const impl = slot.impl;
4522
+ if (!impl) throw new Error("useLayer() needs a Player. The kit does not render; install one with installLayerClock().");
4523
+ return impl();
4728
4524
  }
4729
- function requireLiteralProp(item, prop) {
4730
- return item.lockedProps.includes(prop) ? {
4731
- code: "LOCKED_PROP",
4732
- message: `"${item.id}.${prop}" is an expression; bake it first`,
4733
- path: prop
4734
- } : null;
4525
+ /** Normalised time (0..1) inside a `<Layer>`; `useLayer().t`. */
4526
+ function useT() {
4527
+ const impl = slot.impl;
4528
+ if (!impl) throw new Error("useT() needs a Player. The kit does not render; install one with installLayerClock().");
4529
+ return impl().t;
4735
4530
  }
4736
- function requireLayout(state, item, layout) {
4737
- const track = state.tracks.find((t) => t.id === item.track);
4738
- if (!track || track.layout !== layout) return {
4739
- code: "UNSUPPORTED_ON_LAYOUT",
4740
- message: `needs a ${layout} track`
4531
+ function installLayerClock(fn) {
4532
+ slot.impl = fn;
4533
+ }
4534
+ // src/manifest/transcript.ts
4535
+ /**
4536
+ * Reads a transcript into the manifest's word list (`assets.<id>.transcript.words`), sniffing the format when it is
4537
+ * not given. Subtitle cues time whole lines, so their words share the cue's time by length and are marked `estimated`;
4538
+ * JSON with word times keeps them exactly.
4539
+ *
4540
+ * const { words } = parseTranscript(await (await fetch("/talk.vtt")).text());
4541
+ * manifest.assets.talk = { ...manifest.assets.talk, transcript: { words } };
4542
+ */
4543
+ function parseTranscript(text, format = sniff(text)) {
4544
+ const raw = format === "json" ? fromJson(text) : fromCues(text, format);
4545
+ const words = [];
4546
+ for (const w of raw) {
4547
+ const t = w.text.trim();
4548
+ if (!t || !Number.isFinite(w.start) || !Number.isFinite(w.end)) continue;
4549
+ const start = ms(Math.max(0, w.start));
4550
+ words.push({
4551
+ i: words.length,
4552
+ text: t,
4553
+ start,
4554
+ end: Math.max(start, ms(w.end)),
4555
+ ...w.speaker ? { speaker: w.speaker } : {},
4556
+ ...w.estimated ? { estimated: true } : {}
4557
+ });
4558
+ }
4559
+ return {
4560
+ words,
4561
+ estimated: words.some((w) => w.estimated)
4741
4562
  };
4742
- return null;
4743
4563
  }
4744
- function collectErrors(...errors) {
4745
- return invalid(...errors.filter((e) => Boolean(e) && "code" in e && "message" in e));
4564
+ function sniff(text) {
4565
+ const head = text.replace(/^/, "").trimStart();
4566
+ if (head.startsWith("{") || head.startsWith("[")) return "json";
4567
+ if (head.startsWith("WEBVTT")) return "vtt";
4568
+ if (TIMING.test(text)) return "srt";
4569
+ throw new Error("not a transcript: expected SRT, WebVTT or JSON");
4746
4570
  }
4747
- function isError(value) {
4748
- return "code" in value && "message" in value && !("kind" in value);
4571
+ const ms = (s) => Math.round(s * 1e3) / 1e3;
4572
+ const STAMP = String.raw`(?:(\d+):)?(\d{1,2}):(\d{2})[.,](\d{1,3})`;
4573
+ const TIMING = new RegExp(`${STAMP}\\s*-->\\s*${STAMP}`);
4574
+ function seconds(h, m, s, f) {
4575
+ return Number(h ?? 0) * 3600 + Number(m) * 60 + Number(s) + Number(f.padEnd(3, "0")) / 1e3;
4749
4576
  }
4750
- /** Standard checks for a prop write on an element. */
4751
- function validateTarget(state, id, props = []) {
4752
- const item = requireElement(state, id);
4753
- if (isError(item)) return invalid(item);
4754
- const errors = [];
4755
- const locked = requireUnlocked(state, item);
4756
- if (locked) errors.push(locked);
4757
- const opaque = requireNotOpaque(item);
4758
- if (opaque) errors.push(opaque);
4759
- for (const p of props) {
4760
- const e = requireLiteralProp(item, p);
4761
- if (e) errors.push(e);
4577
+ function fromCues(text, format) {
4578
+ const out = [];
4579
+ const blocks = text.replace(/^/, "").replace(/\r\n?/g, "\n").split(/\n{2,}/);
4580
+ for (const block of blocks) {
4581
+ const lines = block.split("\n");
4582
+ const at = lines.findIndex((l) => TIMING.test(l));
4583
+ if (at < 0) continue;
4584
+ const m = TIMING.exec(lines[at]);
4585
+ const start = seconds(m[1], m[2], m[3], m[4]);
4586
+ const end = seconds(m[5], m[6], m[7], m[8]);
4587
+ const body = lines.slice(at + 1).map((l) => l.replace(/^-\s+/, "")).join(" ");
4588
+ out.push(...format === "vtt" ? vttCue(body, start, end) : shareOut(clean(body), start, end, void 0));
4762
4589
  }
4763
- return invalid(...errors);
4590
+ return out;
4764
4591
  }
4765
- function rawNode(state, id) {
4766
- return state.tree.byId.get(id);
4592
+ /** Text without markup: tags (`<i>`, `<c.x>`, `<v Ann>`), SSA overrides (`{\an8}`) and the common entities. */
4593
+ function clean(text) {
4594
+ return text.replace(/<[^>]*>/g, "").replace(/\{\\[^}]*\}/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
4767
4595
  }
4768
- function rawProp(state, id, prop) {
4769
- const node = rawNode(state, id);
4770
- if (prop.startsWith("interior.")) return asJson(node?.interior?.props[prop.slice(9)]);
4771
- return asJson(node?.props[prop]);
4596
+ /** A cue's words, each given a share of [start, end] by its length: an estimate, marked as one. */
4597
+ function shareOut(text, start, end, speaker, estimated = true) {
4598
+ const words = text.split(/\s+/).filter(Boolean);
4599
+ if (words.length === 0) return [];
4600
+ const total = words.reduce((n, w) => n + w.length, 0);
4601
+ const out = [];
4602
+ let at = start;
4603
+ for (const w of words) {
4604
+ const next = at + (end - start) * w.length / total;
4605
+ out.push({
4606
+ text: w,
4607
+ start: at,
4608
+ end: next,
4609
+ ...speaker ? { speaker } : {},
4610
+ ...estimated ? { estimated } : {}
4611
+ });
4612
+ at = next;
4613
+ }
4614
+ return out;
4772
4615
  }
4773
- function siblingAfter(state, id) {
4774
- const node = rawNode(state, id);
4775
- const parent = node?.parent ? rawNode(state, node.parent) : void 0;
4776
- if (!node || !parent) return null;
4777
- const index = parent.children.findIndex((c) => c.id === id);
4778
- return index > 0 ? parent.children[index - 1].id : null;
4616
+ const INLINE = new RegExp(`<${STAMP}>`, "g");
4617
+ /**
4618
+ * A WebVTT cue: `<v Ann>` names the speaker; inline timestamps (`<00:01.500>`), where a captioner wrote them, start
4619
+ * the words after them. A stretch between two timestamps that holds one word is measured, not estimated.
4620
+ */
4621
+ function vttCue(body, start, end) {
4622
+ const speaker = /<v(?:\.[^\s>]*)?\s+([^>]+)>/.exec(body)?.[1]?.trim();
4623
+ const stamps = [];
4624
+ for (const m of body.matchAll(INLINE)) stamps.push({
4625
+ at: seconds(m[1], m[2], m[3], m[4]),
4626
+ index: m.index,
4627
+ length: m[0].length
4628
+ });
4629
+ if (stamps.length === 0) return shareOut(clean(body), start, end, speaker);
4630
+ const out = [];
4631
+ const edges = [{
4632
+ at: start,
4633
+ index: 0,
4634
+ length: 0
4635
+ }, ...stamps];
4636
+ edges.forEach((edge, n) => {
4637
+ const next = edges[n + 1];
4638
+ const text = clean(body.slice(edge.index + edge.length, next ? next.index : body.length));
4639
+ const words = text.split(/\s+/).filter(Boolean);
4640
+ out.push(...shareOut(text, edge.at, next ? next.at : end, speaker, words.length > 1));
4641
+ });
4642
+ return out;
4779
4643
  }
4780
- function placementProps(place) {
4781
- if ("start" in place) return {
4782
- start: place.start,
4783
- anchor: void 0,
4784
- offset: void 0,
4785
- at: void 0
4786
- };
4787
- if ("anchor" in place) return {
4788
- anchor: place.anchor,
4789
- offset: place.offset,
4790
- start: void 0
4644
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4645
+ const num = (v) => typeof v === "number" ? v : NaN;
4646
+ const str = (v) => typeof v === "string" ? v : void 0;
4647
+ const SHAPES = "{ words: [{ word | text, start, end }] } (OpenAI, ElevenLabs, CutCraft), { segments: [{ words | text, start, end }] } (openai-whisper, faster-whisper, WhisperX) or { transcription: [{ tokens, offsets }] } (whisper.cpp)";
4648
+ function fromJson(text) {
4649
+ let data;
4650
+ try {
4651
+ data = JSON.parse(text);
4652
+ } catch (error) {
4653
+ throw new Error(`not a transcript: ${error instanceof Error ? error.message : String(error)}`);
4654
+ }
4655
+ if (Array.isArray(data)) data = { segments: data };
4656
+ if (!isObject(data)) throw new Error(`not a transcript: expected ${SHAPES}`);
4657
+ if (Array.isArray(data.words)) return words(data.words);
4658
+ if (Array.isArray(data.segments)) return data.segments.filter(isObject).flatMap(segment);
4659
+ if (Array.isArray(data.transcription)) return data.transcription.filter(isObject).flatMap(whisperCpp);
4660
+ throw new Error(`not a transcript: expected ${SHAPES}`);
4661
+ }
4662
+ /** Word objects in seconds: `word` (OpenAI, whisper) or `text` (ElevenLabs, CutCraft); ElevenLabs' spacing dropped. */
4663
+ function words(list, segmentSpeaker) {
4664
+ const out = [];
4665
+ for (const w of list) {
4666
+ if (!isObject(w)) continue;
4667
+ if (typeof w.type === "string" && w.type !== "word") continue;
4668
+ const speaker = str(w.speaker) ?? str(w.speaker_id) ?? segmentSpeaker;
4669
+ out.push({
4670
+ text: str(w.word) ?? str(w.text) ?? "",
4671
+ start: num(w.start),
4672
+ end: num(w.end),
4673
+ ...speaker ? { speaker } : {},
4674
+ ...w.estimated === true ? { estimated: true } : {}
4675
+ });
4676
+ }
4677
+ return out;
4678
+ }
4679
+ /** A segment: its words when it has word times, else its text shared out over its span. */
4680
+ function segment(s) {
4681
+ const speaker = str(s.speaker);
4682
+ if (Array.isArray(s.words) && s.words.length > 0) return words(s.words, speaker);
4683
+ return shareOut(str(s.text) ?? "", num(s.start), num(s.end), speaker);
4684
+ }
4685
+ /** whisper.cpp's `-ojf` segment: sub-word tokens with offsets in ms, joined into words on a leading space. */
4686
+ function whisperCpp(s) {
4687
+ const offsets = isObject(s.offsets) ? s.offsets : {};
4688
+ const tokens = Array.isArray(s.tokens) ? s.tokens.filter(isObject) : [];
4689
+ if (tokens.length === 0) return shareOut(str(s.text) ?? "", num(offsets.from) / 1e3, num(offsets.to) / 1e3, void 0);
4690
+ const out = [];
4691
+ for (const t of tokens) {
4692
+ const text = str(t.text) ?? "";
4693
+ if (/^\s*\[_[^\]]*\]\s*$/.test(text) || /^\s*<\|[^|]*\|>\s*$/.test(text)) continue;
4694
+ const o = isObject(t.offsets) ? t.offsets : {};
4695
+ const from = num(o.from) / 1e3;
4696
+ const to = num(o.to) / 1e3;
4697
+ const last = out[out.length - 1];
4698
+ if (!last || /^\s/.test(text)) out.push({
4699
+ text: text.trim(),
4700
+ start: from,
4701
+ end: to
4702
+ });
4703
+ else {
4704
+ last.text += text;
4705
+ last.end = to;
4706
+ }
4707
+ }
4708
+ return out;
4709
+ }
4710
+ // src/theme/fonts.ts
4711
+ const BUNDLED_FONTS = [
4712
+ {
4713
+ family: "Inter",
4714
+ id: "inter",
4715
+ italic: true
4716
+ },
4717
+ {
4718
+ family: "Space Grotesk",
4719
+ id: "space-grotesk",
4720
+ italic: false
4721
+ },
4722
+ {
4723
+ family: "Playfair Display",
4724
+ id: "playfair-display",
4725
+ italic: true
4726
+ },
4727
+ {
4728
+ family: "DM Sans",
4729
+ id: "dm-sans",
4730
+ italic: true
4731
+ },
4732
+ {
4733
+ family: "JetBrains Mono",
4734
+ id: "jetbrains-mono",
4735
+ italic: true
4736
+ }
4737
+ ];
4738
+ const BUNDLED_FONT_WEIGHTS = [400, 700];
4739
+ function isBundledFont(family) {
4740
+ return BUNDLED_FONTS.some((f) => f.family === family);
4741
+ }
4742
+ const STORE_KEY = Symbol.for("cutcraft.fonts.store");
4743
+ const store = globalThis[STORE_KEY] ??= {
4744
+ scope: null,
4745
+ global: /* @__PURE__ */ new Map()
4746
+ };
4747
+ /** The faces a module loaded by `loadModule` declared, stored on its exports under this key. */
4748
+ const MODULE_FONTS = Symbol.for("cutcraft.fonts");
4749
+ function declare(face) {
4750
+ if (!(Number.isInteger(face.weight) && face.weight >= 100 && face.weight <= 900 && face.weight % 100 === 0)) throw new Error(`font "${face.family}": weight must be 100, 200 … 900, got ${face.weight}`);
4751
+ if (face.style !== "normal" && face.style !== "italic") throw new Error(`font "${face.family}": style must be "normal" or "italic", got "${face.style}"`);
4752
+ (store.scope ?? store.global).set(`${face.family}|${face.weight}|${face.style}`, face);
4753
+ return face.family;
4754
+ }
4755
+ /**
4756
+ * Declares a face from a font file and returns its family, to pass as a `font` prop, a theme font or a CSS
4757
+ * `fontFamily`. Call it at the top of the file with literal arguments, as the editor and lint read them from the source.
4758
+ *
4759
+ * const brand = font({ family: "Brand Sans", src: "asset:brand", weight: 700 });
4760
+ * <Title id="hello" font={brand}>Hello</Title>
4761
+ *
4762
+ * `src: "asset:brand"` finds `brand.ttf` (or `.otf`, `.woff`, `.woff2`) where the export finds media, and asks the
4763
+ * player's `media` for `brand`.
4764
+ */
4765
+ function font(options) {
4766
+ if (!options.src.startsWith("asset:") || options.src.length === 6) throw new Error(`font "${options.family}": src must be "asset:<id>", got "${options.src}"`);
4767
+ return declare({
4768
+ family: options.family,
4769
+ weight: options.weight ?? 400,
4770
+ style: options.style ?? "normal",
4771
+ source: {
4772
+ kind: "asset",
4773
+ id: options.src.slice(6)
4774
+ }
4775
+ });
4776
+ }
4777
+ /**
4778
+ * Declares a Google Fonts family, in each weight and style given (default 400, normal), and returns the family. The
4779
+ * export downloads the files once into `~/.cache/cutcraft/fonts` (`CUTCRAFT_FONT_CACHE` moves it) and reads them from
4780
+ * there; the player loads them from Google.
4781
+ *
4782
+ * const titan = googleFont("Titan One");
4783
+ * const inter = googleFont("Inter Tight", { weights: [400, 700], styles: ["normal", "italic"] });
4784
+ */
4785
+ function googleFont(family, options = {}) {
4786
+ for (const weight of options.weights ?? [400]) for (const style of options.styles ?? ["normal"]) declare({
4787
+ family,
4788
+ weight,
4789
+ style,
4790
+ source: { kind: "google" }
4791
+ });
4792
+ return family;
4793
+ }
4794
+ /** Runs `load` and returns the faces declared while it ran: how `loadModule` keeps each file's faces apart. */
4795
+ function collectFonts(load) {
4796
+ const outer = store.scope;
4797
+ const scope = /* @__PURE__ */ new Map();
4798
+ store.scope = scope;
4799
+ try {
4800
+ load();
4801
+ } finally {
4802
+ store.scope = outer;
4803
+ }
4804
+ return [...scope.values()];
4805
+ }
4806
+ /** The faces a module declared: those `loadModule` recorded for it, or else every face declared outside a load. */
4807
+ function fontsOf(mod) {
4808
+ return mod[MODULE_FONTS] ?? [...store.global.values()];
4809
+ }
4810
+ // src/resolve/collect.ts
4811
+ const NO_BINDINGS = Object.freeze({});
4812
+ /**
4813
+ * Walk the element tree the file's default export returned and index every
4814
+ * vocabulary element by id. Pure helper components are expanded by calling
4815
+ * them; a `<Layer>` interior is not entered.
4816
+ */
4817
+ function collect(element, options = {}) {
4818
+ const walker = {
4819
+ byId: /* @__PURE__ */ new Map(),
4820
+ errors: [],
4821
+ index: options.index,
4822
+ helperUses: /* @__PURE__ */ new Map()
4791
4823
  };
4824
+ const roots = collectChildren(element, null, walker, null);
4825
+ const root = roots.find((n) => n.kind === "composition") ?? roots[0];
4826
+ if (!root) {
4827
+ const empty = {
4828
+ id: "composition",
4829
+ kind: "composition",
4830
+ props: {},
4831
+ children: [],
4832
+ parent: null,
4833
+ editable: "literal",
4834
+ lockedProps: [],
4835
+ boundProps: {}
4836
+ };
4837
+ walker.errors.push({
4838
+ code: "SCHEMA",
4839
+ id: "composition",
4840
+ message: "the file must render a <Composition>"
4841
+ });
4842
+ return {
4843
+ root: empty,
4844
+ byId: walker.byId,
4845
+ errors: walker.errors
4846
+ };
4847
+ }
4848
+ const multi = new Set([...walker.helperUses].filter(([, n]) => n > 1).map(([name]) => name));
4849
+ if (multi.size > 0) {
4850
+ for (const node of walker.byId.values()) if (node.origin && multi.has(node.origin.helper) && node.editable === "literal") node.editable = "generated";
4851
+ }
4792
4852
  return {
4793
- start: void 0,
4794
- anchor: void 0,
4795
- offset: void 0,
4796
- at: void 0
4853
+ root,
4854
+ byId: walker.byId,
4855
+ errors: walker.errors
4797
4856
  };
4798
4857
  }
4799
- function placementOf(state, id) {
4800
- const node = rawNode(state, id);
4801
- if (!node) return { after: null };
4802
- if (typeof node.props.anchor === "string") return typeof node.props.offset === "number" ? {
4803
- anchor: node.props.anchor,
4804
- offset: node.props.offset
4805
- } : { anchor: node.props.anchor };
4806
- if (typeof node.props.start === "number") return { start: node.props.start };
4807
- return { after: siblingAfter(state, id) };
4858
+ function collectModule(mod, overrides = {}, options = {}) {
4859
+ return {
4860
+ ...collect(mod.default({
4861
+ ...mod.defaults ?? {},
4862
+ ...overrides
4863
+ }) ?? Fragment({ children: null }), options),
4864
+ fonts: fontsOf(mod),
4865
+ shaders: shadersOf(mod)
4866
+ };
4808
4867
  }
4809
- /** `base`, or `base-2`, `base-3`, … until it names nothing in the tree, the resolved items or `taken`. */
4810
- function uniqueId(state, base, taken = /* @__PURE__ */ new Set()) {
4811
- let id = base;
4812
- for (let n = 2; state.tree.byId.has(id) || state.items.has(id) || taken.has(id); n++) id = `${base}-${n}`;
4813
- return id;
4868
+ function collectChildren(node, parent, walker, origin) {
4869
+ const out = [];
4870
+ Children.forEach(node, (child) => {
4871
+ if (!isValidElement(child)) return;
4872
+ const element = child;
4873
+ const type = element.type;
4874
+ if (type === Fragment) {
4875
+ out.push(...collectChildren(element.props.children, parent, walker, origin));
4876
+ return;
4877
+ }
4878
+ if (isVocabulary(type)) {
4879
+ if (type.kind === "layer") {
4880
+ const engine = engineOf(type);
4881
+ const node = makeNode(engine ? withEngine(element, engine) : element, type.kind, parent, walker, origin, [], true);
4882
+ const all = Children.toArray(element.props.children);
4883
+ const own = all.filter(isLayerChild);
4884
+ const children = own.length > 0 ? collectChildren(own, node.id, walker, origin) : [];
4885
+ const interior = engine ? null : interiorOf(all.filter((c) => !isLayerChild(c)));
4886
+ const full = {
4887
+ ...node,
4888
+ children,
4889
+ ...interior ? { interior } : {}
4890
+ };
4891
+ if (full !== node) walker.byId.set(node.id, full);
4892
+ out.push(full);
4893
+ return;
4894
+ }
4895
+ const placeholder = makeNode(element, type.kind, parent, walker, origin, [], false);
4896
+ const children = collectChildren(element.props.children, placeholder.id, walker, origin);
4897
+ const node = {
4898
+ ...placeholder,
4899
+ children
4900
+ };
4901
+ walker.byId.set(node.id, node);
4902
+ out.push(node);
4903
+ return;
4904
+ }
4905
+ if (typeof type === "function") {
4906
+ const helper = type;
4907
+ const name = helper.name || "anonymous";
4908
+ const uses = (walker.helperUses.get(name) ?? 0) + 1;
4909
+ walker.helperUses.set(name, uses);
4910
+ let produced;
4911
+ try {
4912
+ produced = helper(element.props);
4913
+ } catch (error) {
4914
+ walker.errors.push({
4915
+ code: "SCHEMA",
4916
+ id: parent ?? "composition",
4917
+ message: `helper ${name} threw while expanding: ${String(error)}`
4918
+ });
4919
+ return;
4920
+ }
4921
+ const childOrigin = origin ?? {
4922
+ helper: name,
4923
+ callerId: parent,
4924
+ callIndex: uses - 1
4925
+ };
4926
+ out.push(...collectChildren(produced, parent, walker, childOrigin));
4927
+ return;
4928
+ }
4929
+ out.push(...collectChildren(element.props.children, parent, walker, origin));
4930
+ });
4931
+ return out;
4814
4932
  }
4815
- function setProps(log, id, props) {
4816
- for (const [prop, value] of Object.entries(props)) log.append({
4817
- op: "set",
4933
+ function makeNode(element, kind, parent, walker, origin, children, opaqueInterior) {
4934
+ const { children: rawChildren, ...props } = element.props;
4935
+ const idValue = props.id ?? (kind === "composition" ? "composition" : void 0);
4936
+ const id = typeof idValue === "string" && idValue.length > 0 ? idValue : `${kind}-${walker.byId.size + 1}`;
4937
+ if (typeof idValue !== "string" || idValue.length === 0) walker.errors.push({
4938
+ code: "SCHEMA",
4818
4939
  id,
4819
- prop,
4820
- value
4940
+ prop: "id",
4941
+ message: `<${kind}> needs a literal id`
4821
4942
  });
4822
- }
4823
- function insertNode(log, parent, after, spec) {
4824
- log.append({
4825
- op: "insert",
4826
- id: spec.id,
4827
- parent,
4828
- after,
4829
- node: nodeOfSpec(spec, parent)
4943
+ if (walker.byId.has(id)) walker.errors.push({
4944
+ code: "DUPLICATE_ID",
4945
+ id,
4946
+ message: `id "${id}" is used more than once`
4830
4947
  });
4831
- }
4832
- function placeNode(log, state, parent, place, spec, afterOverride) {
4833
- const after = afterOverride !== void 0 ? afterOverride : "after" in place ? place.after : lastChildId(state, parent);
4834
- const props = {
4835
- ...spec.props,
4836
- ...placementProps(place)
4948
+ const text = textOf(rawChildren);
4949
+ const indexed = walker.index?.elements.get(id);
4950
+ let editable = "literal";
4951
+ if (walker.index && (!indexed || indexed.generated)) editable = "generated";
4952
+ const lockedProps = indexed?.lockedProps ?? [];
4953
+ const boundProps = indexed?.boundProps ?? NO_BINDINGS;
4954
+ const node = {
4955
+ id,
4956
+ kind,
4957
+ props,
4958
+ children,
4959
+ parent,
4960
+ editable,
4961
+ lockedProps,
4962
+ boundProps,
4963
+ ...text !== void 0 ? { text } : {},
4964
+ ...origin ? { origin } : {}
4837
4965
  };
4838
- const cleaned = Object.fromEntries(Object.entries(props).filter(([, v]) => v !== void 0));
4839
- insertNode(log, parent, after, {
4840
- ...spec,
4841
- props: cleaned
4842
- });
4966
+ if (opaqueInterior) {
4967
+ const opaque = {
4968
+ ...node,
4969
+ editable: editable === "generated" ? "generated" : "literal"
4970
+ };
4971
+ walker.byId.set(id, opaque);
4972
+ return opaque;
4973
+ }
4974
+ walker.byId.set(id, node);
4975
+ return node;
4843
4976
  }
4844
- function lastChildId(state, parent) {
4845
- const node = rawNode(state, parent);
4846
- const last = node?.children[node.children.length - 1];
4847
- return last ? last.id : null;
4977
+ function textOf(children) {
4978
+ const parts = [];
4979
+ let onlyText = true;
4980
+ Children.forEach(children, (child) => {
4981
+ if (typeof child === "string" || typeof child === "number") parts.push(String(child));
4982
+ else if (isValidElement(child) && isVocabulary(child.type)) return;
4983
+ else if (child !== null && child !== void 0 && typeof child !== "boolean") onlyText = false;
4984
+ });
4985
+ if (!onlyText || parts.length === 0) return void 0;
4986
+ return parts.join("");
4848
4987
  }
4849
- function childIndex(state, parent, childId) {
4850
- const node = rawNode(state, parent);
4851
- return node ? node.children.findIndex((c) => c.id === childId) : -1;
4988
+ /** The element with its engine named in its props, where the resolver, the Player and the export read it. */
4989
+ function withEngine(element, engine) {
4990
+ return {
4991
+ ...element,
4992
+ props: {
4993
+ ...element.props,
4994
+ engine
4995
+ }
4996
+ };
4852
4997
  }
4853
- function childAt(state, parent, index) {
4854
- const child = rawNode(state, parent)?.children[index];
4855
- return child ? child.id : null;
4998
+ /** The timeline children a Layer may hold in its JSX, beside its free code. */
4999
+ const LAYER_CHILD_KINDS = /* @__PURE__ */ new Set(["effect", "keyframe"]);
5000
+ function isLayerChild(child) {
5001
+ return isValidElement(child) && isVocabulary(child.type) && LAYER_CHILD_KINDS.has(child.type.kind);
4856
5002
  }
4857
- /** An action that sets literal props on one element; the inverse sets the prior values. */
4858
- function makePropHandler(spec) {
4859
- const target = spec.target ?? ((params) => params.id);
5003
+ /** A Layer's handles: exactly one child element whose type is a function component (not a vocabulary element). */
5004
+ function interiorOf(children) {
5005
+ const only = Children.toArray(children).filter((c) => c !== null && c !== void 0 && typeof c !== "boolean" && c !== "");
5006
+ const child = only.length === 1 ? only[0] : null;
5007
+ if (!isValidElement(child) || typeof child.type !== "function" || isVocabulary(child.type)) return null;
5008
+ const { children: _children, ...props } = child.props;
4860
5009
  return {
4861
- type: spec.type,
4862
- validate(action, state) {
4863
- const id = target(action.params);
4864
- const base = validateTarget(state, id, Object.keys(spec.write(action.params, state)));
4865
- if (!base.valid) return base;
4866
- const item = state.items.get(id);
4867
- if (spec.kinds && item && !spec.kinds.includes(item.kind)) return invalid({
4868
- code: "INVALID_PARAMS",
4869
- message: `${spec.type} does not apply to <${item.kind}>`
4870
- });
4871
- return spec.validate ? spec.validate(action, state) : ok;
4872
- },
4873
- apply(action, log, ctx) {
4874
- setProps(log, target(action.params), spec.write(action.params, ctx.before));
4875
- },
4876
- invert(action, before) {
4877
- const id = target(action.params);
4878
- const prior = {};
4879
- for (const prop of Object.keys(spec.write(action.params, before))) prior[prop] = rawProp(before, id, prop);
4880
- return {
4881
- type: "source/setProps",
4882
- params: {
4883
- id,
4884
- props: prior
4885
- }
4886
- };
4887
- },
4888
- fold: () => []
5010
+ component: child.type.name || "anonymous",
5011
+ props
4889
5012
  };
4890
5013
  }
4891
- /** Register a handler and return it, for one-line namespace files. */
4892
- function define(handler) {
4893
- registerHandler(handler);
4894
- return handler;
5014
+ // src/resolve/errors.ts
5015
+ function isResolveError(value) {
5016
+ return typeof value === "object" && value !== null && "code" in value && "message" in value && "id" in value;
4895
5017
  }
4896
- function stub(type, validate, invert) {
4897
- return define({
4898
- type,
4899
- validate,
4900
- apply: () => {
4901
- throw new NotImplemented(type);
4902
- },
4903
- invert,
4904
- fold: () => []
4905
- });
5018
+ // src/resolve/scene.ts
5019
+ /**
5020
+ * Resolve a `<Nested of={Scene}>` interior: the scene is called with the
5021
+ * nested element's props (minus `of` and `id`) and what it returns is
5022
+ * resolved inside a composition of the outer size. The player renders the
5023
+ * result; `nested/extract` writes it into the file.
5024
+ */
5025
+ function resolveScene(of, props, outer, manifest) {
5026
+ const { of: _of, id: _id, ...rest } = props;
5027
+ const inner = of(rest);
5028
+ return resolve(collect(createElement(Composition, {
5029
+ fps: outer.fps,
5030
+ width: outer.width,
5031
+ height: outer.height
5032
+ }, inner)), manifest);
5033
+ }
5034
+ // src/resolve/subtitles.ts
5035
+ /**
5036
+ * The captions the cut plays, as an SRT or WebVTT file: every `<Caption>` under a `<Captions>`, written in the file
5037
+ * or generated from a transcript, at the times the video shows them. With `from`/`to` (an export of part of the
5038
+ * composition), cues are clipped to that range and timed from its start, so they line up with that file.
5039
+ */
5040
+ function subtitles(state, options) {
5041
+ const from = Math.max(0, options.from ?? 0);
5042
+ const to = Math.min(state.composition.duration, options.to ?? state.composition.duration);
5043
+ const lines = [];
5044
+ for (const p of state.items.values()) {
5045
+ if (p.kind !== "caption" || state.items.get(p.parent ?? "")?.kind !== "captions") continue;
5046
+ const start = Math.max(p.start, from);
5047
+ const end = Math.min(p.end, to);
5048
+ const text = String(p.props.text ?? "").trim();
5049
+ if (end <= start || !text) continue;
5050
+ lines.push({
5051
+ start: start - from,
5052
+ end: end - from,
5053
+ text
5054
+ });
5055
+ }
5056
+ lines.sort((a, b) => a.start - b.start || a.end - b.end);
5057
+ if (options.format === "vtt") return `WEBVTT\n\n${lines.map((l) => `${stamp(l.start, ".")} --> ${stamp(l.end, ".")}\n${l.text}\n`).join("\n")}`;
5058
+ return lines.map((l, n) => `${n + 1}\n${stamp(l.start, ",")} --> ${stamp(l.end, ",")}\n${l.text}\n`).join("\n");
5059
+ }
5060
+ /** `HH:MM:SS,mmm` (SRT) or `HH:MM:SS.mmm` (WebVTT). */
5061
+ function stamp(seconds, mark) {
5062
+ const total = Math.round(seconds * 1e3);
5063
+ const h = Math.floor(total / 36e5);
5064
+ const m = Math.floor(total / 6e4) % 60;
5065
+ const s = Math.floor(total / 1e3) % 60;
5066
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
5067
+ return `${pad(h)}:${pad(m)}:${pad(s)}${mark}${pad(total % 1e3, 3)}`;
4906
5068
  }
4907
5069
  // src/index.ts
4908
5070
  var src_exports = /* @__PURE__ */ __exportAll({
4909
5071
  ANIMATION_UNITS: () => ANIMATION_UNITS,
4910
- AUDIO_EFFECT_TYPES: () => AUDIO_EFFECT_TYPES,
4911
5072
  AUTOMATION_LANES: () => AUTOMATION_LANES,
4912
5073
  AUTO_GROUP_TYPES: () => AUTO_GROUP_TYPES,
4913
5074
  ActionHistory: () => ActionHistory,
4914
5075
  Adjustment: () => Adjustment,
4915
5076
  Angle: () => Angle,
4916
5077
  Audio: () => Audio,
4917
- AudioEffect: () => AudioEffect,
4918
5078
  BLEND_MODES: () => BLEND_MODES,
4919
5079
  BUNDLED_FONTS: () => BUNDLED_FONTS,
4920
5080
  BUNDLED_FONT_WEIGHTS: () => BUNDLED_FONT_WEIGHTS,
@@ -4931,8 +5091,9 @@ var src_exports = /* @__PURE__ */ __exportAll({
4931
5091
  EASINGS: () => EASINGS,
4932
5092
  EASING_FN: () => EASING_FN,
4933
5093
  ELEMENTS: () => ELEMENTS,
4934
- EMPHASIS_ANIMATIONS: () => EMPHASIS_ANIMATIONS,
4935
5094
  EMPTY_MANIFEST: () => EMPTY_MANIFEST,
5095
+ ENGINE_LAYERS: () => ENGINE_LAYERS,
5096
+ ENGINE_SCHEMAS: () => ENGINE_SCHEMAS,
4936
5097
  Effect: () => Effect,
4937
5098
  Executor: () => Executor,
4938
5099
  FIT_MODES: () => FIT_MODES,
@@ -4942,7 +5103,6 @@ var src_exports = /* @__PURE__ */ __exportAll({
4942
5103
  Gap: () => Gap,
4943
5104
  Graphic: () => Graphic,
4944
5105
  INTERIOR_PREFIX: () => INTERIOR_PREFIX,
4945
- INTERPOLATION_QUALITIES: () => INTERPOLATION_QUALITIES,
4946
5106
  KEYFRAME_PROPERTIES: () => KEYFRAME_PROPERTIES,
4947
5107
  KEYFRAME_PROPERTY_PATTERN: () => KEYFRAME_PROPERTY_PATTERN,
4948
5108
  KIND_CAPABILITIES: () => KIND_CAPABILITIES,
@@ -4952,27 +5112,21 @@ var src_exports = /* @__PURE__ */ __exportAll({
4952
5112
  MASK_EASINGS: () => MASK_EASINGS,
4953
5113
  MASK_NOT_RENDERED: () => MASK_NOT_RENDERED,
4954
5114
  MASK_TYPES: () => MASK_TYPES,
4955
- MATTE_CHANNELS: () => MATTE_CHANNELS,
5115
+ MODULE_FONTS: () => MODULE_FONTS,
5116
+ MODULE_SHADERS: () => MODULE_SHADERS,
4956
5117
  MOTIONS: () => MOTIONS,
4957
5118
  MOTION_NAMES: () => MOTION_NAMES,
4958
5119
  Marker: () => Marker,
4959
5120
  Mask: () => Mask,
5121
+ Motion: () => Motion,
4960
5122
  Multicam: () => Multicam,
4961
5123
  NOT_RENDERED: () => NOT_RENDERED,
4962
5124
  Nested: () => Nested,
4963
5125
  NotImplemented: () => NotImplemented,
4964
5126
  PALETTE_KEYS: () => PALETTE_KEYS,
4965
- RENDERED_AUDIO_EFFECTS: () => RENDERED_AUDIO_EFFECTS,
4966
- RENDERED_CAPTION_ANIMATIONS: () => RENDERED_CAPTION_ANIMATIONS,
4967
- RENDERED_COLOR_GRADE: () => RENDERED_COLOR_GRADE,
4968
- RENDERED_EFFECTS: () => RENDERED_EFFECTS,
4969
- RENDERED_MASK_TYPES: () => RENDERED_MASK_TYPES,
4970
- RENDERED_TITLE_ANIMATIONS: () => RENDERED_TITLE_ANIMATIONS,
4971
- RENDERED_TRANSITIONS: () => RENDERED_TRANSITIONS,
4972
5127
  RENDERED_VALUES: () => RENDERED_VALUES,
4973
5128
  SCHEMAS_BY_KIND: () => SCHEMAS_BY_KIND,
4974
5129
  SHAPE_TYPES: () => SHAPE_TYPES,
4975
- STABILIZE_CROP_MODES: () => STABILIZE_CROP_MODES,
4976
5130
  STYLED_KINDS: () => STYLED_KINDS,
4977
5131
  SVG_COLOR_MODES: () => SVG_COLOR_MODES,
4978
5132
  Shape: () => Shape,
@@ -4992,6 +5146,7 @@ var src_exports = /* @__PURE__ */ __exportAll({
4992
5146
  TRACK_ROLES: () => TRACK_ROLES,
4993
5147
  TRANSITION_PARAMS: () => TRANSITION_PARAMS,
4994
5148
  TRANSITION_TYPES: () => TRANSITION_TYPES,
5149
+ Three: () => Three,
4995
5150
  Title: () => Title,
4996
5151
  Track: () => Track,
4997
5152
  Transition: () => Transition,
@@ -5004,22 +5159,32 @@ var src_exports = /* @__PURE__ */ __exportAll({
5004
5159
  childIndex: () => childIndex,
5005
5160
  collect: () => collect,
5006
5161
  collectErrors: () => collectErrors,
5162
+ collectFonts: () => collectFonts,
5007
5163
  collectModule: () => collectModule,
5164
+ collectShaders: () => collectShaders,
5008
5165
  createPatchLog: () => createPatchLog,
5009
5166
  cueNodes: () => cueNodes,
5010
5167
  defaultTransitionParams: () => defaultTransitionParams,
5011
5168
  define: () => define,
5169
+ defineEffect: () => defineEffect,
5012
5170
  defineElement: () => defineElement,
5171
+ defineTransition: () => defineTransition,
5013
5172
  describeAction: () => describeAction,
5173
+ engineOf: () => engineOf,
5014
5174
  findParentOf: () => findParentOf,
5175
+ font: () => font,
5176
+ fontsOf: () => fontsOf,
5015
5177
  freezeHoldsOf: () => freezeHoldsOf,
5016
5178
  freezeShift: () => freezeShift,
5017
5179
  freezeUnshift: () => freezeUnshift,
5018
5180
  getHandler: () => getHandler,
5181
+ googleFont: () => googleFont,
5019
5182
  graphicControls: () => graphicControls,
5183
+ holdLayerFrame: () => holdLayerFrame,
5020
5184
  holdTotal: () => holdTotal,
5021
5185
  insertNode: () => insertNode,
5022
5186
  installLayerClock: () => installLayerClock,
5187
+ installLayerEngine: () => installLayerEngine,
5023
5188
  interpolate: () => interpolate,
5024
5189
  invalid: () => invalid,
5025
5190
  inverseOf: () => inverseOf,
@@ -5031,9 +5196,12 @@ var src_exports = /* @__PURE__ */ __exportAll({
5031
5196
  isVocabulary: () => isVocabulary,
5032
5197
  kindOf: () => kindOf,
5033
5198
  lastChildId: () => lastChildId,
5199
+ layerEngine: () => layerEngine,
5200
+ layerFramesPending: () => layerFramesPending,
5034
5201
  listHandlers: () => listHandlers,
5035
5202
  lookupAsset: () => lookupAsset,
5036
5203
  makePropHandler: () => makePropHandler,
5204
+ motionBlurOf: () => motionBlurOf,
5037
5205
  motionStyleSchema: () => motionStyleSchema,
5038
5206
  nodeOfSpec: () => nodeOfSpec,
5039
5207
  nodeSpecOf: () => nodeSpecOf,
@@ -5041,6 +5209,7 @@ var src_exports = /* @__PURE__ */ __exportAll({
5041
5209
  ok: () => ok,
5042
5210
  onTimeline: () => onTimeline,
5043
5211
  parseTimeRef: () => parseTimeRef,
5212
+ parseTranscript: () => parseTranscript,
5044
5213
  placeNode: () => placeNode,
5045
5214
  placementOf: () => placementOf,
5046
5215
  placementProps: () => placementProps,
@@ -5066,12 +5235,16 @@ var src_exports = /* @__PURE__ */ __exportAll({
5066
5235
  sameAsset: () => sameAsset,
5067
5236
  samePlacement: () => samePlacement,
5068
5237
  setProps: () => setProps,
5238
+ shaderFor: () => shaderFor,
5239
+ shadersOf: () => shadersOf,
5240
+ shutterTimes: () => shutterTimes,
5069
5241
  siblingAfter: () => siblingAfter,
5070
5242
  speedKeysOf: () => speedKeysOf,
5071
5243
  spring: () => spring,
5072
5244
  springAt: () => springAt,
5073
5245
  stableJson: () => stableJson,
5074
5246
  stub: () => stub,
5247
+ subtitles: () => subtitles,
5075
5248
  targetIdOf: () => targetIdOf,
5076
5249
  themeSchema: () => themeSchema,
5077
5250
  themeToken: () => themeToken,
@@ -5080,13 +5253,12 @@ var src_exports = /* @__PURE__ */ __exportAll({
5080
5253
  uniqueId: () => uniqueId,
5081
5254
  useLayer: () => useLayer,
5082
5255
  useT: () => useT,
5256
+ usesShaders: () => usesShaders,
5083
5257
  validateTarget: () => validateTarget,
5084
5258
  walk: () => walk,
5085
- zAudioEffectType: () => zAudioEffectType,
5086
5259
  zBlendMode: () => zBlendMode,
5087
5260
  zCaptionAnimation: () => zCaptionAnimation,
5088
5261
  zEasing: () => zEasing,
5089
- zEmphasisAnimation: () => zEmphasisAnimation,
5090
5262
  zFitMode: () => zFitMode,
5091
5263
  zGraphicAnimation: () => zGraphicAnimation,
5092
5264
  zKeyframeProperty: () => zKeyframeProperty,
@@ -5098,4 +5270,4 @@ var src_exports = /* @__PURE__ */ __exportAll({
5098
5270
  zTransitionType: () => zTransitionType,
5099
5271
  zVideoEffectType: () => zVideoEffectType
5100
5272
  });
5101
- export { resolveTheme as $, MATTE_CHANNELS as $n, Marker as $t, inverseOf as A, RENDERED_COLOR_GRADE as An, kindOf as Ar, rampSpeed as At, ok as B, AUTOMATION_LANES as Bn, Caption as Bt, siblingAfter as C, THEMES as Cn, zTextAnimationPreset as Cr, EASING_FN as Ct, Executor as D, NOT_RENDERED as Dn, zVideoEffectType as Dr, holdTotal as Dt, validateTarget as E, MASK_NOT_RENDERED as En, zTransitionType as Er, freezeUnshift as Et, describeAction as F, RENDERED_VALUES as Fn, useT as Ft, stableJson as G, EMPHASIS_ANIMATIONS as Gn, Cut as Gt, createPatchLog as H, CAPTION_ANIMATIONS as Hn, Clip as Ht, targetIdOf as I, KIND_CAPABILITIES as In, Adjustment as It, isBundledFont as J, INTERPOLATION_QUALITIES as Jn, Freeze as Jt, BUNDLED_FONTS as K, FIT_MODES as Kn, ELEMENTS as Kt, invalid as L, capabilitiesOf as Ln, Angle as Lt, registerHandler as M, RENDERED_MASK_TYPES as Mn, springAt as Mt, AUTO_GROUP_TYPES as N, RENDERED_TITLE_ANIMATIONS as Nn, installLayerClock as Nt, ActionHistory as O, RENDERED_AUDIO_EFFECTS as On, defineElement as Or, rampAdvance as Ot, TARGET_PARAM_KEYS as P, RENDERED_TRANSITIONS as Pn, useLayer as Pt, applyTheme as Q, MASK_TYPES as Qn, Layer as Qt, nodeOfSpec as R, ANIMATION_UNITS as Rn, Audio as Rt, setProps as S, PALETTE_KEYS as Sn, zShapeType as Sr, spring as St, uniqueId as T, MASKED_KINDS as Tn, zTrackRole as Tr, freezeShift as Tt, nodesEqual as U, CAPTION_POSITIONS as Un, ColorGrade as Ut, asJson as V, BLEND_MODES as Vn, Captions as Vt, reduceInto as W, EASINGS as Wn, Composition as Wt, resolve as X, KEYFRAME_PROPERTY_PATTERN as Xn, Graphic as Xt, resolveScene as Y, KEYFRAME_PROPERTIES as Yn, Gap as Yt, samePlacement as Z, MASK_EASINGS as Zn, Keyframe as Zt, requireElement as _, tokenSchema as _n, zEmphasisAnimation as _r, EMPTY_MANIFEST as _t, childIndex as a, SpeedKey as an, TEXT_DECORATIONS as ar, playsKey as at, requireNotOpaque as b, MOTIONS as bn, zKeyframeProperty as br, interpolate as bt, insertNode as c, Title as cn, TRACK_LAYOUTS as cr, isResolveError as ct, makePropHandler as d, TRANSITION_PARAMS as dn, VIDEO_EFFECT_TYPES as dr, INTERIOR_PREFIX as dt, Mask as en, SHAPE_TYPES as er, themeToken as et, placeNode as f, defaultTransitionParams as fn, isKeyframeProperty as fr, findParentOf as ft, rawProp as g, themeSchema as gn, zEasing as gr, resolveTimeRef as gt, rawNode as h, motionStyleSchema as hn, zCaptionAnimation as hr, parseTimeRef as ht, childAt as i, Shape as in, TEXT_ANIMATION_PRESETS as ir, planCues as it, listHandlers as j, RENDERED_EFFECTS as jn, speedKeysOf as jt, getHandler as k, RENDERED_CAPTION_ANIMATIONS as kn, isVocabulary as kr, rampInverse as kt, isError as l, Track as ln, TRACK_ROLES as lr, collect as lt, placementProps as m, TOKEN_PATTERN as mn, zBlendMode as mr, isTimeRefString as mt, src_exports as n, Nested as nn, SVG_COLOR_MODES as nr, cueNodes as nt, collectErrors as o, Sticker as on, TEXT_VALIGNS as or, sameAsset as ot, placementOf as p, STYLED_KINDS as pn, zAudioEffectType as pr, walk as pt, BUNDLED_FONT_WEIGHTS as q, GRAPHIC_ANIMATIONS as qn, Effect as qt, NotImplemented as r, SCHEMAS_BY_KIND as rn, TEXT_ALIGNS as rr, onTimeline as rt, define as s, Svg as sn, TRACK_FLAGS as sr, timelineWords as st, graphicControls as t, Multicam as tn, STABILIZE_CROP_MODES as tr, DEFAULT_CUE_OPTIONS as tt, lastChildId as u, Transition as un, TRANSITION_TYPES as ur, collectModule as ut, requireLayout as v, DEFAULT_THEME as vn, zFitMode as vr, assetIdOf as vt, stub as w, THEME_NAMES as wn, zTrackLayout as wr, freezeHoldsOf as wt, requireUnlocked as x, MOTION_NAMES as xn, zMaskEasing as xr, random$1 as xt, requireLiteralProp as y, FONT_KEYS as yn, zGraphicAnimation as yr, lookupAsset as yt, nodeSpecOf as z, AUDIO_EFFECT_TYPES as zn, AudioEffect as zt };
5273
+ export { siblingAfter as $, EASINGS as $n, shadersOf as $t, interpolate as A, Title as An, zVideoEffectType as Ar, freezeHoldsOf as At, lastChildId as B, tokenSchema as Bn, stableJson as Br, resolveTheme as Bt, RENDERED_VALUES as C, Nested as Cn, zKeyframeProperty as Cr, cueNodes as Ct, installLayerEngine as D, Sticker as Dn, zTrackLayout as Dr, sameAsset as Dt, holdLayerFrame as E, SpeedKey as En, zTextAnimationPreset as Er, playsKey as Et, childIndex as F, defaultTransitionParams as Fn, capabilitiesOf as Fr, rampInverse as Ft, rawNode as G, PALETTE_KEYS as Gn, isTimeRefString as Gt, placeNode as H, FONT_KEYS as Hn, EMPTY_MANIFEST as Ht, collectErrors as I, STYLED_KINDS as In, asJson as Ir, rampSpeed as It, requireLayout as J, ANIMATION_UNITS as Jn, MODULE_SHADERS as Jt, rawProp as K, THEMES as Kn, parseTimeRef as Kt, define as L, TOKEN_PATTERN as Ln, createPatchLog as Lr, speedKeysOf as Lt, spring as M, Transition as Mn, isVocabulary as Mr, freezeUnshift as Mt, NotImplemented as N, engineOf as Nn, kindOf as Nr, holdTotal as Nt, layerEngine as O, Svg as On, zTrackRole as Or, timelineWords as Ot, childAt as P, TRANSITION_PARAMS as Pn, KIND_CAPABILITIES as Pr, rampAdvance as Pt, setProps as Q, CAPTION_POSITIONS as Qn, shaderFor as Qt, insertNode as R, motionStyleSchema as Rn, nodesEqual as Rr, springAt as Rt, NOT_RENDERED as S, Multicam as Sn, zGraphicAnimation as Sr, DEFAULT_CUE_OPTIONS as St, shutterTimes as T, Shape as Tn, zShapeType as Tr, planCues as Tt, placementOf as U, MOTIONS as Un, assetIdOf as Ut, makePropHandler as V, DEFAULT_THEME as Vn, themeToken as Vt, placementProps as W, MOTION_NAMES as Wn, lookupAsset as Wt, requireNotOpaque as X, BLEND_MODES as Xn, defineEffect as Xt, requireLiteralProp as Y, AUTOMATION_LANES as Yn, collectShaders as Yt, requireUnlocked as Z, CAPTION_ANIMATIONS as Zn, defineTransition as Zt, installLayerClock as _, Keyframe as _n, isKeyframeProperty as _r, resolve as _t, isResolveError as a, Captions as an, MASK_TYPES as ar, inverseOf as at, MASKED_KINDS as b, Mask as bn, zEasing as br, findParentOf as bt, BUNDLED_FONTS as c, Composition as cn, TEXT_ALIGNS as cr, ActionHistory as ct, collectFonts as d, ENGINE_LAYERS as dn, TEXT_VALIGNS as dr, describeAction as dt, usesShaders as en, FIT_MODES as er, stub as et, font as f, ENGINE_SCHEMAS as fn, TRACK_FLAGS as fr, targetIdOf as ft, parseTranscript as g, Graphic as gn, VIDEO_EFFECT_TYPES as gr, ok as gt, isBundledFont as h, Gap as hn, TRANSITION_TYPES as hr, nodeSpecOf as ht, resolveScene as i, Caption as in, MASK_EASINGS as ir, getHandler as it, random$1 as j, Track as jn, defineElement as jr, freezeShift as jt, layerFramesPending as k, Three as kn, zTransitionType as kr, EASING_FN as kt, BUNDLED_FONT_WEIGHTS as l, Cut as ln, TEXT_ANIMATION_PRESETS as lr, AUTO_GROUP_TYPES as lt, googleFont as m, Freeze as mn, TRACK_ROLES as mr, nodeOfSpec as mt, src_exports as n, Angle as nn, KEYFRAME_PROPERTIES as nr, validateTarget as nt, collect as o, Clip as on, SHAPE_TYPES as or, listHandlers as ot, fontsOf as p, Effect as pn, TRACK_LAYOUTS as pr, invalid as pt, requireElement as q, THEME_NAMES as qn, resolveTimeRef as qt, subtitles as r, Audio as rn, KEYFRAME_PROPERTY_PATTERN as rr, Executor as rt, collectModule as s, ColorGrade as sn, SVG_COLOR_MODES as sr, registerHandler as st, graphicControls as t, Adjustment as tn, GRAPHIC_ANIMATIONS as tr, uniqueId as tt, MODULE_FONTS as u, ELEMENTS as un, TEXT_DECORATIONS as ur, TARGET_PARAM_KEYS as ut, useLayer as v, Layer as vn, zBlendMode as vr, samePlacement as vt, motionBlurOf as w, SCHEMAS_BY_KIND as wn, zMaskEasing as wr, onTimeline as wt, MASK_NOT_RENDERED as x, Motion as xn, zFitMode as xr, walk as xt, useT as y, Marker as yn, zCaptionAnimation as yr, INTERIOR_PREFIX as yt, isError as z, themeSchema as zn, reduceInto as zr, applyTheme as zt };