@pixerate/editor 0.160.0 → 0.162.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.
Files changed (146) hide show
  1. package/dist/agent/index.cjs +36 -35
  2. package/dist/agent/index.js +7 -6
  3. package/dist/animation/index.cjs +5 -4
  4. package/dist/animation/index.js +4 -3
  5. package/dist/animation-editor/index.cjs +62 -61
  6. package/dist/animation-editor/index.js +6 -5
  7. package/dist/canvas/index.cjs +33 -32
  8. package/dist/canvas/index.js +6 -5
  9. package/dist/chart/index.cjs +88 -87
  10. package/dist/chart/index.js +7 -6
  11. package/dist/{chunk-QMI5HZWA.cjs → chunk-236TKNUR.cjs} +3 -3
  12. package/dist/{chunk-6UPGM3JB.js → chunk-2TKHLX6H.js} +1 -1
  13. package/dist/{chunk-3YDQLQH2.cjs → chunk-32MMQYCQ.cjs} +81 -81
  14. package/dist/{chunk-MHEXLG3M.cjs → chunk-3GJH2MGH.cjs} +37 -37
  15. package/dist/{chunk-AKDWAWSY.cjs → chunk-3IHNXRUV.cjs} +45 -45
  16. package/dist/{chunk-VKF3PWKR.cjs → chunk-3OSXHLIT.cjs} +259 -28
  17. package/dist/{chunk-TVO6W2Q6.cjs → chunk-4NGVHHJB.cjs} +17 -17
  18. package/dist/chunk-4VU4CP7R.js +769 -0
  19. package/dist/chunk-6DM3PUQD.cjs +767 -0
  20. package/dist/{chunk-DBIVICAO.js → chunk-7CKPUUN4.js} +1 -1
  21. package/dist/{chunk-57JLBGJ5.cjs → chunk-7YBDRJXT.cjs} +8 -8
  22. package/dist/{chunk-KY3FYKP3.js → chunk-BQWGN5WH.js} +1 -1
  23. package/dist/{chunk-LEUZ4FRC.cjs → chunk-CIPIESX4.cjs} +3 -3
  24. package/dist/chunk-CJE5MOIS.js +347 -0
  25. package/dist/{chunk-5QBRAIVK.js → chunk-CX6JLDFC.js} +3 -3
  26. package/dist/{chunk-R3QY7PPL.js → chunk-DTSTNRPH.js} +1 -1
  27. package/dist/{chunk-6LUYFHHN.cjs → chunk-EQ46F75O.cjs} +51 -51
  28. package/dist/{chunk-UKCJ43YL.cjs → chunk-ES3UEVUT.cjs} +130 -898
  29. package/dist/{chunk-UM3RPYC5.js → chunk-EVPZ5IKN.js} +1 -1
  30. package/dist/{chunk-XNQ5EFB4.cjs → chunk-EY22AY6R.cjs} +16 -16
  31. package/dist/chunk-FKUKJ3XA.cjs +347 -0
  32. package/dist/{chunk-YABNLNY7.js → chunk-GLMP22FT.js} +3 -3
  33. package/dist/{chunk-SUF6WRXR.cjs → chunk-GTELFJRE.cjs} +2 -2
  34. package/dist/{chunk-72BGL3I2.js → chunk-GWV4T5NZ.js} +1 -1
  35. package/dist/{chunk-AY65VQSU.js → chunk-H3OEM3Q2.js} +1 -1
  36. package/dist/{chunk-4YWAEV4W.cjs → chunk-HDGBM7EA.cjs} +26 -26
  37. package/dist/{chunk-H5XX4KHB.js → chunk-HLOKB7WW.js} +5 -3
  38. package/dist/{chunk-WNAAWLZ5.js → chunk-KGTF55R3.js} +2 -2
  39. package/dist/{chunk-WPPJH6X4.cjs → chunk-LL3H5XUH.cjs} +6 -4
  40. package/dist/{chunk-4T44CJ5K.cjs → chunk-NA4UVVJ4.cjs} +15 -15
  41. package/dist/{chunk-Q6PA362P.js → chunk-NO5QQDHZ.js} +2 -2
  42. package/dist/{chunk-AA34BA7G.js → chunk-QYZGBVNC.js} +30 -798
  43. package/dist/chunk-S6X2QRVU.cjs +769 -0
  44. package/dist/{chunk-SSCL7QQ4.js → chunk-XDCFJEU7.js} +2 -2
  45. package/dist/{chunk-IQ6KKXOU.js → chunk-YN4AYYBJ.js} +243 -12
  46. package/dist/chunk-ZGOHKS3X.js +767 -0
  47. package/dist/color/index.cjs +6 -2
  48. package/dist/color/index.d.cts +1 -0
  49. package/dist/color/index.d.ts +1 -0
  50. package/dist/color/index.js +28 -24
  51. package/dist/color/suggest.d.cts +12 -0
  52. package/dist/color/suggest.d.ts +12 -0
  53. package/dist/effects/index.cjs +44 -771
  54. package/dist/effects/index.js +34 -761
  55. package/dist/effects/ui.d.cts +20 -3
  56. package/dist/effects/ui.d.ts +20 -3
  57. package/dist/export/index.cjs +6 -5
  58. package/dist/export/index.js +5 -4
  59. package/dist/extensions/index.cjs +9 -8
  60. package/dist/extensions/index.js +8 -7
  61. package/dist/gradient/index.cjs +28 -26
  62. package/dist/gradient/index.js +7 -5
  63. package/dist/image-editor/index.cjs +638 -637
  64. package/dist/image-editor/index.js +5 -4
  65. package/dist/index.cjs +15 -14
  66. package/dist/index.js +9 -8
  67. package/dist/paint-editor/index.cjs +17 -16
  68. package/dist/paint-editor/index.js +6 -5
  69. package/dist/typography/accessibility.d.cts +44 -0
  70. package/dist/typography/accessibility.d.ts +44 -0
  71. package/dist/typography/catalog.d.cts +98 -0
  72. package/dist/typography/catalog.d.ts +98 -0
  73. package/dist/typography/index.cjs +1099 -0
  74. package/dist/typography/index.d.cts +16 -0
  75. package/dist/typography/index.d.ts +16 -0
  76. package/dist/typography/index.js +1099 -0
  77. package/dist/typography/metrics.d.cts +31 -0
  78. package/dist/typography/metrics.d.ts +31 -0
  79. package/dist/typography/picker.d.cts +132 -0
  80. package/dist/typography/picker.d.ts +132 -0
  81. package/dist/typography/registry.d.cts +66 -0
  82. package/dist/typography/registry.d.ts +66 -0
  83. package/dist/typography/scale.d.cts +62 -0
  84. package/dist/typography/scale.d.ts +62 -0
  85. package/dist/typography/segment.d.cts +26 -0
  86. package/dist/typography/segment.d.ts +26 -0
  87. package/dist/typography/stack.d.cts +26 -0
  88. package/dist/typography/stack.d.ts +26 -0
  89. package/dist/typography/tools.d.cts +34 -0
  90. package/dist/typography/tools.d.ts +34 -0
  91. package/dist/typography/types.d.cts +96 -0
  92. package/dist/typography/types.d.ts +96 -0
  93. package/dist/typography/weight.d.cts +13 -0
  94. package/dist/typography/weight.d.ts +13 -0
  95. package/dist/vector/index.cjs +950 -949
  96. package/dist/vector/index.js +5 -4
  97. package/dist/video-editor/audio.d.cts +9 -0
  98. package/dist/video-editor/audio.d.ts +9 -0
  99. package/dist/video-editor/check.d.cts +1 -1
  100. package/dist/video-editor/check.d.ts +1 -1
  101. package/dist/video-editor/controller.d.cts +47 -0
  102. package/dist/video-editor/controller.d.ts +47 -0
  103. package/dist/video-editor/dialogue-tools.d.cts +19 -0
  104. package/dist/video-editor/dialogue-tools.d.ts +19 -0
  105. package/dist/video-editor/effect-authoring.d.cts +54 -0
  106. package/dist/video-editor/effect-authoring.d.ts +54 -0
  107. package/dist/video-editor/evaluate.d.cts +11 -0
  108. package/dist/video-editor/evaluate.d.ts +11 -0
  109. package/dist/video-editor/export.d.cts +7 -1
  110. package/dist/video-editor/export.d.ts +7 -1
  111. package/dist/video-editor/index.cjs +1857 -531
  112. package/dist/video-editor/index.d.cts +7 -0
  113. package/dist/video-editor/index.d.ts +7 -0
  114. package/dist/video-editor/index.js +1516 -190
  115. package/dist/video-editor/lut.d.cts +12 -2
  116. package/dist/video-editor/lut.d.ts +12 -2
  117. package/dist/video-editor/mask.d.cts +48 -0
  118. package/dist/video-editor/mask.d.ts +48 -0
  119. package/dist/video-editor/mix.d.cts +107 -0
  120. package/dist/video-editor/mix.d.ts +107 -0
  121. package/dist/video-editor/music-tools.d.cts +19 -0
  122. package/dist/video-editor/music-tools.d.ts +19 -0
  123. package/dist/video-editor/parse.d.cts +3 -1
  124. package/dist/video-editor/parse.d.ts +3 -1
  125. package/dist/video-editor/plan.d.cts +75 -0
  126. package/dist/video-editor/plan.d.ts +75 -0
  127. package/dist/video-editor/render.d.cts +1 -1
  128. package/dist/video-editor/render.d.ts +1 -1
  129. package/dist/video-editor/timeline.d.cts +2 -0
  130. package/dist/video-editor/timeline.d.ts +2 -0
  131. package/dist/video-editor/tools.d.cts +90 -0
  132. package/dist/video-editor/tools.d.ts +90 -0
  133. package/dist/video-editor/transcript-controller.d.cts +106 -0
  134. package/dist/video-editor/transcript-controller.d.ts +106 -0
  135. package/dist/video-editor/transcript.d.cts +40 -0
  136. package/dist/video-editor/transcript.d.ts +40 -0
  137. package/dist/video-editor/types.d.cts +62 -2
  138. package/dist/video-editor/types.d.ts +62 -2
  139. package/dist/video-editor/ui.d.cts +21 -1
  140. package/dist/video-editor/ui.d.ts +21 -1
  141. package/package.json +11 -1
  142. package/skills/README.md +3 -0
  143. package/skills/pixerate-bgm-alignment/SKILL.md +82 -0
  144. package/skills/pixerate-talking-head-edit/SKILL.md +80 -0
  145. package/skills/pixerate-typography/SKILL.md +91 -0
  146. package/skills/pixerate-video-editor/SKILL.md +40 -2
