@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
@@ -7,7 +7,7 @@ import {
7
7
  r2,
8
8
  sameChartSelection,
9
9
  summarizeChartSeries
10
- } from "./chunk-R3QY7PPL.js";
10
+ } from "./chunk-DTSTNRPH.js";
11
11
  import {
12
12
  CHART_TYPE_LABELS,
13
13
  DEFAULT_CHART_PALETTE,
@@ -27,7 +27,7 @@ import {
27
27
  parseChartValue,
28
28
  shadeChartColor,
29
29
  uniqueChartSeriesId
30
- } from "./chunk-H5XX4KHB.js";
30
+ } from "./chunk-HLOKB7WW.js";
31
31
  import {
32
32
  parseCsv,
33
33
  parseTsv
@@ -19,9 +19,11 @@ var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
19
19
  var videoTransitionAudioCurve = (t) => 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(grade?.lut);
573
593
  if (setting && lut) {
574
594
  const out = applyVideoLut(lut, v);
575
- const s = setting.strength ?? 1;
595
+ const s = (setting.strength ?? 1) * (1 - (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" ? 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 (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 && !(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 = (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 = 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) => !!(l.effects?.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 = layer.grade?.lut;
1698
1851
  const lut = setting ? options.lutFor?.(setting.src) : null;
1699
- const scratch = lut ? options.scratch?.(cw, ch, 2 * depth + 1) : null;
1852
+ const scratch = lut || layer.mask ? options.scratch?.(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, 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, setting.strength ?? 1, 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" ? (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 ((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(clip.grade?.lut);
1996
+ if (lut && (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 = target.integrated ?? -14;
1815
2004
  const TP = 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(clip.grade?.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 ((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;
@@ -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 ((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}]`;
@@ -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 });
@@ -2250,7 +2470,9 @@ export {
2250
2470
  videoTransitionAudioCurve,
2251
2471
  VIDEO_BLEND_MODES,
2252
2472
  VIDEO_CLIP_ANIMATION_EFFECTS,
2473
+ VIDEO_MASK_SHAPES,
2253
2474
  VIDEO_FADE_CURVES,
2475
+ VIDEO_ADJUSTMENT_SCOPES,
2254
2476
  VIDEO_PLACEMENT_MODES,
2255
2477
  VIDEO_CLIP_TYPES,
2256
2478
  VIDEO_EASE_NAMES,
@@ -2274,6 +2496,7 @@ export {
2274
2496
  videoLutFromFunction,
2275
2497
  videoLutToCube,
2276
2498
  applyVideoLut,
2499
+ videoSkinToneWeight,
2277
2500
  applyVideoLutToPixels,
2278
2501
  videoLutSvgApproximation,
2279
2502
  VideoLutCache,
@@ -2305,6 +2528,7 @@ export {
2305
2528
  videoSlowMotionRamp,
2306
2529
  VIDEO_TIME_EPSILON,
2307
2530
  videoClipEnd,
2531
+ videoClipTypeLabel,
2308
2532
  videoTrackSyncLocked,
2309
2533
  videoClipSpeed,
2310
2534
  videoClipSourceRange,
@@ -2343,6 +2567,12 @@ export {
2343
2567
  videoClipAnimationProperties,
2344
2568
  videoClipAnimationSpans,
2345
2569
  videoClipAnimated,
2570
+ VIDEO_MASK_DEFAULTS,
2571
+ normalizeVideoClipMask,
2572
+ videoMaskPolygon,
2573
+ videoLayerMask,
2574
+ videoMaskSvgPath,
2575
+ videoMaskContains,
2346
2576
  videoEffectValuesAt,
2347
2577
  videoTransformAt,
2348
2578
  placeVideoRect,
@@ -2374,6 +2604,7 @@ export {
2374
2604
  renderVideoFrameExact,
2375
2605
  toVideoEdl,
2376
2606
  videoFfmpegUnsupportedEffects,
2607
+ videoFfmpegUnsupportedFeatures,
2377
2608
  parseVideoLoudnessMeasurement,
2378
2609
  ffmpegFilterValue,
2379
2610
  videoShellQuote,