@@ -19,9 +19,11 @@ var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
19
19
  var videoTransitionAudioCurve = (t) => _nullishCoalesce(t.audioCurve, () => ( (t.type === "dip" ? "linear" : "equal-power")));
20
20
  var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
21
21
  var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
22
+ var VIDEO_MASK_SHAPES = ["rectangle", "ellipse", "linear", "polygon"];
22
23
  var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
24
+ var VIDEO_ADJUSTMENT_SCOPES = ["underneath", "all", "track"];
23
25
  var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
24
- var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence", "effect"];
26
+ var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence", "effect", "adjustment"];
25
27
 
26
28
  // src/video-editor/keyframes.ts
27
29
  var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
@@ -318,9 +320,25 @@ function applyVideoLut(lut, rgb, out = [0, 0, 0]) {
318
320
  for (let k = 0; k < 3; k++) out[k] = clamp01(w0 * data[o + k] + w1 * data[o + a + k] + w2 * data[o + c + k] + w3 * data[o + d + k]);
319
321
  return out;
320
322
  }
321
- function applyVideoLutToPixels(lut, data, strength = 1) {
323
+ function videoSkinToneWeight(r, g, b) {
324
+ const R = r * 255;
325
+ const G = g * 255;
326
+ const B = b * 255;
327
+ const y = 0.299 * R + 0.587 * G + 0.114 * B;
328
+ if (y < 20 || y > 250) return 0;
329
+ const cb = 128 - 0.168736 * R - 0.331264 * G + 0.5 * B;
330
+ const cr = 128 + 0.5 * R - 0.418688 * G - 0.081312 * B;
331
+ const d = Math.hypot((cb - 108) / 20, (cr - 154) / 18);
332
+ let w = d <= 1 ? 1 : d >= 1.5 ? 0 : 1 - smooth((d - 1) / 0.5);
333
+ if (y < 40) w *= (y - 20) / 20;
334
+ else if (y > 235) w *= (250 - y) / 15;
335
+ return w;
336
+ }
337
+ var smooth = (t) => t * t * (3 - 2 * t);
338
+ function applyVideoLutToPixels(lut, data, strength = 1, preserveSkinTones = 0) {
322
339
  const s = clamp01(strength);
323
340
  if (!s) return;
341
+ const skin = clamp01(preserveSkinTones);
324
342
  const rgb = [0, 0, 0];
325
343
  const out = [0, 0, 0];
326
344
  for (let i = 0; i < data.length; i += 4) {
@@ -329,7 +347,8 @@ function applyVideoLutToPixels(lut, data, strength = 1) {
329
347
  rgb[1] = data[i + 1] / 255;
330
348
  rgb[2] = data[i + 2] / 255;
331
349
  applyVideoLut(lut, rgb, out);
332
- for (let k = 0; k < 3; k++) data[i + k] = Math.round((rgb[k] + (out[k] - rgb[k]) * s) * 255);
350
+ const k = skin ? s * (1 - skin * videoSkinToneWeight(rgb[0], rgb[1], rgb[2])) : s;
351
+ for (let c = 0; c < 3; c++) data[i + c] = Math.round((rgb[c] + (out[c] - rgb[c]) * k) * 255);
333
352
  }
334
353
  }
335
354
  var approximations = /* @__PURE__ */ new WeakMap();
@@ -484,7 +503,8 @@ function normalizeVideoGradeLut(lut) {
484
503
  if (!lut || typeof lut.src !== "string" || !lut.src) return void 0;
485
504
  const strength = typeof lut.strength === "number" && Number.isFinite(lut.strength) ? Math.round(Math.min(1, Math.max(0, lut.strength)) * 1e3) / 1e3 : 1;
486
505
  if (!strength) return void 0;
487
- return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {} };
506
+ const skin = typeof lut.preserveSkinTones === "number" && Number.isFinite(lut.preserveSkinTones) ? Math.round(Math.min(1, Math.max(0, lut.preserveSkinTones)) * 1e3) / 1e3 : 0;
507
+ return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {}, ...skin ? { preserveSkinTones: skin } : {} };
488
508
  }
489
509
  function normalizeVideoGrade(grade) {
490
510
  if (!grade) return void 0;
@@ -572,7 +592,7 @@ function applyVideoGrade(grade, rgb, lut) {
572
592
  const setting = normalizeVideoGradeLut(_optionalChain([grade, 'optionalAccess', _3 => _3.lut]));
573
593
  if (setting && lut) {
574
594
  const out = applyVideoLut(lut, v);
575
- const s = _nullishCoalesce(setting.strength, () => ( 1));
595
+ const s = (_nullishCoalesce(setting.strength, () => ( 1))) * (1 - (_nullishCoalesce(setting.preserveSkinTones, () => ( 0))) * videoSkinToneWeight(v[0], v[1], v[2]));
576
596
  v = v.map((x, k) => x + (out[k] - x) * s);
577
597
  }
578
598
  return v.map((x) => Math.round(x * 255));
@@ -717,6 +737,7 @@ var videoSlowMotionRamp = (duration) => [
717
737
  // src/video-editor/timeline.ts
718
738
  var VIDEO_TIME_EPSILON = 0.5;
719
739
  var videoClipEnd = (clip) => clip.start + clip.duration;
740
+ var videoClipTypeLabel = (clip) => ({ text: "title", color: "colour", effect: "effect clip", adjustment: "adjustment clip", sequence: "compound clip", media: "media clip" })[clip.type];
720
741
  var videoTrackSyncLocked = (track) => track.syncLocked !== false;
721
742
  var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.speed, () => ( 1)) : 1;
722
743
  function videoClipSourceRange(clip) {
@@ -966,6 +987,84 @@ function videoClipAnimated(clip, project) {
966
987
  return out;
967
988
  }
968
989
 
990
+ // src/video-editor/mask.ts
991
+ var VIDEO_MASK_DEFAULTS = { x: 0.5, y: 0.5, width: 0.5, height: 0.5, rotation: 0, feather: 0, cornerRadius: 0 };
992
+ var ELLIPSE_SEGMENTS = 64;
993
+ var CORNER_SEGMENTS = 8;
994
+ function normalizeVideoClipMask(mask) {
995
+ const d = VIDEO_MASK_DEFAULTS;
996
+ const finite = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
997
+ return {
998
+ shape: mask.shape,
999
+ x: finite(mask.x, d.x),
1000
+ y: finite(mask.y, d.y),
1001
+ width: Math.max(0, finite(mask.width, d.width)),
1002
+ height: Math.max(0, finite(mask.height, d.height)),
1003
+ rotation: finite(mask.rotation, d.rotation),
1004
+ feather: Math.max(0, finite(mask.feather, d.feather)),
1005
+ cornerRadius: Math.min(1, Math.max(0, finite(mask.cornerRadius, d.cornerRadius))),
1006
+ inverted: mask.inverted === true,
1007
+ ...mask.points ? { points: mask.points } : {}
1008
+ };
1009
+ }
1010
+ function videoMaskPolygon(mask, rect) {
1011
+ const m = normalizeVideoClipMask(mask);
1012
+ const cx = rect.x + m.x * rect.width;
1013
+ const cy = rect.y + m.y * rect.height;
1014
+ const a = m.rotation * Math.PI / 180;
1015
+ const cos = Math.cos(a);
1016
+ const sin = Math.sin(a);
1017
+ const turn = (dx, dy) => [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
1018
+ if (m.shape === "linear") {
1019
+ const R = 4 * (Math.hypot(rect.width, rect.height) + Math.hypot(cx - rect.x, cy - rect.y)) + 1;
1020
+ return [turn(-R, 0), turn(R, 0), turn(R, -R), turn(-R, -R)];
1021
+ }
1022
+ if (m.shape === "polygon") {
1023
+ return (_nullishCoalesce(m.points, () => ( []))).map((p) => turn((p.x - m.x) * rect.width, (p.y - m.y) * rect.height));
1024
+ }
1025
+ const hw = m.width * rect.width / 2;
1026
+ const hh = m.height * rect.height / 2;
1027
+ if (m.shape === "ellipse") {
1028
+ return Array.from({ length: ELLIPSE_SEGMENTS }, (_, i) => {
1029
+ const t = i / ELLIPSE_SEGMENTS * 2 * Math.PI;
1030
+ return turn(hw * Math.cos(t), hh * Math.sin(t));
1031
+ });
1032
+ }
1033
+ const r = m.cornerRadius * Math.min(hw, hh);
1034
+ if (r <= 0) return [turn(-hw, -hh), turn(hw, -hh), turn(hw, hh), turn(-hw, hh)];
1035
+ const corners = [
1036
+ [-hw + r, -hh + r, Math.PI],
1037
+ [hw - r, -hh + r, 1.5 * Math.PI],
1038
+ [hw - r, hh - r, 0],
1039
+ [-hw + r, hh - r, 0.5 * Math.PI]
1040
+ ];
1041
+ const out = [];
1042
+ for (const [x, y, start] of corners)
1043
+ for (let i = 0; i <= CORNER_SEGMENTS; i++) {
1044
+ const t = start + i / CORNER_SEGMENTS * (Math.PI / 2);
1045
+ out.push(turn(x + r * Math.cos(t), y + r * Math.sin(t)));
1046
+ }
1047
+ return out;
1048
+ }
1049
+ function videoLayerMask(mask, rect) {
1050
+ const m = normalizeVideoClipMask(mask);
1051
+ return { points: videoMaskPolygon(mask, rect), feather: m.feather, inverted: m.inverted };
1052
+ }
1053
+ function videoMaskSvgPath(mask) {
1054
+ const n2 = (v) => String(Math.round(v * 100) / 100);
1055
+ return mask.points.map(([x, y], i) => `${i ? "L" : "M"}${n2(x)} ${n2(y)}`).join("") + "Z";
1056
+ }
1057
+ function videoMaskContains(mask, x, y) {
1058
+ let inside = false;
1059
+ const p = mask.points;
1060
+ for (let i = 0, j = p.length - 1; i < p.length; j = i++) {
1061
+ const [xi, yi] = p[i];
1062
+ const [xj, yj] = p[j];
1063
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
1064
+ }
1065
+ return mask.inverted ? !inside : inside;
1066
+ }
1067
+
969
1068
  // src/video-editor/evaluate.ts
970
1069
  function videoEffectValuesAt(values, clipTime) {
971
1070
  const out = {};
@@ -1050,8 +1149,9 @@ function videoFrameAt(project, time) {
1050
1149
  }
1051
1150
  function tracksAt(project, tracks, time, scope) {
1052
1151
  const { assets, prefix } = scope;
1053
- const layers = [];
1152
+ let layers = [];
1054
1153
  const audio = [];
1154
+ const deferred = [];
1055
1155
  for (let ti = tracks.length - 1; ti >= 0; ti--) {
1056
1156
  const track = tracks[ti];
1057
1157
  for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
@@ -1081,6 +1181,7 @@ function tracksAt(project, tracks, time, scope) {
1081
1181
  }
1082
1182
  if (track.kind !== "video" || track.hidden) continue;
1083
1183
  if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
1184
+ if (clip.type === "adjustment" && !normalizeVideoGrade(clip.grade) && !clip.mask && !(_nullishCoalesce(clip.effects, () => ( []))).some((e) => e.enabled !== false)) continue;
1084
1185
  if (nested && !nested.layers.length) continue;
1085
1186
  const shown = videoClipAnimated(clip, project);
1086
1187
  let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
@@ -1124,6 +1225,22 @@ function tracksAt(project, tracks, time, scope) {
1124
1225
  if (clip.type === "effect") layer.generator = { effect: clip.effect, ...clip.name ? { name: clip.name } : {}, values: videoEffectValuesAt(clip.values, clipTime), time: Math.max(0, clipTime) };
1125
1226
  const effects = (_nullishCoalesce(clip.effects, () => ( []))).filter((e) => e.enabled !== false).map((e) => ({ id: e.id, effect: e.effect, ...e.name ? { name: e.name } : {}, opacity: clamp012(videoValueAt(e.opacity, clipTime, 1)), values: videoEffectValuesAt(e.values, clipTime), time: clipTime })).filter((e) => e.opacity > 0);
1126
1227
  if (effects.length) layer.effects = effects;
1228
+ if (clip.type === "adjustment") {
1229
+ layer.rect = { x: 0, y: 0, width: project.width, height: project.height };
1230
+ layer.rotation = 0;
1231
+ const adjScope = _nullishCoalesce(clip.scope, () => ( "underneath"));
1232
+ layer.adjustment = { scope: adjScope, ...clip.targetTrack ? { track: clip.targetTrack } : {} };
1233
+ if (clip.mask) layer.mask = videoLayerMask(clip.mask, layer.rect);
1234
+ const keepBase = layer.opacity < 1 || !!layer.blend || !!layer.mask || !!layer.wipe || !!layer.iris;
1235
+ if (layer.opacity <= 0) continue;
1236
+ if (adjScope === "underneath") {
1237
+ if (!layers.length) continue;
1238
+ layer.layers = keepBase ? [...layers] : layers.splice(0);
1239
+ layers.push(layer);
1240
+ } else deferred.push({ layer, keepBase });
1241
+ continue;
1242
+ }
1243
+ if (clip.mask) layer.mask = videoLayerMask(clip.mask, layer.rect);
1127
1244
  if (effectIn) {
1128
1245
  const before = videoClipBefore(track, clip);
1129
1246
  const i = before ? layers.findIndex((l) => l.clip === prefix + before.id && l.track === track.id) : -1;
@@ -1135,6 +1252,22 @@ function tracksAt(project, tracks, time, scope) {
1135
1252
  if (dip && dip.opacity > 0) layers.push({ clip: `transition:${prefix}${incoming ? clip.id : outgoing.next.id}`, track: track.id, type: "color", color: dip.color, opacity: clamp012(dip.opacity), rect: { x: 0, y: 0, width: project.width, height: project.height }, rotation: 0 });
1136
1253
  }
1137
1254
  }
1255
+ for (const { layer, keepBase } of deferred) {
1256
+ if (layer.adjustment.scope === "all") {
1257
+ if (!layers.length) continue;
1258
+ layer.layers = [...layers];
1259
+ layers = keepBase ? [...layers, layer] : [layer];
1260
+ continue;
1261
+ }
1262
+ const target = layer.adjustment.track;
1263
+ const first = layers.findIndex((l) => l.track === target);
1264
+ if (first < 0) continue;
1265
+ let last = first;
1266
+ while (last + 1 < layers.length && layers[last + 1].track === target) last++;
1267
+ const run = layers.slice(first, last + 1);
1268
+ layer.layers = run;
1269
+ layers = [...layers.slice(0, first), ...keepBase ? run : [], layer, ...layers.slice(last + 1)];
1270
+ }
1138
1271
  return { layers, audio };
1139
1272
  }
1140
1273
  function videoLayersWithoutTransitionEffects(layers) {
@@ -1166,7 +1299,7 @@ function videoClipEffectUses(clip) {
1166
1299
  }
1167
1300
  var videoLayersUseEffects = (layers) => layers.some((l) => !!(_optionalChain([l, 'access', _31 => _31.effects, 'optionalAccess', _32 => _32.length]) || l.generator || l.transitionEffect || l.layers && videoLayersUseEffects(l.layers)));
1168
1301
  var videoEffectFrameTime = (time, fps) => Math.floor(time * fps / 1e3 + 1e-6) * 1e3 / fps;
1169
- var PLACEMENT = /* @__PURE__ */ new Set(["opacity", "rect", "rotation", "blend", "wipe", "iris", "grade", "track"]);
1302
+ var PLACEMENT = /* @__PURE__ */ new Set(["opacity", "rect", "rotation", "blend", "wipe", "iris", "grade", "track", "mask"]);
1170
1303
  var round = (_k, v) => typeof v === "number" ? Math.round(v * 100) / 100 : v;
1171
1304
  function videoEffectLayerKey(layer, width, height) {
1172
1305
  const content = layer.transitionEffect ? layer : Object.fromEntries(Object.entries(layer).filter(([k]) => !PLACEMENT.has(k)));
@@ -1459,6 +1592,14 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
1459
1592
  inner = `<rect ${box} fill="#334155" stroke="#94a3b8" stroke-width="2"/><text x="${n(rect.x + rect.width / 2)}" y="${n(rect.y + rect.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="${n(size)}" fill="#e2e8f0">${esc(label)}</text>`;
1460
1593
  }
1461
1594
  }
1595
+ if (layer.mask) {
1596
+ const m = layer.mask;
1597
+ const blur = m.feather > 0 ? `<filter id="mf${i}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${n(m.feather / 2)}"/></filter>` : "";
1598
+ if (blur) defs.push(blur);
1599
+ const shape = `<path d="${videoMaskSvgPath(m)}" fill="${m.inverted ? "black" : "white"}"${blur ? ` filter="url(#mf${i})"` : ""}/>`;
1600
+ defs.push(`<mask id="m${i}" maskUnits="userSpaceOnUse" x="-100000" y="-100000" width="200000" height="200000">${m.inverted ? '<rect x="-100000" y="-100000" width="200000" height="200000" fill="white"/>' : ""}${shape}</mask>`);
1601
+ inner = `<g mask="url(#m${i})">${inner}</g>`;
1602
+ }
1462
1603
  let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
1463
1604
  if (layer.iris) {
1464
1605
  defs.push(`<clipPath id="i${i}"><circle cx="${n(frame.width / 2)}" cy="${n(frame.height / 2)}" r="${n(videoIrisRadius(frame, layer.iris.progress))}"/></clipPath>`);
@@ -1489,6 +1630,18 @@ function videoContactSheetSvg(project, times, options = {}) {
1489
1630
  });
1490
1631
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(W)} ${n(H)}" width="${n(W)}" height="${n(H)}"><rect width="100%" height="100%" fill="#f8fafc"/>${cells.join("")}</svg>`;
1491
1632
  }
1633
+ function applyVideoMask(g, mask, scale) {
1634
+ if (!g.moveTo || !g.lineTo || !g.closePath || !g.fill || mask.points.length < 3) return false;
1635
+ g.globalAlpha = 1;
1636
+ g.globalCompositeOperation = mask.inverted ? "destination-out" : "destination-in";
1637
+ g.filter = mask.feather > 0 ? `blur(${Math.max(0.5, mask.feather * scale / 2)}px)` : "none";
1638
+ g.fillStyle = "#000000";
1639
+ g.beginPath();
1640
+ mask.points.forEach(([x, y], i) => i ? g.lineTo(x, y) : g.moveTo(x, y));
1641
+ g.closePath();
1642
+ g.fill();
1643
+ return true;
1644
+ }
1492
1645
  function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
1493
1646
  const { rect } = layer;
1494
1647
  if (layer.iris) {
@@ -1696,10 +1849,10 @@ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth, s
1696
1849
  }
1697
1850
  const setting = _optionalChain([layer, 'access', _70 => _70.grade, 'optionalAccess', _71 => _71.lut]);
1698
1851
  const lut = setting ? _optionalChain([options, 'access', _72 => _72.lutFor, 'optionalCall', _73 => _73(setting.src)]) : null;
1699
- const scratch = lut ? _optionalChain([options, 'access', _74 => _74.scratch, 'optionalCall', _75 => _75(cw, ch, 2 * depth + 1)]) : null;
1852
+ const scratch = lut || layer.mask ? _optionalChain([options, 'access', _74 => _74.scratch, 'optionalCall', _75 => _75(cw, ch, 2 * depth + 1)]) : null;
1700
1853
  ctx.save();
1701
1854
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
1702
- if (lut && scratch) {
1855
+ if (scratch) {
1703
1856
  const s = scratch.ctx;
1704
1857
  s.save();
1705
1858
  s.setTransform(1, 0, 0, 1, 0, 0);
@@ -1709,9 +1862,23 @@ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth, s
1709
1862
  s.globalCompositeOperation = "source-over";
1710
1863
  paintVideoLayer(s, frame, layer, source, options);
1711
1864
  s.restore();
1712
- const img = s.getImageData(0, 0, cw, ch);
1713
- applyVideoLutToPixels(lut, img.data, _nullishCoalesce(setting.strength, () => ( 1)));
1714
- s.putImageData(img, 0, 0);
1865
+ if (lut) {
1866
+ const img = s.getImageData(0, 0, cw, ch);
1867
+ applyVideoLutToPixels(lut, img.data, _nullishCoalesce(setting.strength, () => ( 1)), _nullishCoalesce(setting.preserveSkinTones, () => ( 0)));
1868
+ s.putImageData(img, 0, 0);
1869
+ }
1870
+ if (layer.mask) {
1871
+ s.save();
1872
+ s.setTransform(scale, 0, 0, scale, 0, 0);
1873
+ const { rect } = layer;
1874
+ if (layer.rotation) {
1875
+ s.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
1876
+ s.rotate(layer.rotation * Math.PI / 180);
1877
+ s.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
1878
+ }
1879
+ applyVideoMask(s, layer.mask, scale);
1880
+ s.restore();
1881
+ }
1715
1882
  ctx.setTransform(1, 0, 0, 1, 0, 0);
1716
1883
  ctx.globalAlpha = layer.opacity;
1717
1884
  if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
@@ -1778,6 +1945,7 @@ function toVideoEdl(project, options = {}) {
1778
1945
  const sourceAt = (clip, time) => clip.type === "media" ? (_nullishCoalesce(clip.in, () => ( 0))) + (time - clip.start) * videoClipSpeed(clip) : time - clip.start;
1779
1946
  const row = (n2, reel, kind, srcIn, srcOut, recIn, recOut) => `${n2} ${reel.padEnd(8)} ${channel.padEnd(5)} ${kind.padEnd(8)} ${tc(srcIn)} ${tc(srcOut)} ${rtc(recIn)} ${rtc(recOut)}`;
1780
1947
  for (const clip of sortedVideoClips(track)) {
1948
+ if (clip.type === "adjustment") continue;
1781
1949
  const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
1782
1950
  const reel = clip.type === "media" ? reelName(asset) : "BL";
1783
1951
  const end = videoClipEnd(clip);
@@ -1810,6 +1978,27 @@ function toVideoEdl(project, options = {}) {
1810
1978
  function videoFfmpegUnsupportedEffects(project) {
1811
1979
  return allVideoTracks(project).filter(({ track }) => track.kind === "video" && !track.hidden).flatMap(({ track }) => track.clips.flatMap(videoClipEffectUses)).filter((u) => u.enabled);
1812
1980
  }
1981
+ function videoFfmpegUnsupportedFeatures(project) {
1982
+ const out = [];
1983
+ for (const { track } of allVideoTracks(project)) {
1984
+ if (track.kind !== "video" || track.hidden) continue;
1985
+ for (const clip of track.clips) {
1986
+ if (clip.mask) out.push(`clip "${clip.id}" has a mask`);
1987
+ if (clip.type !== "adjustment") continue;
1988
+ const why = [];
1989
+ if ((_nullishCoalesce(clip.scope, () => ( "underneath"))) === "track") why.push("changes one track only");
1990
+ if (clip.opacity !== void 0 && clip.opacity !== 1) why.push("is not at full opacity");
1991
+ if (clip.fadeIn || clip.fadeOut) why.push("fades");
1992
+ if (clip.blend && clip.blend !== "normal") why.push("blends");
1993
+ if (clip.transition) why.push("has a transition");
1994
+ if (clip.animateIn || clip.animateOut) why.push("is animated");
1995
+ const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _96 => _96.grade, 'optionalAccess', _97 => _97.lut]));
1996
+ if (lut && (_nullishCoalesce(lut.strength, () => ( 1))) < 1) why.push("mixes its LUT at part strength");
1997
+ if (why.length) out.push(`adjustment clip "${clip.id}" ${why.join(", ")}`);
1998
+ }
1999
+ }
2000
+ return out;
2001
+ }
1813
2002
  function loudnormFilter(target = {}, measure = false) {
1814
2003
  const I = _nullishCoalesce(target.integrated, () => ( -14));
1815
2004
  const TP = _nullishCoalesce(target.truePeak, () => ( -1));
@@ -1902,6 +2091,13 @@ function buildFfmpeg(project, options, measure) {
1902
2091
  );
1903
2092
  }
1904
2093
  }
2094
+ if (!measure && !frames) {
2095
+ const features = videoFfmpegUnsupportedFeatures(project);
2096
+ if (features.length)
2097
+ throw new Error(
2098
+ `ffmpeg's filters cannot draw ${features.slice(0, 5).join("; ")}${features.length > 5 ? "; ..." : ""}. Render the picture frame by frame with renderVideoFrameExact (the preview's drawing) to PNGs or raw RGBA and pass them as frames: { input: "frames/%06d.png" }; ffmpeg still mixes the sound. Or remove the mask, or keep adjustment clips to a full-opacity grade over what is underneath or the whole frame.`
2099
+ );
2100
+ }
1905
2101
  if (frames) {
1906
2102
  if (frames.format === "rawvideo") inputs.push(["-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${W}x${H}`, "-framerate", String(fps), "-i", frames.input]);
1907
2103
  else inputs.push(["-framerate", String(fps), "-i", frames.input]);
@@ -1981,11 +2177,33 @@ function buildFfmpeg(project, options, measure) {
1981
2177
  };
1982
2178
  let ak = 0;
1983
2179
  let q = 0;
2180
+ const adjust = (cur2, clip) => {
2181
+ const enable = `enable='between(t,${sec(clip.start)},${sec(videoClipEnd(clip))})'`;
2182
+ const steps = videoGradeFfmpegFilters(clip.grade).map((f) => `${f}:${enable}`);
2183
+ const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _98 => _98.grade, 'optionalAccess', _99 => _99.lut]));
2184
+ if (lut) {
2185
+ const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
2186
+ if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and adjustment clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
2187
+ steps.push("format=gbrap", `lut3d=file=${ffmpegFilterValue(path)}:interp=tetrahedral:${enable}`);
2188
+ if (lut.preserveSkinTones) notes.push(`Adjustment clip "${clip.id}"'s LUT protects skin tones in the preview, which ffmpeg's lut3d cannot; render frames with renderVideoFrameExact to keep it.`);
2189
+ }
2190
+ if (!steps.length) return cur2;
2191
+ const next = `adj${k++}`;
2192
+ graph.push(`[${cur2}]format=rgba,${steps.join(",")},format=yuva420p[${next}]`);
2193
+ return next;
2194
+ };
1984
2195
  const compose = (tracks, base, length, scope) => {
1985
2196
  let cur2 = base;
1986
2197
  const sounds = [];
2198
+ const adjustAll = [];
1987
2199
  for (const track of [...tracks].reverse()) {
1988
2200
  for (const raw of sortedVideoClips(track)) {
2201
+ if (raw.type === "adjustment") {
2202
+ if (!scope.picture || track.kind !== "video" || track.hidden) continue;
2203
+ if ((_nullishCoalesce(raw.scope, () => ( "underneath"))) === "all") adjustAll.push(raw);
2204
+ else if (cur2) cur2 = adjust(cur2, raw);
2205
+ continue;
2206
+ }
1989
2207
  const clip = videoClipAnimated(raw, project);
1990
2208
  const { lead, tail } = videoClipOverhang(track, clip);
1991
2209
  const t0 = clip.start - lead;
@@ -2024,13 +2242,13 @@ function buildFfmpeg(project, options, measure) {
2024
2242
  }
2025
2243
  let index;
2026
2244
  if (shows && asset && asset.kind !== "audio") index = input(clip, asset, lead, tail);
2027
- const audible = clip.type === "sequence" ? !!_optionalChain([compound, 'optionalAccess', _96 => _96.sound]) : videoAssetHasSound(asset);
2245
+ const audible = clip.type === "sequence" ? !!_optionalChain([compound, 'optionalAccess', _100 => _100.sound]) : videoAssetHasSound(asset);
2028
2246
  if (scope.sound && audible && (clip.type === "media" || clip.type === "sequence") && !clip.muted && !track.muted) {
2029
2247
  const trackGain = _nullishCoalesce(track.volume, () => ( 1));
2030
2248
  const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
2031
2249
  const ramp = clip.type === "media" ? clip.timeRemap : void 0;
2032
2250
  const ai = compound ? -1 : _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
2033
- let source2 = _nullishCoalesce(_optionalChain([compound, 'optionalAccess', _97 => _97.sound]), () => ( `[${ai}:a]`));
2251
+ let source2 = _nullishCoalesce(_optionalChain([compound, 'optionalAccess', _101 => _101.sound]), () => ( `[${ai}:a]`));
2034
2252
  if (ramp) {
2035
2253
  source2 = `[r${ak}]`;
2036
2254
  graph.push(...rampedSound(ramp, `${ai}:a`, source2, lead, clip.duration + tail));
@@ -2044,7 +2262,7 @@ function buildFfmpeg(project, options, measure) {
2044
2262
  if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
2045
2263
  if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
2046
2264
  if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
2047
- if (_optionalChain([next2, 'optionalAccess', _98 => _98.transition, 'access', _99 => _99.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
2265
+ if (_optionalChain([next2, 'optionalAccess', _102 => _102.transition, 'access', _103 => _103.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
2048
2266
  if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
2049
2267
  if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
2050
2268
  chain2.push(`adelay=${Math.round(t0)}:all=1`);
@@ -2057,7 +2275,7 @@ function buildFfmpeg(project, options, measure) {
2057
2275
  const onTimeline = `(t*1000-${Math.round(clip.start)})`;
2058
2276
  const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
2059
2277
  const tf = clip.transform;
2060
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _100 => _100[p]])));
2278
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _104 => _104[p]])));
2061
2279
  if (clip.type === "text" && options.titleImage) {
2062
2280
  inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(moving ? { ...clip, transform: void 0 } : clip, { width: project.width, height: project.height })]);
2063
2281
  index = inputs.length - 1;
@@ -2067,7 +2285,7 @@ function buildFfmpeg(project, options, measure) {
2067
2285
  const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
2068
2286
  const alpha = [op, clip.fadeIn ? `min(1,(t-${sec(clip.start)})/${sec(clip.fadeIn)})` : "", clip.fadeOut ? `min(1,(${sec(end)}-t)/${sec(clip.fadeOut)})` : ""].filter(Boolean).join("*");
2069
2287
  const coord = (axis) => {
2070
- const v = _optionalChain([tf, 'optionalAccess', _101 => _101[axis]]);
2288
+ const v = _optionalChain([tf, 'optionalAccess', _105 => _105[axis]]);
2071
2289
  const base2 = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
2072
2290
  return isVideoKeyframes(v) ? `${axis}='${base2}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base2}+${_nullishCoalesce(v, () => ( 0))}`;
2073
2291
  };
@@ -2075,7 +2293,7 @@ function buildFfmpeg(project, options, measure) {
2075
2293
  const opts = [
2076
2294
  `text=${ffmpegFilterValue(clip.text)}`,
2077
2295
  options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
2078
- isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _102 => _102.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _103 => _103.scale]), () => ( 1))))}`,
2296
+ isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _106 => _106.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _107 => _107.scale]), () => ( 1))))}`,
2079
2297
  `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
2080
2298
  coord("x"),
2081
2299
  coord("y"),
@@ -2083,8 +2301,8 @@ function buildFfmpeg(project, options, measure) {
2083
2301
  ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
2084
2302
  `enable='between(t,${sec(clip.start)},${sec(end)})'`
2085
2303
  ];
2086
- if (_optionalChain([tf, 'optionalAccess', _104 => _104.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2087
- if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _105 => _105.grade, 'optionalAccess', _106 => _106.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
2304
+ if (_optionalChain([tf, 'optionalAccess', _108 => _108.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2305
+ if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _109 => _109.grade, 'optionalAccess', _110 => _110.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
2088
2306
  if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
2089
2307
  graph.push(`[${cur2}]drawtext=${opts.join(":")}[${next}]`);
2090
2308
  cur2 = next;
@@ -2097,7 +2315,7 @@ function buildFfmpeg(project, options, measure) {
2097
2315
  const chain = [];
2098
2316
  let source;
2099
2317
  if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
2100
- else if (_optionalChain([compound, 'optionalAccess', _107 => _107.picture])) {
2318
+ else if (_optionalChain([compound, 'optionalAccess', _111 => _111.picture])) {
2101
2319
  source = compound.picture;
2102
2320
  chain.push(`scale=${even(rect.width)}:${even(rect.height)}`);
2103
2321
  } else {
@@ -2107,7 +2325,7 @@ function buildFfmpeg(project, options, measure) {
2107
2325
  }
2108
2326
  const grade = videoGradeFfmpegFilters(clip.grade);
2109
2327
  if (grade.length) chain.push("format=rgba", ...grade);
2110
- const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _108 => _108.grade, 'optionalAccess', _109 => _109.lut]));
2328
+ const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _112 => _112.grade, 'optionalAccess', _113 => _113.lut]));
2111
2329
  if (lut) {
2112
2330
  chain.push("format=gbrap");
2113
2331
  const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
@@ -2116,6 +2334,7 @@ function buildFfmpeg(project, options, measure) {
2116
2334
  const into2 = `q${next}`;
2117
2335
  graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
2118
2336
  const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
2337
+ if (lut.preserveSkinTones) notes.push(`Clip "${clip.id}"'s LUT protects skin tones in the preview, which ffmpeg's lut3d cannot; render frames with renderVideoFrameExact to keep it.`);
2119
2338
  if ((_nullishCoalesce(lut.strength, () => ( 1))) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
2120
2339
  else graph.push(`[${into2}]split[o${next}][p${next}]`, `[p${next}]${lut3d}[s${next}]`, `[s${next}][o${next}]blend=all_mode=normal:all_opacity=${lut.strength}[r${next}]`);
2121
2340
  source = `[r${next}]`;
@@ -2138,10 +2357,10 @@ function buildFfmpeg(project, options, measure) {
2138
2357
  const r = videoIrisRadius({ width: W, height: H }, 1);
2139
2358
  chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
2140
2359
  }
2141
- const rotation = _optionalChain([placed, 'optionalAccess', _110 => _110.rotation]);
2360
+ const rotation = _optionalChain([placed, 'optionalAccess', _114 => _114.rotation]);
2142
2361
  if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
2143
2362
  else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
2144
- if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _111 => _111.scale]))) {
2363
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _115 => _115.scale]))) {
2145
2364
  const s = expr(placed.scale, onChain("t"), 1, "scale");
2146
2365
  chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
2147
2366
  }
@@ -2160,9 +2379,9 @@ function buildFfmpeg(project, options, measure) {
2160
2379
  const into = lead ? clip.transition.type : void 0;
2161
2380
  if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
2162
2381
  const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
2163
- if (_optionalChain([after, 'optionalAccess', _112 => _112.transition, 'access', _113 => _113.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2382
+ if (_optionalChain([after, 'optionalAccess', _116 => _116.transition, 'access', _117 => _117.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2164
2383
  const at = (axis, c) => {
2165
- const v = _optionalChain([placed, 'optionalAccess', _114 => _114[axis]]);
2384
+ const v = _optionalChain([placed, 'optionalAccess', _118 => _118[axis]]);
2166
2385
  const extra = shifts[axis].map((s) => `+${s}`).join("");
2167
2386
  return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
2168
2387
  };
@@ -2196,6 +2415,7 @@ function buildFfmpeg(project, options, measure) {
2196
2415
  }
2197
2416
  }
2198
2417
  }
2418
+ for (const clip of adjustAll) if (cur2) cur2 = adjust(cur2, clip);
2199
2419
  return { video: cur2, audio: sounds };
2200
2420
  };
2201
2421
  const top = compose(project.tracks, measure || frames ? null : "base", D, { within: [], picture: !measure && !frames, sound: true });
@@ -2209,7 +2429,7 @@ function buildFfmpeg(project, options, measure) {
2209
2429
  if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
2210
2430
  else if (options.loudness && !options.loudness.measured)
2211
2431
  notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
2212
- if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _115 => _115.get, 'call', _116 => _116(c.asset), 'optionalAccess', _117 => _117.kind]) === "video" && _optionalChain([assets, 'access', _118 => _118.get, 'call', _119 => _119(c.asset), 'optionalAccess', _120 => _120.hasAudio]) === void 0)))
2432
+ if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _119 => _119.get, 'call', _120 => _120(c.asset), 'optionalAccess', _121 => _121.kind]) === "video" && _optionalChain([assets, 'access', _122 => _122.get, 'call', _123 => _123(c.asset), 'optionalAccess', _124 => _124.hasAudio]) === void 0)))
2213
2433
  notes.push('Video assets are assumed to have sound; set "hasAudio": false on any that do not, or ffmpeg will stop at a missing audio stream.');
2214
2434
  if (animated.size)
2215
2435
  notes.push(`Keyframed ${[...animated].sort().join(", ")} ${animated.size > 1 ? "are" : "is"} rendered as straight lines between keys, with eased segments sampled every two frames${animated.has("scale") ? "; animated scale needs ffmpeg 5 or later (scale with eval=frame)" : ""}.`);
@@ -2379,4 +2599,15 @@ function buildFfmpeg(project, options, measure) {
2379
2599
 
2380
2600
 
2381
2601
 
2382
- exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.isVideoKeyframes = isVideoKeyframes; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoValueAt = videoValueAt; exports.videoKeysOf = videoKeysOf; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.splitVideoAnimatable = splitVideoAnimatable; exports.sortVideoKeys = sortVideoKeys; exports.setVideoKey = setVideoKey; exports.removeVideoKey = removeVideoKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.parseVideoCubeLut = parseVideoCubeLut; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutToCube = videoLutToCube; exports.applyVideoLut = applyVideoLut; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.VideoLutCache = VideoLutCache; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoGrade = normalizeVideoGrade; exports.videoGradePreset = videoGradePreset; exports.videoGradeLook = videoGradeLook; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.applyVideoGrade = applyVideoGrade; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.videoClipEnd = videoClipEnd; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSourceRange = videoClipSourceRange; exports.videoFrameDuration = videoFrameDuration; exports.snapToVideoFrame = snapToVideoFrame; exports.videoFrames = videoFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.videoTimecodeFrames = videoTimecodeFrames; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.videoHourFrames = videoHourFrames; exports.formatVideoSeconds = formatVideoSeconds; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetMap = videoAssetMap; exports.videoSequenceMap = videoSequenceMap; exports.videoSequenceDuration = videoSequenceDuration; exports.videoSequenceChildren = videoSequenceChildren; exports.findVideoSequenceCycle = findVideoSequenceCycle; exports.allVideoTracks = allVideoTracks; exports.sortedVideoClips = sortedVideoClips; exports.findVideoClip = findVideoClip; exports.allVideoClipIds = allVideoClipIds; exports.findVideoSequenceClip = findVideoSequenceClip; exports.videoProjectDuration = videoProjectDuration; exports.videoClipBefore = videoClipBefore; exports.videoClipAfter = videoClipAfter; exports.videoClipHandles = videoClipHandles; exports.videoClipOverhang = videoClipOverhang; exports.videoSnapPoints = videoSnapPoints; exports.snapVideoTime = snapVideoTime; exports.findVideoGaps = findVideoGaps; exports.videoOverlap = videoOverlap; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.videoClipsAt = videoClipsAt; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipAnimated = videoClipAnimated; exports.videoEffectValuesAt = videoEffectValuesAt; exports.videoTransformAt = videoTransformAt; exports.placeVideoRect = placeVideoRect; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoTransitionEase = videoTransitionEase; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoClipSoundGain = videoClipSoundGain; exports.videoFrameAt = videoFrameAt; exports.videoLayersWithoutTransitionEffects = videoLayersWithoutTransitionEffects; exports.videoFrameLeafLayers = videoFrameLeafLayers; exports.videoProjectEffects = videoProjectEffects; exports.videoClipEffectUses = videoClipEffectUses; exports.videoLayersUseEffects = videoLayersUseEffects; exports.videoEffectFrameTime = videoEffectFrameTime; exports.videoEffectLayerKey = videoEffectLayerKey; exports.mixEffectPixels = mixEffectPixels; exports.overEffectPixels = overEffectPixels; exports.VideoEffectsEngine = VideoEffectsEngine; exports.VIDEO_EFFECT_PLACES = VIDEO_EFFECT_PLACES; exports.checkVideoEffectPlace = checkVideoEffectPlace; exports.checkVideoEffectValues = checkVideoEffectValues; exports.videoIrisRadius = videoIrisRadius; exports.videoWipeRect = videoWipeRect; exports.videoFrameToSvg = videoFrameToSvg; exports.videoProjectSvg = videoProjectSvg; exports.videoContactSheetSvg = videoContactSheetSvg; exports.drawVideoFrame = drawVideoFrame; exports.renderVideoFrameExact = renderVideoFrameExact; exports.toVideoEdl = toVideoEdl; exports.videoFfmpegUnsupportedEffects = videoFfmpegUnsupportedEffects; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.ffmpegFilterValue = ffmpegFilterValue; exports.videoShellQuote = videoShellQuote; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure;
2602
+
2603
+
2604
+
2605
+
2606
+
2607
+
2608
+
2609
+
2610
+
2611
+
2612
+
2613
+ exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_MASK_SHAPES = VIDEO_MASK_SHAPES; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_ADJUSTMENT_SCOPES = VIDEO_ADJUSTMENT_SCOPES; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.isVideoKeyframes = isVideoKeyframes; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoValueAt = videoValueAt; exports.videoKeysOf = videoKeysOf; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.splitVideoAnimatable = splitVideoAnimatable; exports.sortVideoKeys = sortVideoKeys; exports.setVideoKey = setVideoKey; exports.removeVideoKey = removeVideoKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.parseVideoCubeLut = parseVideoCubeLut; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutToCube = videoLutToCube; exports.applyVideoLut = applyVideoLut; exports.videoSkinToneWeight = videoSkinToneWeight; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.VideoLutCache = VideoLutCache; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoGrade = normalizeVideoGrade; exports.videoGradePreset = videoGradePreset; exports.videoGradeLook = videoGradeLook; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.applyVideoGrade = applyVideoGrade; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.videoClipEnd = videoClipEnd; exports.videoClipTypeLabel = videoClipTypeLabel; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSourceRange = videoClipSourceRange; exports.videoFrameDuration = videoFrameDuration; exports.snapToVideoFrame = snapToVideoFrame; exports.videoFrames = videoFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.videoTimecodeFrames = videoTimecodeFrames; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.videoHourFrames = videoHourFrames; exports.formatVideoSeconds = formatVideoSeconds; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetMap = videoAssetMap; exports.videoSequenceMap = videoSequenceMap; exports.videoSequenceDuration = videoSequenceDuration; exports.videoSequenceChildren = videoSequenceChildren; exports.findVideoSequenceCycle = findVideoSequenceCycle; exports.allVideoTracks = allVideoTracks; exports.sortedVideoClips = sortedVideoClips; exports.findVideoClip = findVideoClip; exports.allVideoClipIds = allVideoClipIds; exports.findVideoSequenceClip = findVideoSequenceClip; exports.videoProjectDuration = videoProjectDuration; exports.videoClipBefore = videoClipBefore; exports.videoClipAfter = videoClipAfter; exports.videoClipHandles = videoClipHandles; exports.videoClipOverhang = videoClipOverhang; exports.videoSnapPoints = videoSnapPoints; exports.snapVideoTime = snapVideoTime; exports.findVideoGaps = findVideoGaps; exports.videoOverlap = videoOverlap; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.videoClipsAt = videoClipsAt; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipAnimated = videoClipAnimated; exports.VIDEO_MASK_DEFAULTS = VIDEO_MASK_DEFAULTS; exports.normalizeVideoClipMask = normalizeVideoClipMask; exports.videoMaskPolygon = videoMaskPolygon; exports.videoLayerMask = videoLayerMask; exports.videoMaskSvgPath = videoMaskSvgPath; exports.videoMaskContains = videoMaskContains; exports.videoEffectValuesAt = videoEffectValuesAt; exports.videoTransformAt = videoTransformAt; exports.placeVideoRect = placeVideoRect; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoTransitionEase = videoTransitionEase; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoClipSoundGain = videoClipSoundGain; exports.videoFrameAt = videoFrameAt; exports.videoLayersWithoutTransitionEffects = videoLayersWithoutTransitionEffects; exports.videoFrameLeafLayers = videoFrameLeafLayers; exports.videoProjectEffects = videoProjectEffects; exports.videoClipEffectUses = videoClipEffectUses; exports.videoLayersUseEffects = videoLayersUseEffects; exports.videoEffectFrameTime = videoEffectFrameTime; exports.videoEffectLayerKey = videoEffectLayerKey; exports.mixEffectPixels = mixEffectPixels; exports.overEffectPixels = overEffectPixels; exports.VideoEffectsEngine = VideoEffectsEngine; exports.VIDEO_EFFECT_PLACES = VIDEO_EFFECT_PLACES; exports.checkVideoEffectPlace = checkVideoEffectPlace; exports.checkVideoEffectValues = checkVideoEffectValues; exports.videoIrisRadius = videoIrisRadius; exports.videoWipeRect = videoWipeRect; exports.videoFrameToSvg = videoFrameToSvg; exports.videoProjectSvg = videoProjectSvg; exports.videoContactSheetSvg = videoContactSheetSvg; exports.drawVideoFrame = drawVideoFrame; exports.renderVideoFrameExact = renderVideoFrameExact; exports.toVideoEdl = toVideoEdl; exports.videoFfmpegUnsupportedEffects = videoFfmpegUnsupportedEffects; exports.videoFfmpegUnsupportedFeatures = videoFfmpegUnsupportedFeatures; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.ffmpegFilterValue = ffmpegFilterValue; exports.videoShellQuote = videoShellQuote; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure;