@pixerate/editor 0.87.0 → 0.89.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 (72) hide show
  1. package/THIRD_PARTY_NOTICES.md +8 -2
  2. package/dist/agent/index.cjs +28 -28
  3. package/dist/agent/index.js +3 -3
  4. package/dist/canvas/index.cjs +3 -3
  5. package/dist/canvas/index.cjs.map +1 -1
  6. package/dist/canvas/index.js +16 -16
  7. package/dist/{chunk-TYOAJDFD.cjs → chunk-66IMF6OD.cjs} +3 -3
  8. package/dist/{chunk-TYOAJDFD.cjs.map → chunk-66IMF6OD.cjs.map} +1 -1
  9. package/dist/chunk-D4ZCU72J.js +604 -0
  10. package/dist/chunk-D4ZCU72J.js.map +1 -0
  11. package/dist/{chunk-IMSX4IVG.js → chunk-FWEO3HSL.js} +4 -4
  12. package/dist/chunk-H543GD6T.cjs +1696 -0
  13. package/dist/chunk-H543GD6T.cjs.map +1 -0
  14. package/dist/{chunk-ROXFP2LQ.cjs → chunk-HNDSPZ5T.cjs} +4 -4
  15. package/dist/{chunk-ROXFP2LQ.cjs.map → chunk-HNDSPZ5T.cjs.map} +1 -1
  16. package/dist/{chunk-MXF4DP2C.cjs → chunk-J3B45HTA.cjs} +74 -74
  17. package/dist/{chunk-MXF4DP2C.cjs.map → chunk-J3B45HTA.cjs.map} +1 -1
  18. package/dist/{chunk-GNO5PEMH.js → chunk-OK7T2LHK.js} +2 -2
  19. package/dist/chunk-P5TPUNBZ.js +1696 -0
  20. package/dist/chunk-P5TPUNBZ.js.map +1 -0
  21. package/dist/chunk-PDCITYBT.cjs +416 -0
  22. package/dist/chunk-PDCITYBT.cjs.map +1 -0
  23. package/dist/{chunk-RKJDBXTY.cjs → chunk-T4XWVPJK.cjs} +3 -3
  24. package/dist/{chunk-RKJDBXTY.cjs.map → chunk-T4XWVPJK.cjs.map} +1 -1
  25. package/dist/{chunk-ZQNDSUZO.js → chunk-UKI64KUD.js} +6 -6
  26. package/dist/{chunk-SVW2UNUC.js → chunk-WON34HKM.js} +2 -2
  27. package/dist/chunk-YLB2WVNA.js +416 -0
  28. package/dist/chunk-YLB2WVNA.js.map +1 -0
  29. package/dist/chunk-ZOOEX3WT.cjs +604 -0
  30. package/dist/chunk-ZOOEX3WT.cjs.map +1 -0
  31. package/dist/extensions/index.cjs +6 -6
  32. package/dist/extensions/index.cjs.map +1 -1
  33. package/dist/extensions/index.js +5 -5
  34. package/dist/image-editor/index.cjs +4387 -690
  35. package/dist/image-editor/index.cjs.map +1 -1
  36. package/dist/image-editor/index.d.cts +1677 -590
  37. package/dist/image-editor/index.d.ts +1677 -590
  38. package/dist/image-editor/index.js +4273 -576
  39. package/dist/image-editor/index.js.map +1 -1
  40. package/dist/index.cjs +27 -27
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.js +10 -10
  43. package/dist/paint/index.cjs +38 -0
  44. package/dist/paint/index.cjs.map +1 -0
  45. package/dist/paint/index.d.cts +167 -0
  46. package/dist/paint/index.d.ts +167 -0
  47. package/dist/paint/index.js +38 -0
  48. package/dist/paint/index.js.map +1 -0
  49. package/dist/raster/index.cjs +121 -0
  50. package/dist/raster/index.cjs.map +1 -0
  51. package/dist/raster/index.d.cts +365 -0
  52. package/dist/raster/index.d.ts +365 -0
  53. package/dist/raster/index.js +121 -0
  54. package/dist/raster/index.js.map +1 -0
  55. package/dist/serializers/index.cjs +4 -4
  56. package/dist/serializers/index.cjs.map +1 -1
  57. package/dist/serializers/index.js +3 -3
  58. package/dist/surface-fJhkmNQn.d.cts +181 -0
  59. package/dist/surface-fJhkmNQn.d.ts +181 -0
  60. package/dist/video-editor/index.cjs +930 -503
  61. package/dist/video-editor/index.cjs.map +1 -1
  62. package/dist/video-editor/index.d.cts +158 -11
  63. package/dist/video-editor/index.d.ts +158 -11
  64. package/dist/video-editor/index.js +841 -414
  65. package/dist/video-editor/index.js.map +1 -1
  66. package/package.json +13 -2
  67. package/skills/pixerate-image-editor/SKILL.md +111 -11
  68. package/skills/pixerate-video-editor/SKILL.md +11 -2
  69. /package/dist/{chunk-IMSX4IVG.js.map → chunk-FWEO3HSL.js.map} +0 -0
  70. /package/dist/{chunk-GNO5PEMH.js.map → chunk-OK7T2LHK.js.map} +0 -0
  71. /package/dist/{chunk-ZQNDSUZO.js.map → chunk-UKI64KUD.js.map} +0 -0
  72. /package/dist/{chunk-SVW2UNUC.js.map → chunk-WON34HKM.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6;
2
2
 
3
3
  var _chunkWAJQKWQUcjs = require('../chunk-WAJQKWQU.cjs');
4
4
 
@@ -210,6 +210,255 @@ function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
210
210
  return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
211
211
  }
212
212
 
213
+ // src/video-editor/lut.ts
214
+ var VIDEO_LUT_MAX_SIZE = 129;
215
+ function parseVideoCubeLut(text) {
216
+ let size = 0;
217
+ let size1 = 0;
218
+ let title;
219
+ const domainMin = [0, 0, 0];
220
+ const domainMax = [1, 1, 1];
221
+ let data = null;
222
+ let rows = 0;
223
+ const lines = text.split(/\r\n|\r|\n/);
224
+ const fail2 = (line, message) => {
225
+ throw new Error(`Line ${line + 1} of the .cube file: ${message}.`);
226
+ };
227
+ const numbers = (parts, line) => parts.map((p) => {
228
+ const v = Number(p);
229
+ if (!Number.isFinite(v)) fail2(line, `"${p}" is not a number`);
230
+ return v;
231
+ });
232
+ for (let i = 0; i < lines.length; i++) {
233
+ const line = lines[i].replace(/#.*/, "").trim();
234
+ if (!line) continue;
235
+ const parts = line.split(/\s+/);
236
+ const key = parts[0];
237
+ if (/^[A-Za-z]/.test(key)) {
238
+ if (rows) fail2(i, `"${key}" comes after the table; keywords go before it`);
239
+ if (key === "TITLE") title = line.slice(5).trim().replace(/^"|"$/g, "");
240
+ else if (key === "LUT_3D_SIZE") {
241
+ size = Number(parts[1]);
242
+ if (!Number.isInteger(size) || size < 2 || size > VIDEO_LUT_MAX_SIZE) fail2(i, `LUT_3D_SIZE must be a whole number from 2 to ${VIDEO_LUT_MAX_SIZE}, not "${_nullishCoalesce(parts[1], () => ( ""))}"`);
243
+ } else if (key === "LUT_1D_SIZE") size1 = Number(parts[1]) || 1;
244
+ else if (key === "DOMAIN_MIN" || key === "DOMAIN_MAX") {
245
+ const v2 = numbers(parts.slice(1), i);
246
+ if (v2.length !== 3) fail2(i, `${key} needs 3 numbers`);
247
+ (key === "DOMAIN_MIN" ? domainMin : domainMax).splice(0, 3, ...v2);
248
+ } else if (key === "LUT_3D_INPUT_RANGE") {
249
+ const v2 = numbers(parts.slice(1), i);
250
+ if (v2.length !== 2 || !(v2[1] > v2[0])) fail2(i, "LUT_3D_INPUT_RANGE needs a minimum and a larger maximum");
251
+ domainMin.fill(v2[0]);
252
+ domainMax.fill(v2[1]);
253
+ }
254
+ continue;
255
+ }
256
+ if (!size) fail2(i, size1 ? "this is a 1D LUT (LUT_1D_SIZE); only 3D LUTs (LUT_3D_SIZE) are supported" : "the table starts before LUT_3D_SIZE");
257
+ data ??= new Float32Array(size * size * size * 3);
258
+ const v = numbers(parts, i);
259
+ if (v.length !== 3) fail2(i, `a table row needs 3 numbers, not ${v.length}`);
260
+ if (rows >= size ** 3) fail2(i, `the table has more than the ${size ** 3} rows LUT_3D_SIZE ${size} needs`);
261
+ data.set(v, rows * 3);
262
+ rows++;
263
+ }
264
+ if (!size) throw new Error(size1 ? "The .cube file is a 1D LUT (LUT_1D_SIZE); only 3D LUTs (LUT_3D_SIZE) are supported." : "The .cube file has no LUT_3D_SIZE line, so it is not a 3D LUT.");
265
+ if (rows !== size ** 3) throw new Error(`The .cube file has ${rows} table rows; LUT_3D_SIZE ${size} needs ${size ** 3}.`);
266
+ for (let c = 0; c < 3; c++) if (!(domainMax[c] > domainMin[c])) throw new Error("The .cube file's DOMAIN_MAX must be larger than its DOMAIN_MIN on every channel.");
267
+ return { ...title ? { title } : {}, size, domainMin, domainMax, data };
268
+ }
269
+ function videoLutFromFunction(size, fn, title) {
270
+ const n2 = size - 1;
271
+ const data = new Float32Array(size ** 3 * 3);
272
+ let i = 0;
273
+ for (let b = 0; b < size; b++)
274
+ for (let g = 0; g < size; g++)
275
+ for (let r = 0; r < size; r++) {
276
+ data.set(fn([r / n2, g / n2, b / n2]), i);
277
+ i += 3;
278
+ }
279
+ return { ...title ? { title } : {}, size, domainMin: [0, 0, 0], domainMax: [1, 1, 1], data };
280
+ }
281
+ function videoLutToCube(lut) {
282
+ const f = (v) => (Math.round(v * 1e6) / 1e6).toString();
283
+ const head = [...lut.title ? [`TITLE "${lut.title.replace(/"/g, "'")}"`] : [], `LUT_3D_SIZE ${lut.size}`];
284
+ if (lut.domainMin.some((v) => v !== 0) || lut.domainMax.some((v) => v !== 1)) head.push(`DOMAIN_MIN ${lut.domainMin.map(f).join(" ")}`, `DOMAIN_MAX ${lut.domainMax.map(f).join(" ")}`);
285
+ const rows = [];
286
+ for (let i = 0; i < lut.data.length; i += 3) rows.push(`${f(lut.data[i])} ${f(lut.data[i + 1])} ${f(lut.data[i + 2])}`);
287
+ return `${head.join("\n")}
288
+ ${rows.join("\n")}
289
+ `;
290
+ }
291
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
292
+ function applyVideoLut(lut, rgb, out = [0, 0, 0]) {
293
+ const { size, data, domainMin, domainMax } = lut;
294
+ const n2 = size - 1;
295
+ const x = clamp01((rgb[0] - domainMin[0]) / (domainMax[0] - domainMin[0])) * n2;
296
+ const y = clamp01((rgb[1] - domainMin[1]) / (domainMax[1] - domainMin[1])) * n2;
297
+ const z = clamp01((rgb[2] - domainMin[2]) / (domainMax[2] - domainMin[2])) * n2;
298
+ const r0 = Math.min(n2 - 1, Math.floor(x));
299
+ const g0 = Math.min(n2 - 1, Math.floor(y));
300
+ const b0 = Math.min(n2 - 1, Math.floor(z));
301
+ const fr = x - r0;
302
+ const fg = y - g0;
303
+ const fb = z - b0;
304
+ const sr = 3;
305
+ const sg = 3 * size;
306
+ const sb = 3 * size * size;
307
+ const o = r0 * sr + g0 * sg + b0 * sb;
308
+ let a, c, w0, w1, w2, w3;
309
+ if (fr > fg) {
310
+ if (fg > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sg, 1 - fr, fr - fg, fg - fb, fb];
311
+ else if (fr > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sb, 1 - fr, fr - fb, fb - fg, fg];
312
+ else [a, c, w0, w1, w2, w3] = [sb, sb + sr, 1 - fb, fb - fr, fr - fg, fg];
313
+ } else if (fb > fg) [a, c, w0, w1, w2, w3] = [sb, sb + sg, 1 - fb, fb - fg, fg - fr, fr];
314
+ else if (fb > fr) [a, c, w0, w1, w2, w3] = [sg, sg + sb, 1 - fg, fg - fb, fb - fr, fr];
315
+ else [a, c, w0, w1, w2, w3] = [sg, sg + sr, 1 - fg, fg - fr, fr - fb, fb];
316
+ const d = sr + sg + sb;
317
+ 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]);
318
+ return out;
319
+ }
320
+ function applyVideoLutToPixels(lut, data, strength = 1) {
321
+ const s = clamp01(strength);
322
+ if (!s) return;
323
+ const rgb = [0, 0, 0];
324
+ const out = [0, 0, 0];
325
+ for (let i = 0; i < data.length; i += 4) {
326
+ if (!data[i + 3]) continue;
327
+ rgb[0] = data[i] / 255;
328
+ rgb[1] = data[i + 1] / 255;
329
+ rgb[2] = data[i + 2] / 255;
330
+ applyVideoLut(lut, rgb, out);
331
+ for (let k = 0; k < 3; k++) data[i + k] = Math.round((rgb[k] + (out[k] - rgb[k]) * s) * 255);
332
+ }
333
+ }
334
+ var approximations = /* @__PURE__ */ new WeakMap();
335
+ function videoLutSvgApproximation(lut) {
336
+ const cached = approximations.get(lut);
337
+ if (cached) return cached;
338
+ const points = 17;
339
+ const tables = [[], [], []];
340
+ for (let i = 0; i < points; i++) {
341
+ const v = i / (points - 1);
342
+ const out = applyVideoLut(lut, [v, v, v]);
343
+ for (let k = 0; k < 3; k++) tables[k].push(out[k]);
344
+ }
345
+ const curve = (k, v) => {
346
+ const x = clamp01(v) * (points - 1);
347
+ const i = Math.min(points - 2, Math.floor(x));
348
+ return tables[k][i] + (tables[k][i + 1] - tables[k][i]) * (x - i);
349
+ };
350
+ const ata = Array.from({ length: 4 }, () => [0, 0, 0, 0]);
351
+ const atb = Array.from({ length: 3 }, () => [0, 0, 0, 0]);
352
+ const grid = 9;
353
+ for (let b = 0; b < grid; b++)
354
+ for (let g = 0; g < grid; g++)
355
+ for (let r = 0; r < grid; r++) {
356
+ const x = [r / (grid - 1), g / (grid - 1), b / (grid - 1)];
357
+ const z = [curve(0, x[0]), curve(1, x[1]), curve(2, x[2]), 1];
358
+ const y = applyVideoLut(lut, x);
359
+ for (let i = 0; i < 4; i++) {
360
+ for (let j = 0; j < 4; j++) ata[i][j] += z[i] * z[j];
361
+ for (let k = 0; k < 3; k++) atb[k][i] += z[i] * y[k];
362
+ }
363
+ }
364
+ const matrix = atb.map((rhs) => solve4(ata, rhs));
365
+ const result = { tables, matrix };
366
+ approximations.set(lut, result);
367
+ return result;
368
+ }
369
+ function solve4(a, b) {
370
+ const m = a.map((row, i) => [...row, b[i]]);
371
+ for (let col = 0; col < 4; col++) {
372
+ let pivot = col;
373
+ for (let r = col + 1; r < 4; r++) if (Math.abs(m[r][col]) > Math.abs(m[pivot][col])) pivot = r;
374
+ if (Math.abs(m[pivot][col]) < 1e-12) continue;
375
+ [m[col], m[pivot]] = [m[pivot], m[col]];
376
+ for (let r = 0; r < 4; r++) {
377
+ if (r === col) continue;
378
+ const f = m[r][col] / m[col][col];
379
+ for (let c = col; c < 5; c++) m[r][c] -= f * m[col][c];
380
+ }
381
+ }
382
+ return m.map((row, i) => Math.abs(row[i]) < 1e-12 ? 0 : row[4] / row[i]);
383
+ }
384
+ var VideoLutCache = (_class = class {
385
+ constructor(loader = async (src) => {
386
+ const res = await fetch(src);
387
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
388
+ return res.text();
389
+ }) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);
390
+ this.loader = loader;
391
+ }
392
+
393
+ __init() {this.ready = /* @__PURE__ */ new Map()}
394
+ __init2() {this.pending = /* @__PURE__ */ new Map()}
395
+ __init3() {this.failed = /* @__PURE__ */ new Map()}
396
+ __init4() {this.listeners = /* @__PURE__ */ new Set()}
397
+ /** The LUT if loaded, without starting to load it. */
398
+ peek(src) {
399
+ return this.ready.get(src);
400
+ }
401
+ /** The LUT if loaded; otherwise `undefined`, and loading starts (once) unless it failed. */
402
+ get(src) {
403
+ const lut = this.ready.get(src);
404
+ if (!lut && !this.failed.has(src)) void this.load(src).catch(() => {
405
+ });
406
+ return lut;
407
+ }
408
+ /** Why loading a LUT failed, if it did. */
409
+ error(src) {
410
+ return this.failed.get(src);
411
+ }
412
+ /** The LUT, loading and reading it if needed. Rejects with a readable error. */
413
+ load(src) {
414
+ const ready = this.ready.get(src);
415
+ if (ready) return Promise.resolve(ready);
416
+ const failed = this.failed.get(src);
417
+ if (failed) return Promise.reject(new Error(failed));
418
+ let pending = this.pending.get(src);
419
+ if (!pending) {
420
+ pending = Promise.resolve().then(() => this.loader(src)).then((v) => typeof v === "string" ? parseVideoCubeLut(v) : v).then(
421
+ (lut) => {
422
+ this.pending.delete(src);
423
+ this.ready.set(src, lut);
424
+ this.emit();
425
+ return lut;
426
+ },
427
+ (e) => {
428
+ this.pending.delete(src);
429
+ const message = `The LUT "${src}" could not be read: ${e instanceof Error ? e.message : String(e)}`;
430
+ this.failed.set(src, message);
431
+ this.emit();
432
+ throw new Error(message);
433
+ }
434
+ );
435
+ this.pending.set(src, pending);
436
+ }
437
+ return pending;
438
+ }
439
+ /** Uses a LUT the host already has (`.cube` text or a parsed LUT), under `src`. */
440
+ set(src, lut) {
441
+ const parsed = typeof lut === "string" ? parseVideoCubeLut(lut) : lut;
442
+ this.ready.set(src, parsed);
443
+ this.failed.delete(src);
444
+ this.emit();
445
+ return parsed;
446
+ }
447
+ /** Forgets a failure so the next `get` tries again. */
448
+ retry(src) {
449
+ if (this.failed.delete(src)) this.emit();
450
+ }
451
+ subscribe(fn) {
452
+ this.listeners.add(fn);
453
+ return () => {
454
+ this.listeners.delete(fn);
455
+ };
456
+ }
457
+ emit() {
458
+ for (const l of [...this.listeners]) l();
459
+ }
460
+ }, _class);
461
+
213
462
  // src/video-editor/grade.ts
214
463
  var VIDEO_GRADE_CONTROLS = [
215
464
  { key: "exposure", label: "Exposure", min: -3, max: 3, step: 0.1, description: "Exposure in stops: +1 doubles every channel, -1 halves it (default 0)." },
@@ -230,6 +479,12 @@ var VIDEO_GRADE_PRESETS = {
230
479
  };
231
480
  var VIDEO_GRADE_PRESET_NAMES = Object.keys(VIDEO_GRADE_PRESETS);
232
481
  var clampTo = (c, v) => Math.min(c.max, Math.max(c.min, v));
482
+ function normalizeVideoGradeLut(lut) {
483
+ if (!lut || typeof lut.src !== "string" || !lut.src) return void 0;
484
+ const strength = typeof lut.strength === "number" && Number.isFinite(lut.strength) ? Math.round(Math.min(1, Math.max(0, lut.strength)) * 1e3) / 1e3 : 1;
485
+ if (!strength) return void 0;
486
+ return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {} };
487
+ }
233
488
  function normalizeVideoGrade(grade) {
234
489
  if (!grade) return void 0;
235
490
  const out = {};
@@ -237,6 +492,8 @@ function normalizeVideoGrade(grade) {
237
492
  const v = grade[c.key];
238
493
  if (typeof v === "number" && Number.isFinite(v) && v !== 0) out[c.key] = Math.round(clampTo(c, v) * 1e3) / 1e3;
239
494
  }
495
+ const lut = normalizeVideoGradeLut(grade.lut);
496
+ if (lut) out.lut = lut;
240
497
  return Object.keys(out).length ? out : void 0;
241
498
  }
242
499
  function videoGradePreset(name, amount = 1) {
@@ -245,6 +502,9 @@ function videoGradePreset(name, amount = 1) {
245
502
  const k = Math.min(1, Math.max(0, amount));
246
503
  return normalizeVideoGrade(Object.fromEntries(Object.entries(preset).map(([key, v]) => [key, v * k])));
247
504
  }
505
+ function videoGradeLook(current, name, amount = 1) {
506
+ return normalizeVideoGrade({ ...videoGradePreset(name, amount), ..._optionalChain([current, 'optionalAccess', _2 => _2.lut]) ? { lut: current.lut } : {} });
507
+ }
248
508
  function videoGradeSteps(grade) {
249
509
  const g = normalizeVideoGrade(grade);
250
510
  if (!g) return [];
@@ -302,21 +562,35 @@ function videoGradeStepMatrix(step) {
302
562
  }
303
563
  }
304
564
  }
305
- function applyVideoGrade(grade, rgb) {
565
+ function applyVideoGrade(grade, rgb, lut) {
306
566
  let v = rgb.map((x) => x / 255);
307
567
  for (const step of videoGradeSteps(grade)) {
308
568
  const m = videoGradeStepMatrix(step);
309
569
  v = m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2] + row[3])));
310
570
  }
571
+ const setting = normalizeVideoGradeLut(_optionalChain([grade, 'optionalAccess', _3 => _3.lut]));
572
+ if (setting && lut) {
573
+ const out = applyVideoLut(lut, v);
574
+ const s = _nullishCoalesce(setting.strength, () => ( 1));
575
+ v = v.map((x, k) => x + (out[k] - x) * s);
576
+ }
311
577
  return v.map((x) => Math.round(x * 255));
312
578
  }
313
579
  var fmt2 = (v) => String(Math.round(v * 1e5) / 1e5);
314
580
  function videoGradeStepSvgValues(step) {
315
581
  return videoGradeStepMatrix(step).map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
316
582
  }
317
- function videoGradeSvgFilter(id, grade) {
583
+ function videoLutSvgPrimitives(lut, strength = 1) {
584
+ const { tables, matrix } = videoLutSvgApproximation(lut);
585
+ const table = (k) => tables[k].map(fmt2).join(" ");
586
+ const values = matrix.map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
587
+ const s = Math.min(1, Math.max(0, strength));
588
+ return `<feComponentTransfer result="lutIn"><feFuncA type="identity"/></feComponentTransfer><feComponentTransfer><feFuncR type="table" tableValues="${table(0)}"/><feFuncG type="table" tableValues="${table(1)}"/><feFuncB type="table" tableValues="${table(2)}"/></feComponentTransfer><feColorMatrix type="matrix" values="${values}"/>` + (s < 1 ? `<feComposite in2="lutIn" operator="arithmetic" k1="0" k2="${fmt2(s)}" k3="${fmt2(1 - s)}" k4="0"/>` : "");
589
+ }
590
+ function videoGradeSvgFilter(id, grade, lut) {
318
591
  const steps = videoGradeSteps(grade);
319
- return `<filter id="${id}" color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">${steps.map((s) => `<feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/>`).join("")}</filter>`;
592
+ const setting = normalizeVideoGradeLut(_optionalChain([grade, 'optionalAccess', _4 => _4.lut]));
593
+ return `<filter id="${id}" color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">${steps.map((s) => `<feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/>`).join("")}${setting && lut ? videoLutSvgPrimitives(lut, setting.strength) : ""}</filter>`;
320
594
  }
321
595
  var videoGradeGainsFilterId = (step) => `pixerate-wb-${[step.r, step.g, step.b].map((v) => Math.round(v * 1e3)).join("-")}`;
322
596
  function videoGradeCssFilter(grade) {
@@ -530,10 +804,17 @@ function parseVideoClipAnimation(v, path) {
530
804
  function parseVideoGrade(v, path) {
531
805
  if (v === void 0) return void 0;
532
806
  if (!isObject(v)) return fail(path, "must be an object such as { exposure, contrast, saturation }");
533
- const known = new Set(VIDEO_GRADE_CONTROLS.map((c) => c.key));
807
+ const known = /* @__PURE__ */ new Set([...VIDEO_GRADE_CONTROLS.map((c) => c.key), "lut"]);
534
808
  const unknown = Object.keys(v).find((k) => !known.has(k));
535
809
  if (unknown) fail(`${path}.${unknown}`, `is not a grade setting; use ${[...known].join(", ")}`);
536
- return compact(Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, num(v[c.key], `${path}.${c.key}`, { optional: true, min: c.min, max: c.max })])));
810
+ return compact({ ...Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, num(v[c.key], `${path}.${c.key}`, { optional: true, min: c.min, max: c.max })])), lut: parseVideoGradeLut(v.lut, `${path}.lut`) });
811
+ }
812
+ function parseVideoGradeLut(v, path) {
813
+ if (v === void 0) return void 0;
814
+ if (!isObject(v)) return fail(path, "must be an object { src, strength } naming a .cube file");
815
+ const unknown = Object.keys(v).find((k) => !["src", "strength", "name"].includes(k));
816
+ if (unknown) fail(`${path}.${unknown}`, "is not a LUT setting; use src, strength, name");
817
+ return compact({ src: str(v.src, `${path}.src`), strength: num(v.strength, `${path}.strength`, { optional: true, min: 0, max: 1 }), name: str(v.name, `${path}.name`, true) });
537
818
  }
538
819
  function parseVideoTransition(v, path) {
539
820
  if (v === void 0) return void 0;
@@ -701,7 +982,7 @@ var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.spe
701
982
  function videoClipSourceRange(clip) {
702
983
  if (clip.type !== "media") return null;
703
984
  const start = _nullishCoalesce(clip.in, () => ( 0));
704
- if (_optionalChain([clip, 'access', _2 => _2.timeRemap, 'optionalAccess', _3 => _3.keys, 'access', _4 => _4.length])) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
985
+ if (_optionalChain([clip, 'access', _5 => _5.timeRemap, 'optionalAccess', _6 => _6.keys, 'access', _7 => _7.length])) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
705
986
  return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
706
987
  }
707
988
  var videoFrameDuration = (fps) => 1e3 / fps;
@@ -782,13 +1063,13 @@ function videoClipHandles(clip, assets) {
782
1063
  const speed = videoClipSpeed(clip);
783
1064
  const range = videoClipSourceRange(clip);
784
1065
  const before = range.in / speed;
785
- if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _5 => _5.kind]) === "image" ? Infinity : before, after: Infinity };
1066
+ if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _8 => _8.kind]) === "image" ? Infinity : before, after: Infinity };
786
1067
  return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
787
1068
  }
788
1069
  function videoClipOverhang(track, clip) {
789
1070
  const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
790
1071
  const next = videoClipAfter(track, clip);
791
- const tail = _optionalChain([next, 'optionalAccess', _6 => _6.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
1072
+ const tail = _optionalChain([next, 'optionalAccess', _9 => _9.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
792
1073
  return { lead, tail };
793
1074
  }
794
1075
  function videoSnapPoints(project, options = {}) {
@@ -872,7 +1153,7 @@ function videoClipAnimated(clip, project) {
872
1153
  const spans = videoClipAnimationSpans(clip);
873
1154
  const out = { ...clip, transform: { ...clip.transform } };
874
1155
  for (const prop of PROPS) {
875
- const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _7 => _7.transform, 'optionalAccess', _8 => _8[prop]]);
1156
+ const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _10 => _10.transform, 'optionalAccess', _11 => _11[prop]]);
876
1157
  if (isVideoKeyframes(current)) continue;
877
1158
  const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
878
1159
  const away = (spec) => {
@@ -909,7 +1190,7 @@ function videoClipKeyframes(clip) {
909
1190
  const out = [
910
1191
  { property: "opacity", keys: videoKeysOf(clip.opacity) },
911
1192
  { property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
912
- ...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _9 => _9.transform, 'optionalAccess', _10 => _10[p]])) }))
1193
+ ...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _12 => _12.transform, 'optionalAccess', _13 => _13[p]])) }))
913
1194
  ];
914
1195
  return out.filter((o) => o.keys.length);
915
1196
  }
@@ -966,7 +1247,7 @@ function checkVideoProject(project) {
966
1247
  for (const which of ["animateIn", "animateOut"]) {
967
1248
  const a = clip[which];
968
1249
  if (!a) continue;
969
- const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _11 => _11.transform, 'optionalAccess', _12 => _12[p]])).length);
1250
+ const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _14 => _14.transform, 'optionalAccess', _15 => _15[p]])).length);
970
1251
  if (keyed.length)
971
1252
  issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
972
1253
  }
@@ -1078,7 +1359,7 @@ function clipAnimatables(clip) {
1078
1359
  // A speed ramp is keys in clip time too, so trims and splits keep it on the same frames of source.
1079
1360
  { get: () => c.timeRemap, set: (v) => c.timeRemap = v }
1080
1361
  ];
1081
- for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _13 => _13.transform, 'optionalAccess', _14 => _14[p]]), set: (v) => c.transform && (c.transform[p] = v) });
1362
+ for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _16 => _16.transform, 'optionalAccess', _17 => _17[p]]), set: (v) => c.transform && (c.transform[p] = v) });
1082
1363
  return out;
1083
1364
  }
1084
1365
  function retimeClipKeys(clip, fn) {
@@ -1089,12 +1370,12 @@ function retimeClipKeys(clip, fn) {
1089
1370
  }
1090
1371
  var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
1091
1372
  var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
1092
- var Editor = (_class = class {
1093
- constructor(p) {;_class.prototype.__init.call(this);
1373
+ var Editor = (_class2 = class {
1374
+ constructor(p) {;_class2.prototype.__init5.call(this);
1094
1375
  this.p = p;
1095
1376
  }
1096
1377
 
1097
- __init() {this.outcomes = []}
1378
+ __init5() {this.outcomes = []}
1098
1379
  track(id) {
1099
1380
  const t = this.p.tracks.find((x) => x.id === id);
1100
1381
  if (!t) refuse(`There is no track ${_chunkWAJQKWQUcjs.namedWithSuggestion.call(void 0, id, this.p.tracks.map((x) => x.id))}; the tracks are ${this.p.tracks.map((x) => `"${x.id}" (${x.kind})`).join(", ") || "none"}.`);
@@ -1126,7 +1407,7 @@ var Editor = (_class = class {
1126
1407
  kindFits(track, clip) {
1127
1408
  const asset = clip.type === "media" ? this.asset(clip.asset) : null;
1128
1409
  if (track.kind === "audio" && (clip.type !== "media" || asset.kind === "image")) refuse(`Track "${track.id}" is an audio track; ${clip.type === "media" ? "images" : `${clip.type} clips`} go on a video track.`);
1129
- if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _15 => _15.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
1410
+ if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _18 => _18.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
1130
1411
  }
1131
1412
  /** Moves every clip on `track` starting at or after `from` by `delta`. */
1132
1413
  shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
@@ -1168,7 +1449,7 @@ var Editor = (_class = class {
1168
1449
  }
1169
1450
  /** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
1170
1451
  moveIn(clip, delta) {
1171
- if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _16 => _16(this.p), 'access', _17 => _17.get, 'call', _18 => _18(clip.asset), 'optionalAccess', _19 => _19.kind]) === "image") return;
1452
+ if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _19 => _19(this.p), 'access', _20 => _20.get, 'call', _21 => _21(clip.asset), 'optionalAccess', _22 => _22.kind]) === "image") return;
1172
1453
  clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
1173
1454
  if (clip.in < 1e-9) delete clip.in;
1174
1455
  }
@@ -1434,7 +1715,7 @@ var Editor = (_class = class {
1434
1715
  if (clip.type === "media" && !isRamped(clip)) {
1435
1716
  const asset = assets.get(clip.asset);
1436
1717
  const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
1437
- if (_optionalChain([asset, 'optionalAccess', _20 => _20.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
1718
+ if (_optionalChain([asset, 'optionalAccess', _23 => _23.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
1438
1719
  refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
1439
1720
  }
1440
1721
  if (e.ripple) {
@@ -1492,7 +1773,7 @@ var Editor = (_class = class {
1492
1773
  let stretch = 1;
1493
1774
  if (at.clip.type === "media" && !("duration" in set)) {
1494
1775
  const was = videoClipSpeed(at.clip);
1495
- const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _21 => _21.remove, 'optionalAccess', _22 => _22.includes, 'call', _23 => _23("speed")]) ? 1 : was;
1776
+ const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _24 => _24.remove, 'optionalAccess', _25 => _25.includes, 'call', _26 => _26("speed")]) ? 1 : was;
1496
1777
  if (now !== was) {
1497
1778
  set.duration = at.clip.duration * was / now;
1498
1779
  stretch = was / now;
@@ -1638,7 +1919,7 @@ var Editor = (_class = class {
1638
1919
  this.trimHead(after, delta);
1639
1920
  }
1640
1921
  track.clips.sort((a, b) => a.start - b.start);
1641
- return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _24 => _24.id]), clip.id, _optionalChain([after, 'optionalAccess', _25 => _25.id])].filter((x) => !!x), track: track.id, delta });
1922
+ return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _27 => _27.id]), clip.id, _optionalChain([after, 'optionalAccess', _28 => _28.id])].filter((x) => !!x), track: track.id, delta });
1642
1923
  }
1643
1924
  case "lift_range":
1644
1925
  case "extract_range": {
@@ -1669,7 +1950,7 @@ var Editor = (_class = class {
1669
1950
  const limits = VIDEO_KEYFRAME_LIMITS[e.property];
1670
1951
  const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
1671
1952
  const c = clip;
1672
- const current = transform ? _optionalChain([c, 'access', _26 => _26.transform, 'optionalAccess', _27 => _27[e.property]]) : c[e.property];
1953
+ const current = transform ? _optionalChain([c, 'access', _29 => _29.transform, 'optionalAccess', _30 => _30[e.property]]) : c[e.property];
1673
1954
  let value;
1674
1955
  if (e.keys === null) {
1675
1956
  const v = videoValueAt(current, 0, limits.default);
@@ -1740,7 +2021,7 @@ var Editor = (_class = class {
1740
2021
  refuse(`Unknown op ${_chunkWAJQKWQUcjs.namedWithSuggestion.call(void 0, String(e.op), VIDEO_EDIT_OPS)}; the ops are ${VIDEO_EDIT_OPS.join(", ")}.`);
1741
2022
  }
1742
2023
  }
1743
- }, _class);
2024
+ }, _class2);
1744
2025
  function applyVideoEdits(project, edits) {
1745
2026
  const list = Array.isArray(edits) ? edits : [edits];
1746
2027
  if (!list.length) return { ok: true, project, issues: checkVideoProject(project), outcomes: [] };
@@ -1751,7 +2032,7 @@ function applyVideoEdits(project, edits) {
1751
2032
  editor.run(edit);
1752
2033
  } catch (error) {
1753
2034
  const message = error.message;
1754
- return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _28 => _28.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
2035
+ return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _31 => _31.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
1755
2036
  }
1756
2037
  }
1757
2038
  let next;
@@ -1770,7 +2051,7 @@ function applyVideoEdits(project, edits) {
1770
2051
 
1771
2052
  // src/video-editor/evaluate.ts
1772
2053
  function videoTransformAt(transform, clipTime = 0) {
1773
- return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _29 => _29.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _30 => _30.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _31 => _31.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _32 => _32.rotation]), clipTime, 0) };
2054
+ return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _32 => _32.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _33 => _33.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _34 => _34.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _35 => _35.rotation]), clipTime, 0) };
1774
2055
  }
1775
2056
  function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
1776
2057
  let w = size.width;
@@ -1797,10 +2078,10 @@ function videoTitleFrame(project, clip, clipTime = 0) {
1797
2078
  const layer = { clip: clip.id, track: "", type: "text", opacity: 1, rect: placeVideoRect(project, project, "fill", clip.transform, clipTime), rotation: videoTransformAt(clip.transform, clipTime).rotation, text: videoTextStyle(clip) };
1798
2079
  return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
1799
2080
  }
1800
- var clamp01 = (v) => Math.min(1, Math.max(0, v));
1801
- var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp01(progress))) / 2;
2081
+ var clamp012 = (v) => Math.min(1, Math.max(0, v));
2082
+ var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp012(progress))) / 2;
1802
2083
  function videoFadeCurveGain(curve, x) {
1803
- const t = clamp01(x);
2084
+ const t = clamp012(x);
1804
2085
  switch (curve) {
1805
2086
  case "equal-power":
1806
2087
  return Math.sin(t * Math.PI / 2);
@@ -1817,8 +2098,8 @@ function videoFadeCurveGain(curve, x) {
1817
2098
  function fadeGain(clip, time, sound = false) {
1818
2099
  let g = 1;
1819
2100
  const shape = sound && clip.type === "media" ? clip : void 0;
1820
- if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _33 => _33.fadeInCurve]), (time - clip.start) / clip.fadeIn);
1821
- if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _34 => _34.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
2101
+ if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _36 => _36.fadeInCurve]), (time - clip.start) / clip.fadeIn);
2102
+ if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _37 => _37.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
1822
2103
  return g;
1823
2104
  }
1824
2105
  function videoClipSoundGain(clip, clipTime) {
@@ -1831,11 +2112,11 @@ function activeOnTrack(track, time) {
1831
2112
  const before = clip.transition ? videoClipBefore(track, clip) : null;
1832
2113
  const lead = before ? clip.transition.duration / 2 : 0;
1833
2114
  const next = videoClipAfter(track, clip);
1834
- const tail = _optionalChain([next, 'optionalAccess', _35 => _35.transition]) ? next.transition.duration / 2 : 0;
2115
+ const tail = _optionalChain([next, 'optionalAccess', _38 => _38.transition]) ? next.transition.duration / 2 : 0;
1835
2116
  if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
1836
2117
  const a = { clip };
1837
- if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
1838
- if (tail && time >= videoClipEnd(clip) - tail) a.outgoing = { progress: clamp01((time - (videoClipEnd(clip) - tail)) / (2 * tail)), type: next.transition.type, next };
2118
+ if (lead && time < clip.start + lead) a.incoming = { progress: clamp012((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
2119
+ if (tail && time >= videoClipEnd(clip) - tail) a.outgoing = { progress: clamp012((time - (videoClipEnd(clip) - tail)) / (2 * tail)), type: next.transition.type, next };
1839
2120
  out.push(a);
1840
2121
  }
1841
2122
  return out;
@@ -1848,8 +2129,8 @@ function videoFrameAt(project, time) {
1848
2129
  for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
1849
2130
  const track = project.tracks[ti];
1850
2131
  for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
1851
- if (_optionalChain([incoming, 'optionalAccess', _36 => _36.type]) === "dip" && incoming.progress < 0.5) continue;
1852
- if (_optionalChain([outgoing, 'optionalAccess', _37 => _37.type]) === "dip" && outgoing.progress >= 0.5) continue;
2132
+ if (_optionalChain([incoming, 'optionalAccess', _39 => _39.type]) === "dip" && incoming.progress < 0.5) continue;
2133
+ if (_optionalChain([outgoing, 'optionalAccess', _40 => _40.type]) === "dip" && outgoing.progress >= 0.5) continue;
1853
2134
  const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
1854
2135
  const fade = fadeGain(clip, inside);
1855
2136
  const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
@@ -1880,8 +2161,8 @@ function videoFrameAt(project, time) {
1880
2161
  shift = videoTransitionEase(incoming.progress) - 1;
1881
2162
  } else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
1882
2163
  }
1883
- if (_optionalChain([outgoing, 'optionalAccess', _38 => _38.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
1884
- if (_optionalChain([outgoing, 'optionalAccess', _39 => _39.type]) === "push") {
2164
+ if (_optionalChain([outgoing, 'optionalAccess', _41 => _41.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
2165
+ if (_optionalChain([outgoing, 'optionalAccess', _42 => _42.type]) === "push") {
1885
2166
  shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
1886
2167
  shift = videoTransitionEase(outgoing.progress);
1887
2168
  }
@@ -1891,7 +2172,7 @@ function videoFrameAt(project, time) {
1891
2172
  const across = shiftDirection === "left" || shiftDirection === "right";
1892
2173
  const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
1893
2174
  const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
1894
- const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _40 => _40.transform, 'optionalAccess', _41 => _41.rotation]), clipTime, 0) };
2175
+ const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp012(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _43 => _43.transform, 'optionalAccess', _44 => _44.rotation]), clipTime, 0) };
1895
2176
  if (wipe) layer.wipe = wipe;
1896
2177
  if (iris) layer.iris = iris;
1897
2178
  if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
@@ -1901,7 +2182,7 @@ function videoFrameAt(project, time) {
1901
2182
  if (clip.type === "text") layer.text = videoTextStyle(clip);
1902
2183
  if (clip.type === "color") layer.color = clip.color;
1903
2184
  if (layer.opacity > 0) layers.push(layer);
1904
- if (dip && dip.opacity > 0) layers.push({ clip: `transition:${incoming ? clip.id : outgoing.next.id}`, track: track.id, type: "color", color: dip.color, opacity: clamp01(dip.opacity), rect: { x: 0, y: 0, width: project.width, height: project.height }, rotation: 0 });
2185
+ if (dip && dip.opacity > 0) layers.push({ clip: `transition:${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 });
1905
2186
  }
1906
2187
  }
1907
2188
  return { time, width: project.width, height: project.height, background: _nullishCoalesce(project.background, () => ( "#000000")), layers, audio };
@@ -2058,15 +2339,15 @@ async function decodeVideoAudioPeaks(src, options = {}) {
2058
2339
  }
2059
2340
  return buildVideoAudioPeaks(Array.from({ length: buffer.numberOfChannels }, (_, i) => buffer.getChannelData(i)), buffer.sampleRate);
2060
2341
  }
2061
- var VideoAudioPeaksCache = (_class2 = class {
2062
- constructor(loader = (asset) => decodeVideoAudioPeaks(asset.src)) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);
2342
+ var VideoAudioPeaksCache = (_class3 = class {
2343
+ constructor(loader = (asset) => decodeVideoAudioPeaks(asset.src)) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);
2063
2344
  this.loader = loader;
2064
2345
  }
2065
2346
 
2066
- __init2() {this.ready = /* @__PURE__ */ new Map()}
2067
- __init3() {this.pending = /* @__PURE__ */ new Map()}
2068
- __init4() {this.failed = /* @__PURE__ */ new Map()}
2069
- __init5() {this.listeners = /* @__PURE__ */ new Set()}
2347
+ __init6() {this.ready = /* @__PURE__ */ new Map()}
2348
+ __init7() {this.pending = /* @__PURE__ */ new Map()}
2349
+ __init8() {this.failed = /* @__PURE__ */ new Map()}
2350
+ __init9() {this.listeners = /* @__PURE__ */ new Set()}
2070
2351
  /** The peaks if ready, without starting to load them. */
2071
2352
  peek(asset) {
2072
2353
  return this.ready.get(asset.src);
@@ -2122,7 +2403,7 @@ var VideoAudioPeaksCache = (_class2 = class {
2122
2403
  emit() {
2123
2404
  for (const fn of this.listeners) fn();
2124
2405
  }
2125
- }, _class2);
2406
+ }, _class3);
2126
2407
 
2127
2408
  // src/video-editor/scopes.ts
2128
2409
  var VIDEO_SCOPE_KINDS = ["histogram", "waveform", "vectorscope"];
@@ -2408,10 +2689,10 @@ function videoScopeImage(kind, pixels, width = 256, height = 128, options = {})
2408
2689
  const size = Math.min(width, height);
2409
2690
  return videoVectorscopeImage(videoVectorscope(pixels, { ...options, size }), size);
2410
2691
  }
2411
- var VideoPreviewPixels = (_class3 = class {constructor() { _class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this); }
2412
- __init6() {this.reader = null}
2413
- __init7() {this.drawnAt = null}
2414
- __init8() {this.listeners = /* @__PURE__ */ new Set()}
2692
+ var VideoPreviewPixels = (_class4 = class {constructor() { _class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this); }
2693
+ __init10() {this.reader = null}
2694
+ __init11() {this.drawnAt = null}
2695
+ __init12() {this.listeners = /* @__PURE__ */ new Set()}
2415
2696
  /** Gives the canvas to read (the latest preview wins). Returns a function that takes it away again. */
2416
2697
  attach(reader) {
2417
2698
  this.reader = reader;
@@ -2479,7 +2760,7 @@ var VideoPreviewPixels = (_class3 = class {constructor() { _class3.prototype.__i
2479
2760
  emit() {
2480
2761
  for (const l of [...this.listeners]) l();
2481
2762
  }
2482
- }, _class3);
2763
+ }, _class4);
2483
2764
  function readVideoScopesPanel(feed, kinds = VIDEO_SCOPE_KINDS, options = {}) {
2484
2765
  let snap;
2485
2766
  try {
@@ -2562,8 +2843,8 @@ function videoTranscriptTrack(project, track) {
2562
2843
  return track;
2563
2844
  }
2564
2845
  const assets = videoAssetMap(project);
2565
- const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _42 => _42.get, 'call', _43 => _43(c.asset), 'optionalAccess', _44 => _44.transcript, 'optionalAccess', _45 => _45.length]));
2566
- return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _46 => _46.id]), () => ( null));
2846
+ const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _45 => _45.get, 'call', _46 => _46(c.asset), 'optionalAccess', _47 => _47.transcript, 'optionalAccess', _48 => _48.length]));
2847
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _49 => _49.id]), () => ( null));
2567
2848
  }
2568
2849
  function videoTimelineTranscript(project, options = {}) {
2569
2850
  const id = videoTranscriptTrack(project, options.track);
@@ -2573,8 +2854,8 @@ function videoTimelineTranscript(project, options = {}) {
2573
2854
  const out = [];
2574
2855
  for (const clip of sortedVideoClips(track)) {
2575
2856
  if (clip.type !== "media") continue;
2576
- const words = _optionalChain([assets, 'access', _47 => _47.get, 'call', _48 => _48(clip.asset), 'optionalAccess', _49 => _49.transcript]);
2577
- if (!_optionalChain([words, 'optionalAccess', _50 => _50.length])) continue;
2857
+ const words = _optionalChain([assets, 'access', _50 => _50.get, 'call', _51 => _51(clip.asset), 'optionalAccess', _52 => _52.transcript]);
2858
+ if (!_optionalChain([words, 'optionalAccess', _53 => _53.length])) continue;
2578
2859
  const speed = videoClipSpeed(clip);
2579
2860
  const from = _nullishCoalesce(clip.in, () => ( 0));
2580
2861
  const to = from + clip.duration * speed;
@@ -2685,7 +2966,7 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
2685
2966
  if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
2686
2967
  if (layer.blend) attrs.push(`style="mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}"`);
2687
2968
  if (layer.grade) {
2688
- defs.push(videoGradeSvgFilter(`g${i}`, layer.grade));
2969
+ defs.push(videoGradeSvgFilter(`g${i}`, layer.grade, layer.grade.lut ? _optionalChain([options, 'access', _54 => _54.lutFor, 'optionalCall', _55 => _55(layer.grade.lut.src)]) : null));
2689
2970
  attrs.push(`filter="url(#g${i})"`);
2690
2971
  }
2691
2972
  if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
@@ -2748,69 +3029,96 @@ function videoContactSheetSvg(project, times, options = {}) {
2748
3029
  });
2749
3030
  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>`;
2750
3031
  }
3032
+ function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
3033
+ const { rect } = layer;
3034
+ if (layer.iris) {
3035
+ ctx.beginPath();
3036
+ ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
3037
+ ctx.clip();
3038
+ }
3039
+ if (layer.grade) {
3040
+ const defs = videoGradeSvgDefs(layer.grade);
3041
+ if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
3042
+ const css = videoGradeCssFilter(layer.grade);
3043
+ ctx.filter = options.defineSvgFilter ? css || "none" : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
3044
+ }
3045
+ if (layer.rotation) {
3046
+ ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
3047
+ ctx.rotate(layer.rotation * Math.PI / 180);
3048
+ ctx.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
3049
+ }
3050
+ if (layer.wipe) {
3051
+ const r = videoWipeRect(rect, layer.wipe);
3052
+ ctx.beginPath();
3053
+ ctx.rect(r.x, r.y, r.width, r.height);
3054
+ ctx.clip();
3055
+ }
3056
+ if (layer.type === "color") {
3057
+ ctx.fillStyle = _nullishCoalesce(layer.color, () => ( "#000000"));
3058
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
3059
+ } else if (layer.type === "text" && layer.text) {
3060
+ const t = layer.text;
3061
+ const lines = t.text.split("\n");
3062
+ const lh = t.fontSize * 1.2;
3063
+ const cx = rect.x + rect.width / 2;
3064
+ const top = rect.y + rect.height / 2 - lh * (lines.length - 1) / 2;
3065
+ ctx.font = `${t.fontWeight} ${t.fontSize}px ${t.fontFamily}`;
3066
+ ctx.textAlign = "center";
3067
+ ctx.textBaseline = "middle";
3068
+ if (t.background) {
3069
+ const w = Math.max(...lines.map((l) => ctx.measureText(l).width)) + t.fontSize;
3070
+ const h = lh * lines.length + t.fontSize * 0.4;
3071
+ ctx.fillStyle = t.background;
3072
+ ctx.fillRect(cx - w / 2, rect.y + rect.height / 2 - h / 2, w, h);
3073
+ }
3074
+ ctx.fillStyle = t.color;
3075
+ lines.forEach((l, k) => ctx.fillText(l, cx, top + k * lh));
3076
+ } else {
3077
+ const source = sourceFor(layer);
3078
+ if (source) ctx.drawImage(source, rect.x, rect.y, rect.width, rect.height);
3079
+ else {
3080
+ ctx.fillStyle = "#334155";
3081
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
3082
+ ctx.strokeStyle = "#94a3b8";
3083
+ ctx.lineWidth = 2;
3084
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
3085
+ }
3086
+ }
3087
+ }
2751
3088
  function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
2752
3089
  ctx.save();
2753
3090
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
2754
3091
  ctx.globalAlpha = 1;
2755
3092
  ctx.fillStyle = frame.background;
2756
3093
  ctx.fillRect(0, 0, frame.width, frame.height);
3094
+ const cw = Math.max(1, Math.round(frame.width * scale));
3095
+ const ch = Math.max(1, Math.round(frame.height * scale));
2757
3096
  for (const layer of frame.layers) {
2758
- const { rect } = layer;
3097
+ const setting = _optionalChain([layer, 'access', _56 => _56.grade, 'optionalAccess', _57 => _57.lut]);
3098
+ const lut = setting ? _optionalChain([options, 'access', _58 => _58.lutFor, 'optionalCall', _59 => _59(setting.src)]) : null;
3099
+ const scratch = lut ? _optionalChain([options, 'access', _60 => _60.scratch, 'optionalCall', _61 => _61(cw, ch)]) : null;
2759
3100
  ctx.save();
2760
- ctx.globalAlpha = layer.opacity;
2761
- if (layer.iris) {
2762
- ctx.beginPath();
2763
- ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
2764
- ctx.clip();
2765
- }
2766
- if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
2767
- if (layer.grade) {
2768
- const defs = videoGradeSvgDefs(layer.grade);
2769
- if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
2770
- const css = videoGradeCssFilter(layer.grade);
2771
- ctx.filter = options.defineSvgFilter ? css : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
2772
- }
2773
- if (layer.rotation) {
2774
- ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
2775
- ctx.rotate(layer.rotation * Math.PI / 180);
2776
- ctx.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
2777
- }
2778
- if (layer.wipe) {
2779
- const r = videoWipeRect(rect, layer.wipe);
2780
- ctx.beginPath();
2781
- ctx.rect(r.x, r.y, r.width, r.height);
2782
- ctx.clip();
2783
- }
2784
- if (layer.type === "color") {
2785
- ctx.fillStyle = _nullishCoalesce(layer.color, () => ( "#000000"));
2786
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
2787
- } else if (layer.type === "text" && layer.text) {
2788
- const t = layer.text;
2789
- const lines = t.text.split("\n");
2790
- const lh = t.fontSize * 1.2;
2791
- const cx = rect.x + rect.width / 2;
2792
- const top = rect.y + rect.height / 2 - lh * (lines.length - 1) / 2;
2793
- ctx.font = `${t.fontWeight} ${t.fontSize}px ${t.fontFamily}`;
2794
- ctx.textAlign = "center";
2795
- ctx.textBaseline = "middle";
2796
- if (t.background) {
2797
- const w = Math.max(...lines.map((l) => ctx.measureText(l).width)) + t.fontSize;
2798
- const h = lh * lines.length + t.fontSize * 0.4;
2799
- ctx.fillStyle = t.background;
2800
- ctx.fillRect(cx - w / 2, rect.y + rect.height / 2 - h / 2, w, h);
2801
- }
2802
- ctx.fillStyle = t.color;
2803
- lines.forEach((l, k) => ctx.fillText(l, cx, top + k * lh));
3101
+ if (lut && scratch) {
3102
+ const s = scratch.ctx;
3103
+ s.save();
3104
+ s.setTransform(1, 0, 0, 1, 0, 0);
3105
+ s.clearRect(0, 0, cw, ch);
3106
+ s.setTransform(scale, 0, 0, scale, 0, 0);
3107
+ s.globalAlpha = 1;
3108
+ s.globalCompositeOperation = "source-over";
3109
+ paintVideoLayer(s, frame, layer, sourceFor, options);
3110
+ s.restore();
3111
+ const img = s.getImageData(0, 0, cw, ch);
3112
+ applyVideoLutToPixels(lut, img.data, _nullishCoalesce(setting.strength, () => ( 1)));
3113
+ s.putImageData(img, 0, 0);
3114
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
3115
+ ctx.globalAlpha = layer.opacity;
3116
+ if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
3117
+ ctx.drawImage(scratch.canvas, 0, 0, cw, ch);
2804
3118
  } else {
2805
- const source = sourceFor(layer);
2806
- if (source) ctx.drawImage(source, rect.x, rect.y, rect.width, rect.height);
2807
- else {
2808
- ctx.fillStyle = "#334155";
2809
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
2810
- ctx.strokeStyle = "#94a3b8";
2811
- ctx.lineWidth = 2;
2812
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
2813
- }
3119
+ ctx.globalAlpha = layer.opacity;
3120
+ if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
3121
+ paintVideoLayer(ctx, frame, layer, sourceFor, options);
2814
3122
  }
2815
3123
  ctx.restore();
2816
3124
  }
@@ -2818,7 +3126,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
2818
3126
  }
2819
3127
 
2820
3128
  // src/video-editor/export.ts
2821
- var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _51 => _51.name]), () => ( _optionalChain([asset, 'optionalAccess', _52 => _52.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
3129
+ var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _62 => _62.name]), () => ( _optionalChain([asset, 'optionalAccess', _63 => _63.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
2822
3130
  function toVideoEdl(project, options = {}) {
2823
3131
  const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
2824
3132
  if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
@@ -2853,7 +3161,7 @@ function toVideoEdl(project, options = {}) {
2853
3161
  lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2854
3162
  } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2855
3163
  if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
2856
- if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _53 => _53.name]), () => ( _optionalChain([asset, 'optionalAccess', _54 => _54.src]))), () => ( clip.asset))}`);
3164
+ if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _64 => _64.name]), () => ( _optionalChain([asset, 'optionalAccess', _65 => _65.src]))), () => ( clip.asset))}`);
2857
3165
  if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
2858
3166
  if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
2859
3167
  lines.push("");
@@ -3049,7 +3357,7 @@ function buildFfmpeg(project, options, measure) {
3049
3357
  if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
3050
3358
  if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
3051
3359
  if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
3052
- if (_optionalChain([next2, 'optionalAccess', _55 => _55.transition, 'access', _56 => _56.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
3360
+ if (_optionalChain([next2, 'optionalAccess', _66 => _66.transition, 'access', _67 => _67.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
3053
3361
  if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
3054
3362
  if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
3055
3363
  chain2.push(`adelay=${Math.round(t0)}:all=1`);
@@ -3062,7 +3370,7 @@ function buildFfmpeg(project, options, measure) {
3062
3370
  const onTimeline = `(t*1000-${Math.round(clip.start)})`;
3063
3371
  const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
3064
3372
  const tf = clip.transform;
3065
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _57 => _57[p]])));
3373
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _68 => _68[p]])));
3066
3374
  if (clip.type === "text" && options.titleImage) {
3067
3375
  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 })]);
3068
3376
  index = inputs.length - 1;
@@ -3071,7 +3379,7 @@ function buildFfmpeg(project, options, measure) {
3071
3379
  const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
3072
3380
  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("*");
3073
3381
  const coord = (axis) => {
3074
- const v = _optionalChain([tf, 'optionalAccess', _58 => _58[axis]]);
3382
+ const v = _optionalChain([tf, 'optionalAccess', _69 => _69[axis]]);
3075
3383
  const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
3076
3384
  return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
3077
3385
  };
@@ -3079,7 +3387,7 @@ function buildFfmpeg(project, options, measure) {
3079
3387
  const opts = [
3080
3388
  `text=${ffmpegFilterValue(clip.text)}`,
3081
3389
  options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
3082
- isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _59 => _59.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _60 => _60.scale]), () => ( 1))))}`,
3390
+ isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _70 => _70.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _71 => _71.scale]), () => ( 1))))}`,
3083
3391
  `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
3084
3392
  coord("x"),
3085
3393
  coord("y"),
@@ -3087,8 +3395,8 @@ function buildFfmpeg(project, options, measure) {
3087
3395
  ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
3088
3396
  `enable='between(t,${sec(clip.start)},${sec(end)})'`
3089
3397
  ];
3090
- if (_optionalChain([tf, 'optionalAccess', _61 => _61.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
3091
- if (videoGradeFfmpegFilters(clip.grade).length) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
3398
+ if (_optionalChain([tf, 'optionalAccess', _72 => _72.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
3399
+ if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _73 => _73.grade, 'optionalAccess', _74 => _74.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
3092
3400
  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.`);
3093
3401
  graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
3094
3402
  cur = next;
@@ -3108,6 +3416,20 @@ function buildFfmpeg(project, options, measure) {
3108
3416
  }
3109
3417
  const grade = videoGradeFfmpegFilters(clip.grade);
3110
3418
  if (grade.length) chain.push("format=rgba", ...grade);
3419
+ const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _75 => _75.grade, 'optionalAccess', _76 => _76.lut]));
3420
+ if (lut) {
3421
+ chain.push("format=gbrap");
3422
+ const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
3423
+ if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
3424
+ const file = ffmpegFilterValue(path);
3425
+ const into2 = `q${next}`;
3426
+ graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
3427
+ const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
3428
+ if ((_nullishCoalesce(lut.strength, () => ( 1))) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
3429
+ 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}]`);
3430
+ source = `[r${next}]`;
3431
+ chain.length = 0;
3432
+ }
3111
3433
  chain.push("format=yuva420p");
3112
3434
  if (isVideoKeyframes(clip.opacity)) chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='alpha(X,Y)*clip(${expr(clip.opacity, onChain("T"), 1, "opacity")},0,1)'`);
3113
3435
  else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
@@ -3125,10 +3447,10 @@ function buildFfmpeg(project, options, measure) {
3125
3447
  const r = videoIrisRadius({ width: W, height: H }, 1);
3126
3448
  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)'`);
3127
3449
  }
3128
- const rotation = _optionalChain([placed, 'optionalAccess', _62 => _62.rotation]);
3450
+ const rotation = _optionalChain([placed, 'optionalAccess', _77 => _77.rotation]);
3129
3451
  if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
3130
3452
  else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
3131
- if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _63 => _63.scale]))) {
3453
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _78 => _78.scale]))) {
3132
3454
  const s = expr(placed.scale, onChain("t"), 1, "scale");
3133
3455
  chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
3134
3456
  }
@@ -3147,9 +3469,9 @@ function buildFfmpeg(project, options, measure) {
3147
3469
  const into = lead ? clip.transition.type : void 0;
3148
3470
  if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
3149
3471
  const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
3150
- if (_optionalChain([after, 'optionalAccess', _64 => _64.transition, 'access', _65 => _65.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
3472
+ if (_optionalChain([after, 'optionalAccess', _79 => _79.transition, 'access', _80 => _80.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
3151
3473
  const at = (axis, c) => {
3152
- const v = _optionalChain([placed, 'optionalAccess', _66 => _66[axis]]);
3474
+ const v = _optionalChain([placed, 'optionalAccess', _81 => _81[axis]]);
3153
3475
  const extra = shifts[axis].map((s) => `+${s}`).join("");
3154
3476
  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}'`;
3155
3477
  };
@@ -3183,7 +3505,7 @@ function buildFfmpeg(project, options, measure) {
3183
3505
  if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
3184
3506
  else if (options.loudness && !options.loudness.measured)
3185
3507
  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.");
3186
- if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.kind]) === "video" && _optionalChain([assets, 'access', _70 => _70.get, 'call', _71 => _71(c.asset), 'optionalAccess', _72 => _72.hasAudio]) === void 0)))
3508
+ if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _82 => _82.get, 'call', _83 => _83(c.asset), 'optionalAccess', _84 => _84.kind]) === "video" && _optionalChain([assets, 'access', _85 => _85.get, 'call', _86 => _86(c.asset), 'optionalAccess', _87 => _87.hasAudio]) === void 0)))
3187
3509
  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.');
3188
3510
  if (animated.size)
3189
3511
  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)" : ""}.`);
@@ -3218,13 +3540,16 @@ function buildFfmpeg(project, options, measure) {
3218
3540
  }
3219
3541
 
3220
3542
  // src/video-editor/preview.ts
3221
- var VideoPreviewEngine = (_class4 = class {
3222
- constructor(source, canvas2, options = {}) {;_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);
3543
+ var VideoPreviewEngine = (_class5 = class {
3544
+ constructor(source, canvas2, options = {}) {;_class5.prototype.__init13.call(this);_class5.prototype.__init14.call(this);_class5.prototype.__init15.call(this);_class5.prototype.__init16.call(this);_class5.prototype.__init17.call(this);_class5.prototype.__init18.call(this);_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);
3223
3545
  this.source = source;
3224
3546
  this.options = options;
3225
3547
  this.canvas = canvas2;
3226
- this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _73 => _73.getContext, 'optionalCall', _74 => _74("2d")]), () => ( null));
3548
+ this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _91 => _91.getContext, 'optionalCall', _92 => _92("2d")]), () => ( null));
3227
3549
  this.unsubscribe = source.subscribe(() => this.update());
3550
+ this.lutCache = options.luts !== void 0 ? options.luts : _nullishCoalesce(source.luts, () => ( null));
3551
+ this.unsubscribeLuts = this.lutCache ? this.lutCache.subscribe(() => this.redraw()) : () => {
3552
+ };
3228
3553
  const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
3229
3554
  this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
3230
3555
  };
@@ -3235,21 +3560,24 @@ var VideoPreviewEngine = (_class4 = class {
3235
3560
 
3236
3561
 
3237
3562
 
3238
- __init9() {this.media = /* @__PURE__ */ new Map()}
3239
- __init10() {this.images = /* @__PURE__ */ new Map()}
3563
+ __init13() {this.media = /* @__PURE__ */ new Map()}
3564
+ __init14() {this.images = /* @__PURE__ */ new Map()}
3565
+
3566
+ __init15() {this.scheduled = false}
3567
+ __init16() {this.destroyed = false}
3568
+ __init17() {this.filterHost = null}
3569
+ __init18() {this.filters = /* @__PURE__ */ new Set()}
3570
+
3571
+
3572
+ __init19() {this.drawn = null}
3573
+ __init20() {this.complete = true}
3240
3574
 
3241
- __init11() {this.scheduled = false}
3242
- __init12() {this.destroyed = false}
3243
- __init13() {this.filterHost = null}
3244
- __init14() {this.filters = /* @__PURE__ */ new Set()}
3245
3575
 
3246
3576
 
3247
- __init15() {this.drawn = null}
3248
- __init16() {this.complete = true}
3249
3577
  /** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
3250
3578
  readPixels() {
3251
3579
  const ctx = this.ctx;
3252
- if (!_optionalChain([ctx, 'optionalAccess', _75 => _75.getImageData]) || !this.drawn) return null;
3580
+ if (!_optionalChain([ctx, 'optionalAccess', _93 => _93.getImageData]) || !this.drawn) return null;
3253
3581
  return { ...this.drawn, pixels: ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) };
3254
3582
  }
3255
3583
  createMedia(kind) {
@@ -3287,8 +3615,8 @@ var VideoPreviewEngine = (_class4 = class {
3287
3615
  entry.media.pause();
3288
3616
  entry.media.removeEventListener("seeked", entry.onReady);
3289
3617
  entry.media.removeEventListener("loadeddata", entry.onReady);
3290
- _optionalChain([entry, 'access', _76 => _76.media, 'access', _77 => _77.removeAttribute, 'optionalCall', _78 => _78("src")]);
3291
- _optionalChain([entry, 'access', _79 => _79.media, 'access', _80 => _80.load, 'optionalCall', _81 => _81()]);
3618
+ _optionalChain([entry, 'access', _94 => _94.media, 'access', _95 => _95.removeAttribute, 'optionalCall', _96 => _96("src")]);
3619
+ _optionalChain([entry, 'access', _97 => _97.media, 'access', _98 => _98.load, 'optionalCall', _99 => _99()]);
3292
3620
  this.media.delete(entry.clip);
3293
3621
  }
3294
3622
  /** Brings the media elements in step with the editor and draws the frame. */
@@ -3322,7 +3650,7 @@ var VideoPreviewEngine = (_class4 = class {
3322
3650
  for (const layer of frame.layers) {
3323
3651
  if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
3324
3652
  const sound = sounding.get(layer.clip);
3325
- sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _82 => _82.speed]), () => ( 1)), sound ? sound.gain : null);
3653
+ sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _100 => _100.speed]), () => ( 1)), sound ? sound.gain : null);
3326
3654
  sounding.delete(layer.clip);
3327
3655
  }
3328
3656
  for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
@@ -3337,7 +3665,7 @@ var VideoPreviewEngine = (_class4 = class {
3337
3665
  if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
3338
3666
  });
3339
3667
  }
3340
- __init17() {this.sourceFor = (layer) => {
3668
+ __init21() {this.sourceFor = (layer) => {
3341
3669
  if (layer.assetKind === "image" && layer.src) {
3342
3670
  const img = this.image(layer.src);
3343
3671
  return img.complete ? img : null;
@@ -3362,12 +3690,34 @@ var VideoPreviewEngine = (_class4 = class {
3362
3690
  if (!source) this.complete = false;
3363
3691
  return source;
3364
3692
  };
3365
- drawVideoFrame(this.ctx, frame, sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
3693
+ drawVideoFrame(this.ctx, frame, sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter, lutFor: this.lutFor, scratch: this.scratch });
3366
3694
  this.drawn = { time: frame.time, complete: this.complete };
3367
- _optionalChain([this, 'access', _83 => _83.options, 'access', _84 => _84.onFrame, 'optionalCall', _85 => _85(frame)]);
3368
- _optionalChain([this, 'access', _86 => _86.pixelsTarget, 'optionalAccess', _87 => _87.drawn, 'call', _88 => _88(frame.time, this.complete)]);
3369
- }
3370
- __init18() {this.defineSvgFilter = (id, markup) => {
3695
+ _optionalChain([this, 'access', _101 => _101.options, 'access', _102 => _102.onFrame, 'optionalCall', _103 => _103(frame)]);
3696
+ _optionalChain([this, 'access', _104 => _104.pixelsTarget, 'optionalAccess', _105 => _105.drawn, 'call', _106 => _106(frame.time, this.complete)]);
3697
+ }
3698
+ /** A grade's LUT once loaded; while it loads (or if it fails) the frame is drawn without it and counts as incomplete. */
3699
+ __init22() {this.lutFor = (src) => {
3700
+ const lut = _optionalChain([this, 'access', _107 => _107.lutCache, 'optionalAccess', _108 => _108.get, 'call', _109 => _109(src)]);
3701
+ if (!lut) this.complete = false;
3702
+ return lut;
3703
+ }}
3704
+ __init23() {this.scratch = (width, height) => {
3705
+ if (this.scratchCanvas === void 0) {
3706
+ if (this.options.createScratch) this.scratchCanvas = this.options.createScratch(width, height);
3707
+ else if (typeof document !== "undefined") {
3708
+ const canvas2 = document.createElement("canvas");
3709
+ const ctx = canvas2.getContext("2d", { willReadFrequently: true });
3710
+ this.scratchCanvas = ctx ? { canvas: canvas2, ctx } : null;
3711
+ } else this.scratchCanvas = null;
3712
+ }
3713
+ const s = this.scratchCanvas;
3714
+ if (!s) return null;
3715
+ const c = s.canvas;
3716
+ if (c.width !== width) c.width = width;
3717
+ if (c.height !== height) c.height = height;
3718
+ return s;
3719
+ }}
3720
+ __init24() {this.defineSvgFilter = (id, markup) => {
3371
3721
  if (this.options.defineSvgFilter === null) return;
3372
3722
  if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
3373
3723
  if (this.filters.has(id) || typeof document === "undefined") return;
@@ -3387,37 +3737,41 @@ var VideoPreviewEngine = (_class4 = class {
3387
3737
  destroy() {
3388
3738
  this.destroyed = true;
3389
3739
  this.detachPixels();
3390
- _optionalChain([this, 'access', _89 => _89.filterHost, 'optionalAccess', _90 => _90.remove, 'call', _91 => _91()]);
3740
+ this.unsubscribeLuts();
3741
+ _optionalChain([this, 'access', _110 => _110.filterHost, 'optionalAccess', _111 => _111.remove, 'call', _112 => _112()]);
3391
3742
  this.filterHost = null;
3392
3743
  this.filters.clear();
3393
3744
  this.unsubscribe();
3394
3745
  for (const entry of [...this.media.values()]) this.release(entry);
3395
3746
  this.images.clear();
3396
3747
  }
3397
- }, _class4);
3748
+ }, _class5);
3398
3749
 
3399
3750
  // src/video-editor/controller.ts
3400
- var VideoEditorController = (_class5 = class {
3751
+ var VideoEditorController = (_class6 = class {
3401
3752
 
3402
3753
 
3403
3754
  /** Sound levels per asset, loaded on demand for waveforms and agents. */
3404
3755
 
3405
3756
  /** The pixels the preview last drew, for scopes and `get_video_scopes` (a `VideoPreviewEngine` fills it). */
3406
- __init19() {this.previewPixels = new VideoPreviewPixels()}
3757
+ __init25() {this.previewPixels = new VideoPreviewPixels()}
3758
+ /** Grades' LUTs, loaded on demand for the preview, SVG frames and agents. */
3407
3759
 
3408
3760
 
3409
- __init20() {this.selection = []}
3410
3761
 
3411
- __init21() {this.error = null}
3412
- __init22() {this.revision = 0}
3413
- __init23() {this.applying = false}
3762
+ __init26() {this.selection = []}
3414
3763
 
3415
- __init24() {this.listeners = /* @__PURE__ */ new Set()}
3416
- __init25() {this.unsubscribers = []}
3764
+ __init27() {this.error = null}
3765
+ __init28() {this.revision = 0}
3766
+ __init29() {this.applying = false}
3417
3767
 
3418
- constructor(options = {}) {;_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);_class5.prototype.__init25.call(this);_class5.prototype.__init26.call(this);_class5.prototype.__init27.call(this);
3768
+ __init30() {this.listeners = /* @__PURE__ */ new Set()}
3769
+ __init31() {this.unsubscribers = []}
3770
+
3771
+ constructor(options = {}) {;_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);_class6.prototype.__init27.call(this);_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);
3419
3772
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
3420
3773
  this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
3774
+ this.luts = options.luts instanceof VideoLutCache ? options.luts : new VideoLutCache(options.luts);
3421
3775
  this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
3422
3776
  this.issues = checkVideoProject(this.project);
3423
3777
  this.readOnly = _nullishCoalesce(options.readOnly, () => ( false));
@@ -3440,8 +3794,8 @@ var VideoEditorController = (_class5 = class {
3440
3794
  this.player.pause();
3441
3795
  this.unsubscribers.splice(0).forEach((u) => u());
3442
3796
  }
3443
- __init26() {this.getState = () => this.state}
3444
- __init27() {this.subscribe = (listener) => {
3797
+ __init32() {this.getState = () => this.state}
3798
+ __init33() {this.subscribe = (listener) => {
3445
3799
  this.connect();
3446
3800
  this.listeners.add(listener);
3447
3801
  return () => {
@@ -3497,7 +3851,7 @@ var VideoEditorController = (_class5 = class {
3497
3851
  this.applying = false;
3498
3852
  }
3499
3853
  this.notify();
3500
- _optionalChain([this, 'access', _92 => _92.onChange, 'optionalCall', _93 => _93(project)]);
3854
+ _optionalChain([this, 'access', _113 => _113.onChange, 'optionalCall', _114 => _114(project)]);
3501
3855
  }
3502
3856
  commit(label, next) {
3503
3857
  const before = this.project;
@@ -3561,7 +3915,7 @@ var VideoEditorController = (_class5 = class {
3561
3915
  }
3562
3916
  /** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
3563
3917
  select(clips, options = {}) {
3564
- const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _94 => _94.filter, 'call', _95 => _95((id) => findVideoClip(this.project, id))]), () => ( []));
3918
+ const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _115 => _115.filter, 'call', _116 => _116((id) => findVideoClip(this.project, id))]), () => ( []));
3565
3919
  let next;
3566
3920
  if (options.add) {
3567
3921
  next = [...this.selection];
@@ -3628,7 +3982,7 @@ var VideoEditorController = (_class5 = class {
3628
3982
  }
3629
3983
  /** The frame at `time` (default: the playhead) as SVG. */
3630
3984
  toSvg(time = this.player.currentTime, options = {}) {
3631
- return videoProjectSvg(this.project, time, options);
3985
+ return videoProjectSvg(this.project, time, { lutFor: (src) => this.luts.peek(src), ...options });
3632
3986
  }
3633
3987
  /** The project as an ffmpeg command. */
3634
3988
  toFfmpeg(options = {}) {
@@ -3642,118 +3996,356 @@ var VideoEditorController = (_class5 = class {
3642
3996
  toEdl(options = {}) {
3643
3997
  return toVideoEdl(this.project, options);
3644
3998
  }
3645
- }, _class5);
3999
+ }, _class6);
3646
4000
 
3647
- // src/video-editor/tools.ts
3648
- var round = (ms) => Math.round(ms * 10) / 10;
3649
- var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3650
- var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
3651
- var WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3652
- var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
3653
- var object = (properties, required = [], description) => ({
3654
- type: "object",
3655
- ...description ? { description } : {},
3656
- properties,
3657
- ...required.length ? { required } : {},
3658
- additionalProperties: false
3659
- });
3660
- var MS = (what) => ({ type: "number", minimum: 0, description: `${what}, in milliseconds.` });
3661
- var CLIP_ID = { type: "string", minLength: 1, description: 'A clip id from get_video_project (e.g. "c1").' };
3662
- var TRACK_ID = { type: "string", minLength: 1, description: 'A track id from get_video_project (e.g. "v1" or "a1").' };
3663
- var ASSET_ID = { type: "string", minLength: 1, description: "An asset id from get_video_project." };
3664
- var COLOR = (what) => ({ type: "string", minLength: 1, description: `${what}: a CSS colour such as "#ffcc00".` });
3665
- var RIPPLE = {
3666
- type: "boolean",
3667
- description: "Push (or pull) later material along by the change, keeping the cuts after it in sync (default false: refuse if it would overlap). When the track is sync-locked (the default), every other unlocked sync-locked track moves too: clips across the edit point are cut when time opens, and closing time is refused if another sync-locked track has a clip in that stretch."
3668
- };
3669
- var MODE = {
3670
- type: "string",
3671
- enum: [...VIDEO_PLACEMENT_MODES],
3672
- description: 'What to do with clips already where it lands: "refuse" (default) fails with the next free place; "overwrite" cuts, trims or removes what is underneath on that track; "insert" cuts everything across the start and pushes later material along by its length on this track and every sync-locked track.'
3673
- };
3674
- var DELTA = (what) => ({ type: "number", description: `${what}, in milliseconds of timeline time: positive is later, negative earlier. It is clamped to what the media and neighbours allow; the result says how far it went.` });
3675
- var RANGE_TRACKS = { type: "array", items: TRACK_ID, minItems: 1, uniqueItems: true, description: "Tracks to work on (default: every unlocked track). Locked tracks are refused." };
3676
- var TRANSFORM = object(
3677
- {
3678
- x: { type: "number", description: "Offset of the centre to the right of the frame's centre, in pixels (default 0)." },
3679
- y: { type: "number", description: "Offset of the centre below the frame's centre, in pixels (default 0)." },
3680
- scale: { type: "number", minimum: 0, description: "Size after fitting (1 = fitted size, default 1)." },
3681
- rotation: { type: "number", description: "Degrees clockwise (default 0)." }
3682
- },
3683
- [],
3684
- "Where the picture sits after it is fitted."
3685
- );
3686
- var TRANSITION = object(
3687
- {
3688
- type: { type: "string", enum: [...VIDEO_TRANSITION_TYPES], description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre)." },
3689
- duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. Every type but dip needs half of it in media past the cut on both sides (handles); a dip needs none." },
3690
- color: COLOR("For dip: the colour passed through (default black)"),
3691
- direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
3692
- audioCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fades through it: default equal-power for every type but dip (steady loudness while the two sounds overlap), linear for dip; or s-curve, exponential, logarithmic." }
3693
- },
3694
- ["type", "duration"],
3695
- "A transition from the clip that ends where this one starts on the same track."
3696
- );
3697
- var GRADE = object(
3698
- Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, { type: "number", minimum: c.min, maximum: c.max, description: c.description }])),
3699
- [],
3700
- "Colour correction of the picture, applied in order: exposure, contrast, saturation, hue, white balance (temperature, tint). Leave a setting out for no change; the object replaces the clip's whole grade."
3701
- );
3702
- var ANIMATION = (when) => object(
3703
- {
3704
- effect: { type: "string", enum: [...VIDEO_CLIP_ANIMATION_EFFECTS], description: "fade; rise (up into place) or drop (down into place); slide-left / slide-right (moving that way); zoom (grows into place); pop (grows past its size and settles). Each also fades; an exit plays the movement backwards." },
3705
- duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default 500)." }
3706
- },
3707
- ["effect"],
3708
- when
3709
- );
3710
- var CLIP_COMMON = {
3711
- name: { type: "string", description: "A label for the clip." },
3712
- opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1). A number replaces any keyframes; set_video_keyframes animates it." },
3713
- transform: TRANSFORM,
3714
- fit: { type: "string", enum: [...VIDEO_FITS], description: "How the picture fits the frame before the transform, as CSS object-fit (default contain for media, fill for colours)." },
3715
- grade: GRADE,
3716
- animateIn: ANIMATION("How the picture enters, from the clip's start (on top of any fade in). Titles usually rise, pop or fade in."),
3717
- animateOut: ANIMATION("How the picture leaves, ending at the clip's end."),
3718
- blend: { type: "string", enum: [...VIDEO_BLEND_MODES], description: "How the picture mixes with what is beneath it, as CSS mix-blend-mode (default normal): multiply darkens (shadows, textures), screen and add lighten (light leaks, flares, glows), overlay and soft-light add contrast, difference and exclusion invert." },
3719
- fadeIn: MS("Fade up from nothing (picture and sound) over this long"),
3720
- fadeOut: MS("Fade down to nothing over this long")
3721
- };
3722
- var MEDIA_FIELDS = {
3723
- in: MS("Where in the source it starts (default 0)"),
3724
- speed: { type: "number", minimum: 0.05, maximum: 20, description: "Playback speed (default 1). Changing it keeps the stretch of source, so the clip gets shorter or longer." },
3725
- volume: { type: "number", minimum: 0, maximum: 4, description: "Gain (1 = unchanged, default 1). A number replaces any keyframes; set_video_keyframes animates it." },
3726
- muted: { type: "boolean", description: "Silence this clip (default false)." },
3727
- fadeInCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fade in (default linear; the picture always fades linearly): linear, equal-power (steady loudness, best for crossfades), s-curve (gentle at both ends), exponential (slow start, natural from silence) or logarithmic (fast start)." },
3728
- fadeOutCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fade out (default linear), the same choices as fadeInCurve; exponential sounds natural fading to silence." }
4001
+ // src/video-editor/ui.ts
4002
+ var STEPS = [40, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 15e3, 3e4, 6e4, 12e4, 3e5, 6e5];
4003
+ function videoRulerTicks(duration, pxPerSecond, fps) {
4004
+ const pxPerMs = pxPerSecond / 1e3;
4005
+ const major = _nullishCoalesce(STEPS.find((s) => s * pxPerMs >= 80), () => ( STEPS[STEPS.length - 1]));
4006
+ const minor = STEPS.slice().reverse().find((s) => s < major && major % s === 0 && s * pxPerMs >= 10);
4007
+ const step = _nullishCoalesce(minor, () => ( major));
4008
+ const ticks = [];
4009
+ for (let t = 0; t <= duration + 1e-6; t += step) {
4010
+ const isMajor = Math.abs(t / major - Math.round(t / major)) < 1e-9;
4011
+ ticks.push({ time: t, x: t * pxPerMs, major: isMajor, ...isMajor ? { label: formatVideoTimecode(t, fps).replace(/^00:/, "") } : {} });
4012
+ }
4013
+ return ticks;
4014
+ }
4015
+ function videoClipShowsWaveform(clip, asset, mode = "audio") {
4016
+ if (clip.type !== "media" || mode === "none" || !videoAssetHasSound(asset)) return false;
4017
+ return mode === "all" || asset.kind === "audio";
4018
+ }
4019
+ var VIDEO_CLIP_ANIMATION_LABELS = {
4020
+ fade: "Fade",
4021
+ rise: "Rise",
4022
+ drop: "Drop",
4023
+ "slide-left": "Slide left",
4024
+ "slide-right": "Slide right",
4025
+ zoom: "Zoom",
4026
+ pop: "Pop"
3729
4027
  };
3730
- var TEXT_FIELDS = {
3731
- text: { type: "string", minLength: 1, description: `The title's text ("\\n" breaks lines).` },
3732
- color: COLOR("Text colour (default white)"),
3733
- fontSize: { type: "number", exclusiveMinimum: 0, description: "Pixels (default 64)." },
3734
- fontFamily: { type: "string", description: "CSS font family (default sans-serif)." },
3735
- fontWeight: { type: "number", minimum: 100, maximum: 1e3, description: "CSS weight (default 700)." },
3736
- background: COLOR("A box behind the text (default none)")
4028
+ var VIDEO_TRANSITION_LABELS = {
4029
+ crossfade: "Crossfade",
4030
+ dip: "Dip to colour",
4031
+ wipe: "Wipe",
4032
+ push: "Push",
4033
+ slide: "Slide",
4034
+ iris: "Iris"
3737
4035
  };
3738
- var PLACEMENT = {
3739
- id: { type: "string", minLength: 1, description: 'Id for the clip (default: a fresh one such as "c3").' },
3740
- start: MS("Where it starts on the timeline (default: the end of the track)"),
3741
- duration: { type: "number", exclusiveMinimum: 0, description: "Length on the timeline, in ms (default: the rest of the source for media, 5000 for stills, 3000 for titles and colours)." }
4036
+ var videoTransitionHasDirection = (type) => type === "wipe" || type === "push" || type === "slide";
4037
+ var VIDEO_BLEND_MODE_LABELS = {
4038
+ normal: "Normal",
4039
+ multiply: "Multiply",
4040
+ screen: "Screen",
4041
+ overlay: "Overlay",
4042
+ darken: "Darken",
4043
+ lighten: "Lighten",
4044
+ "color-dodge": "Colour dodge",
4045
+ "color-burn": "Colour burn",
4046
+ "hard-light": "Hard light",
4047
+ "soft-light": "Soft light",
4048
+ difference: "Difference",
4049
+ exclusion: "Exclusion",
4050
+ add: "Add"
3742
4051
  };
3743
- var NEW_CLIP = {
3744
- description: "The clip: media (a stretch of an asset), text (a title) or color (a solid fill).",
3745
- oneOf: [
3746
- object({ type: { const: "media", description: "A stretch of an asset." }, asset: ASSET_ID, ...PLACEMENT, ...CLIP_COMMON, ...MEDIA_FIELDS }, ["type", "asset"], "A media clip."),
3747
- object({ type: { const: "text", description: "A title over the picture." }, ...PLACEMENT, ...CLIP_COMMON, ...TEXT_FIELDS }, ["type", "text"], "A title."),
3748
- object({ type: { const: "color", description: "A solid colour filling the frame." }, color: COLOR("The fill"), ...PLACEMENT, ...CLIP_COMMON }, ["type", "color"], "A colour.")
3749
- ]
4052
+ var VIDEO_FADE_CURVE_LABELS = {
4053
+ linear: "Linear",
4054
+ "equal-power": "Equal power",
4055
+ "s-curve": "S-curve",
4056
+ exponential: "Exponential",
4057
+ logarithmic: "Logarithmic"
3750
4058
  };
3751
- var SETTINGS = {
3752
- name: { type: "string", description: "Project name." },
3753
- width: { type: "integer", minimum: 16, maximum: 8192, description: "Frame width in pixels." },
3754
- height: { type: "integer", minimum: 16, maximum: 8192, description: "Frame height in pixels." },
3755
- fps: { type: "number", exclusiveMinimum: 0, maximum: 240, description: "Frames per second (e.g. 24, 25, 30, 60)." },
3756
- background: COLOR("What shows where no clip covers the frame (default black)")
4059
+ function videoClipTint(clip, asset) {
4060
+ if (clip.type === "text") return "#a855f7";
4061
+ if (clip.type === "color") return clip.color;
4062
+ if (!asset) return "#ef4444";
4063
+ return asset.kind === "audio" ? "#10b981" : asset.kind === "image" ? "#f59e0b" : "#3b82f6";
4064
+ }
4065
+ function videoClipLabel(clip, asset) {
4066
+ if (clip.name) return clip.name;
4067
+ if (clip.type === "text") return clip.text.split("\n")[0];
4068
+ if (clip.type === "color") return clip.color;
4069
+ const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _117 => _117.name]), () => ( _optionalChain([asset, 'optionalAccess', _118 => _118.src, 'access', _119 => _119.replace, 'call', _120 => _120(/^.*[\\/]/, "")]))), () => ( clip.asset));
4070
+ const speed = videoClipSpeed(clip);
4071
+ return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
4072
+ }
4073
+ function planVideoClipDrag(project, input) {
4074
+ const at = findVideoClip(project, input.clip);
4075
+ if (!at) return null;
4076
+ const { clip, track } = at;
4077
+ const snap = _nullishCoalesce(input.snap, () => ( ((t) => t)));
4078
+ if (input.mode === "move") {
4079
+ const raw = Math.max(0, clip.start + input.delta);
4080
+ const head = snap(raw) - raw;
4081
+ const tail = snap(raw + clip.duration) - (raw + clip.duration);
4082
+ const shifts = [head, tail].filter((d) => Math.abs(d) > 1e-9);
4083
+ const start = Math.max(0, raw + (shifts.length ? shifts.reduce((a, b) => Math.abs(b) < Math.abs(a) ? b : a) : 0));
4084
+ const target = _nullishCoalesce(input.track, () => ( track.id));
4085
+ const moved = Math.abs(start - clip.start) > 1e-6 || target !== track.id;
4086
+ const mode = input.placement && input.placement !== "refuse" ? { mode: input.placement } : {};
4087
+ return { start, duration: clip.duration, track: target, edit: moved ? { op: "move_clip", clip: clip.id, start, ...target !== track.id ? { track: target } : {}, ...mode } : null };
4088
+ }
4089
+ const minimum = 1e3 / project.fps;
4090
+ if (input.mode === "trim-start") {
4091
+ const to2 = Math.min(snap(Math.max(0, clip.start + input.delta)), videoClipEnd(clip) - minimum);
4092
+ const changed2 = Math.abs(to2 - clip.start) > 1e-6;
4093
+ const plan = input.ripple ? { start: clip.start, duration: clip.duration - (to2 - clip.start) } : { start: to2, duration: videoClipEnd(clip) - to2 };
4094
+ return { ...plan, track: track.id, edit: changed2 ? { op: "trim_clip", clip: clip.id, edge: "start", to: to2, ...input.ripple ? { ripple: true } : {} } : null };
4095
+ }
4096
+ const to = Math.max(snap(videoClipEnd(clip) + input.delta), clip.start + minimum);
4097
+ const changed = Math.abs(to - videoClipEnd(clip)) > 1e-6;
4098
+ return { start: clip.start, duration: to - clip.start, track: track.id, edit: changed ? { op: "trim_clip", clip: clip.id, edge: "end", to, ...input.ripple ? { ripple: true } : {} } : null };
4099
+ }
4100
+ function videoDropTracks(project, clipId) {
4101
+ const at = findVideoClip(project, clipId);
4102
+ if (!at) return [];
4103
+ return project.tracks.filter((t) => t.kind === at.track.kind && !t.locked).map((t) => t.id);
4104
+ }
4105
+ function videoDragLabel(edit) {
4106
+ if (edit.op === "move_clip") return edit.mode === "insert" ? "Insert clip" : edit.mode === "overwrite" ? "Overwrite clip" : "Move clip";
4107
+ return edit.op === "trim_clip" && edit.ripple ? "Ripple trim" : "Trim clip";
4108
+ }
4109
+ function videoAssetDropEdit(project, assetId, options = {}) {
4110
+ const asset = videoAssetMap(project).get(assetId);
4111
+ if (!asset) return null;
4112
+ const kind = asset.kind === "audio" ? "audio" : "video";
4113
+ const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _121 => _121.tracks, 'access', _122 => _122.find, 'call', _123 => _123((t) => t.kind === kind && !t.locked), 'optionalAccess', _124 => _124.id])));
4114
+ if (!track) return null;
4115
+ return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
4116
+ }
4117
+ function videoAssetKindOf(typeOrName) {
4118
+ const s = typeOrName.toLowerCase().replace(/[?#].*$/, "");
4119
+ if (s.startsWith("video/") || /\.(mp4|m4v|mov|webm|ogv|mkv)$/.test(s)) return "video";
4120
+ if (s.startsWith("audio/") || /\.(mp3|m4a|aac|wav|ogg|oga|flac|opus)$/.test(s)) return "audio";
4121
+ if (s.startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg|bmp)$/.test(s)) return "image";
4122
+ return null;
4123
+ }
4124
+ function probeVideoMedia(src, kind, timeout = 15e3) {
4125
+ return new Promise((resolve, reject) => {
4126
+ if (typeof document === "undefined") return reject(new Error("Media can only be probed in a browser."));
4127
+ const timer = setTimeout(() => reject(new Error(`"${src}" took too long to load.`)), timeout);
4128
+ const done = (probe) => {
4129
+ clearTimeout(timer);
4130
+ resolve(probe);
4131
+ };
4132
+ const failed = () => {
4133
+ clearTimeout(timer);
4134
+ reject(new Error(`"${src}" could not be loaded as ${kind}.`));
4135
+ };
4136
+ if (kind === "image") {
4137
+ const img = new Image();
4138
+ img.onload = () => done({ kind, width: img.naturalWidth, height: img.naturalHeight });
4139
+ img.onerror = failed;
4140
+ img.src = src;
4141
+ return;
4142
+ }
4143
+ const el = document.createElement(kind);
4144
+ el.preload = "metadata";
4145
+ el.onloadedmetadata = () => {
4146
+ const duration = Number.isFinite(el.duration) ? Math.round(el.duration * 1e3) : void 0;
4147
+ const v = el;
4148
+ done({ kind, ...duration ? { duration } : {}, ...kind === "video" && v.videoWidth ? { width: v.videoWidth, height: v.videoHeight } : {} });
4149
+ };
4150
+ el.onerror = failed;
4151
+ el.src = src;
4152
+ });
4153
+ }
4154
+ var KEY_LABELS = {
4155
+ opacity: { label: "Opacity", step: 0.05 },
4156
+ volume: { label: "Volume", step: 0.05 },
4157
+ x: { label: "X", step: 1 },
4158
+ y: { label: "Y", step: 1 },
4159
+ scale: { label: "Scale", step: 0.05 },
4160
+ rotation: { label: "Rotation", step: 1 }
4161
+ };
4162
+ var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _125 => _125.transform, 'optionalAccess', _126 => _126[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4163
+ var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
4164
+ function videoKeyframeRows(project, clipId, time) {
4165
+ const at = findVideoClip(project, clipId);
4166
+ if (!at) return [];
4167
+ const { clip } = at;
4168
+ const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
4169
+ const visual = !(_optionalChain([asset, 'optionalAccess', _127 => _127.kind]) === "audio" || at.track.kind === "audio");
4170
+ const audible = clip.type === "media" && videoAssetHasSound(asset);
4171
+ const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
4172
+ const clipTime = playheadInClip(clip, time);
4173
+ return properties.map((property) => {
4174
+ const prop = animatableOf(clip, property);
4175
+ const keys = isVideoKeyframes(prop) ? prop.keys : [];
4176
+ const limits = VIDEO_KEYFRAME_LIMITS[property];
4177
+ return {
4178
+ property,
4179
+ ...KEY_LABELS[property],
4180
+ value: Math.round(videoValueAt(prop, clipTime, limits.default) * 1e3) / 1e3,
4181
+ keyed: keys.length > 0,
4182
+ keys: keys.length,
4183
+ onKey: keys.some((k) => Math.abs(k.t - clipTime) < 0.5),
4184
+ clipTime,
4185
+ ...limits.min !== void 0 ? { min: limits.min } : {},
4186
+ ...limits.max !== void 0 ? { max: limits.max } : {}
4187
+ };
4188
+ });
4189
+ }
4190
+ function plainValueEdit(clip, property, value) {
4191
+ if (!VIDEO_TRANSFORM_PROPERTIES.includes(property))
4192
+ return value === void 0 ? { op: "update_clip", clip: clip.id, remove: [property] } : { op: "update_clip", clip: clip.id, set: { [property]: value } };
4193
+ const transform = { ...clip.transform, [property]: value };
4194
+ if (value === void 0) delete transform[property];
4195
+ return Object.keys(transform).length ? { op: "update_clip", clip: clip.id, set: { transform } } : { op: "update_clip", clip: clip.id, remove: ["transform"] };
4196
+ }
4197
+ function videoKeyframeValueEdit(project, clipId, property, time, value) {
4198
+ const at = findVideoClip(project, clipId);
4199
+ if (!at) return null;
4200
+ const prop = animatableOf(at.clip, property);
4201
+ if (!isVideoKeyframes(prop)) return plainValueEdit(at.clip, property, value);
4202
+ if (value === void 0) return null;
4203
+ return { op: "set_keyframes", clip: clipId, property, keys: setVideoKey(prop, { t: playheadInClip(at.clip, time), v: value }, VIDEO_KEYFRAME_LIMITS[property].default).keys };
4204
+ }
4205
+ function videoKeyframeToggleEdit(project, clipId, property, time) {
4206
+ const at = findVideoClip(project, clipId);
4207
+ if (!at) return null;
4208
+ const prop = animatableOf(at.clip, property);
4209
+ const t = playheadInClip(at.clip, time);
4210
+ const fallback = VIDEO_KEYFRAME_LIMITS[property].default;
4211
+ if (isVideoKeyframes(prop)) {
4212
+ const key = prop.keys.find((k) => Math.abs(k.t - t) < 0.5);
4213
+ if (key) {
4214
+ const rest = removeVideoKey(prop, key.t);
4215
+ return isVideoKeyframes(rest) ? { op: "set_keyframes", clip: clipId, property, keys: rest.keys } : plainValueEdit(at.clip, property, rest === fallback ? void 0 : rest);
4216
+ }
4217
+ }
4218
+ const v = videoValueAt(prop, t, fallback);
4219
+ return { op: "set_keyframes", clip: clipId, property, keys: isVideoKeyframes(prop) ? setVideoKey(prop, { t, v }, fallback).keys : [{ t, v }] };
4220
+ }
4221
+ function videoLutEdits(project, clips, lut) {
4222
+ const edits = [];
4223
+ for (const id of clips) {
4224
+ const at = findVideoClip(project, id);
4225
+ if (!at) continue;
4226
+ const { lut: current, ...rest } = _nullishCoalesce(at.clip.grade, () => ( {}));
4227
+ if (!lut && !current) continue;
4228
+ const grade = normalizeVideoGrade(lut ? { ...rest, lut } : rest);
4229
+ edits.push(grade ? { op: "update_clip", clip: id, set: { grade } } : { op: "update_clip", clip: id, remove: ["grade"] });
4230
+ }
4231
+ return edits;
4232
+ }
4233
+
4234
+ // src/video-editor/tools.ts
4235
+ var round = (ms) => Math.round(ms * 10) / 10;
4236
+ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
4237
+ var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
4238
+ var WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
4239
+ var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
4240
+ var object = (properties, required = [], description) => ({
4241
+ type: "object",
4242
+ ...description ? { description } : {},
4243
+ properties,
4244
+ ...required.length ? { required } : {},
4245
+ additionalProperties: false
4246
+ });
4247
+ var MS = (what) => ({ type: "number", minimum: 0, description: `${what}, in milliseconds.` });
4248
+ var CLIP_ID = { type: "string", minLength: 1, description: 'A clip id from get_video_project (e.g. "c1").' };
4249
+ var TRACK_ID = { type: "string", minLength: 1, description: 'A track id from get_video_project (e.g. "v1" or "a1").' };
4250
+ var ASSET_ID = { type: "string", minLength: 1, description: "An asset id from get_video_project." };
4251
+ var COLOR = (what) => ({ type: "string", minLength: 1, description: `${what}: a CSS colour such as "#ffcc00".` });
4252
+ var RIPPLE = {
4253
+ type: "boolean",
4254
+ description: "Push (or pull) later material along by the change, keeping the cuts after it in sync (default false: refuse if it would overlap). When the track is sync-locked (the default), every other unlocked sync-locked track moves too: clips across the edit point are cut when time opens, and closing time is refused if another sync-locked track has a clip in that stretch."
4255
+ };
4256
+ var MODE = {
4257
+ type: "string",
4258
+ enum: [...VIDEO_PLACEMENT_MODES],
4259
+ description: 'What to do with clips already where it lands: "refuse" (default) fails with the next free place; "overwrite" cuts, trims or removes what is underneath on that track; "insert" cuts everything across the start and pushes later material along by its length on this track and every sync-locked track.'
4260
+ };
4261
+ var DELTA = (what) => ({ type: "number", description: `${what}, in milliseconds of timeline time: positive is later, negative earlier. It is clamped to what the media and neighbours allow; the result says how far it went.` });
4262
+ var RANGE_TRACKS = { type: "array", items: TRACK_ID, minItems: 1, uniqueItems: true, description: "Tracks to work on (default: every unlocked track). Locked tracks are refused." };
4263
+ var TRANSFORM = object(
4264
+ {
4265
+ x: { type: "number", description: "Offset of the centre to the right of the frame's centre, in pixels (default 0)." },
4266
+ y: { type: "number", description: "Offset of the centre below the frame's centre, in pixels (default 0)." },
4267
+ scale: { type: "number", minimum: 0, description: "Size after fitting (1 = fitted size, default 1)." },
4268
+ rotation: { type: "number", description: "Degrees clockwise (default 0)." }
4269
+ },
4270
+ [],
4271
+ "Where the picture sits after it is fitted."
4272
+ );
4273
+ var TRANSITION = object(
4274
+ {
4275
+ type: { type: "string", enum: [...VIDEO_TRANSITION_TYPES], description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre)." },
4276
+ duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. Every type but dip needs half of it in media past the cut on both sides (handles); a dip needs none." },
4277
+ color: COLOR("For dip: the colour passed through (default black)"),
4278
+ direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
4279
+ audioCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fades through it: default equal-power for every type but dip (steady loudness while the two sounds overlap), linear for dip; or s-curve, exponential, logarithmic." }
4280
+ },
4281
+ ["type", "duration"],
4282
+ "A transition from the clip that ends where this one starts on the same track."
4283
+ );
4284
+ var LUT_SRC = { type: "string", minLength: 1, description: 'A .cube 3D LUT file the host can load and ffmpeg can read: a URL or path, often from the user (e.g. "luts/teal-orange.cube").' };
4285
+ var LUT_STRENGTH = { type: "number", minimum: 0, maximum: 1, description: "How much of the LUT's change to apply, 0 (none) to 1 (full, the default); 0.3-0.7 for a subtler look." };
4286
+ var GRADE = object(
4287
+ {
4288
+ ...Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, { type: "number", minimum: c.min, maximum: c.max, description: c.description }])),
4289
+ lut: object({ src: LUT_SRC, strength: LUT_STRENGTH, name: { type: "string", minLength: 1, description: "A label for the look." } }, ["src"], "A 3D LUT applied after the other settings (set_video_lut checks the file first).")
4290
+ },
4291
+ [],
4292
+ "Colour correction of the picture, applied in order: exposure, contrast, saturation, hue, white balance (temperature, tint), then a LUT. Leave a setting out for no change; the object replaces the clip's whole grade."
4293
+ );
4294
+ var ANIMATION = (when) => object(
4295
+ {
4296
+ effect: { type: "string", enum: [...VIDEO_CLIP_ANIMATION_EFFECTS], description: "fade; rise (up into place) or drop (down into place); slide-left / slide-right (moving that way); zoom (grows into place); pop (grows past its size and settles). Each also fades; an exit plays the movement backwards." },
4297
+ duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default 500)." }
4298
+ },
4299
+ ["effect"],
4300
+ when
4301
+ );
4302
+ var CLIP_COMMON = {
4303
+ name: { type: "string", description: "A label for the clip." },
4304
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1). A number replaces any keyframes; set_video_keyframes animates it." },
4305
+ transform: TRANSFORM,
4306
+ fit: { type: "string", enum: [...VIDEO_FITS], description: "How the picture fits the frame before the transform, as CSS object-fit (default contain for media, fill for colours)." },
4307
+ grade: GRADE,
4308
+ animateIn: ANIMATION("How the picture enters, from the clip's start (on top of any fade in). Titles usually rise, pop or fade in."),
4309
+ animateOut: ANIMATION("How the picture leaves, ending at the clip's end."),
4310
+ blend: { type: "string", enum: [...VIDEO_BLEND_MODES], description: "How the picture mixes with what is beneath it, as CSS mix-blend-mode (default normal): multiply darkens (shadows, textures), screen and add lighten (light leaks, flares, glows), overlay and soft-light add contrast, difference and exclusion invert." },
4311
+ fadeIn: MS("Fade up from nothing (picture and sound) over this long"),
4312
+ fadeOut: MS("Fade down to nothing over this long")
4313
+ };
4314
+ var MEDIA_FIELDS = {
4315
+ in: MS("Where in the source it starts (default 0)"),
4316
+ speed: { type: "number", minimum: 0.05, maximum: 20, description: "Playback speed (default 1). Changing it keeps the stretch of source, so the clip gets shorter or longer." },
4317
+ volume: { type: "number", minimum: 0, maximum: 4, description: "Gain (1 = unchanged, default 1). A number replaces any keyframes; set_video_keyframes animates it." },
4318
+ muted: { type: "boolean", description: "Silence this clip (default false)." },
4319
+ fadeInCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fade in (default linear; the picture always fades linearly): linear, equal-power (steady loudness, best for crossfades), s-curve (gentle at both ends), exponential (slow start, natural from silence) or logarithmic (fast start)." },
4320
+ fadeOutCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fade out (default linear), the same choices as fadeInCurve; exponential sounds natural fading to silence." }
4321
+ };
4322
+ var TEXT_FIELDS = {
4323
+ text: { type: "string", minLength: 1, description: `The title's text ("\\n" breaks lines).` },
4324
+ color: COLOR("Text colour (default white)"),
4325
+ fontSize: { type: "number", exclusiveMinimum: 0, description: "Pixels (default 64)." },
4326
+ fontFamily: { type: "string", description: "CSS font family (default sans-serif)." },
4327
+ fontWeight: { type: "number", minimum: 100, maximum: 1e3, description: "CSS weight (default 700)." },
4328
+ background: COLOR("A box behind the text (default none)")
4329
+ };
4330
+ var PLACEMENT = {
4331
+ id: { type: "string", minLength: 1, description: 'Id for the clip (default: a fresh one such as "c3").' },
4332
+ start: MS("Where it starts on the timeline (default: the end of the track)"),
4333
+ duration: { type: "number", exclusiveMinimum: 0, description: "Length on the timeline, in ms (default: the rest of the source for media, 5000 for stills, 3000 for titles and colours)." }
4334
+ };
4335
+ var NEW_CLIP = {
4336
+ description: "The clip: media (a stretch of an asset), text (a title) or color (a solid fill).",
4337
+ oneOf: [
4338
+ object({ type: { const: "media", description: "A stretch of an asset." }, asset: ASSET_ID, ...PLACEMENT, ...CLIP_COMMON, ...MEDIA_FIELDS }, ["type", "asset"], "A media clip."),
4339
+ object({ type: { const: "text", description: "A title over the picture." }, ...PLACEMENT, ...CLIP_COMMON, ...TEXT_FIELDS }, ["type", "text"], "A title."),
4340
+ object({ type: { const: "color", description: "A solid colour filling the frame." }, color: COLOR("The fill"), ...PLACEMENT, ...CLIP_COMMON }, ["type", "color"], "A colour.")
4341
+ ]
4342
+ };
4343
+ var SETTINGS = {
4344
+ name: { type: "string", description: "Project name." },
4345
+ width: { type: "integer", minimum: 16, maximum: 8192, description: "Frame width in pixels." },
4346
+ height: { type: "integer", minimum: 16, maximum: 8192, description: "Frame height in pixels." },
4347
+ fps: { type: "number", exclusiveMinimum: 0, maximum: 240, description: "Frames per second (e.g. 24, 25, 30, 60)." },
4348
+ background: COLOR("What shows where no clip covers the frame (default black)")
3757
4349
  };
3758
4350
  var TRACK_FIELDS = {
3759
4351
  name: { type: "string", description: "Track label." },
@@ -3855,14 +4447,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
3855
4447
  return null;
3856
4448
  };
3857
4449
  const placed = (o) => ({
3858
- ..._optionalChain([o, 'access', _96 => _96.created, 'optionalAccess', _97 => _97.length]) ? { created: o.created } : {},
3859
- ..._optionalChain([o, 'access', _98 => _98.trimmed, 'optionalAccess', _99 => _99.length]) ? { trimmed: o.trimmed } : {},
3860
- ..._optionalChain([o, 'access', _100 => _100.removed, 'optionalAccess', _101 => _101.length]) ? { removed: o.removed } : {},
4450
+ ..._optionalChain([o, 'access', _128 => _128.created, 'optionalAccess', _129 => _129.length]) ? { created: o.created } : {},
4451
+ ..._optionalChain([o, 'access', _130 => _130.trimmed, 'optionalAccess', _131 => _131.length]) ? { trimmed: o.trimmed } : {},
4452
+ ..._optionalChain([o, 'access', _132 => _132.removed, 'optionalAccess', _133 => _133.length]) ? { removed: o.removed } : {},
3861
4453
  ...o.rippled ? { rippled: o.rippled } : {}
3862
4454
  });
3863
4455
  const keysOut = (p, clip, property) => {
3864
4456
  const c = clipOut(p, clip);
3865
- const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _102 => _102.transform, 'optionalAccess', _103 => _103[property]]) : _optionalChain([c, 'optionalAccess', _104 => _104[property]]);
4457
+ const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _134 => _134.transform, 'optionalAccess', _135 => _135[property]]) : _optionalChain([c, 'optionalAccess', _136 => _136[property]]);
3866
4458
  return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
3867
4459
  };
3868
4460
  const getProject = {
@@ -3923,14 +4515,18 @@ function createVideoEditorToolProvider(controller, options = {}) {
3923
4515
  annotations: { title: "Render a frame", ...READ },
3924
4516
  handler: async ({ time, times, columns, format = "svg" }) => {
3925
4517
  const p = project();
3926
- const svg = times ? videoContactSheetSvg(p, times, columns ? { columns } : {}) : videoProjectSvg(p, _nullishCoalesce(time, () => ( controller.getState().time)));
4518
+ const lutSrcs = [...new Set(p.tracks.flatMap((t) => t.clips.map((c) => _optionalChain([c, 'access', _137 => _137.grade, 'optionalAccess', _138 => _138.lut, 'optionalAccess', _139 => _139.src])).filter((x) => !!x)))];
4519
+ const missing = (await Promise.all(lutSrcs.map((src) => controller.luts.load(src).then(() => null, (e) => e.message)))).filter((m) => !!m);
4520
+ const svgOptions = { lutFor: (src) => controller.luts.peek(src), ...columns ? { columns } : {} };
4521
+ const svg = times ? videoContactSheetSvg(p, times, svgOptions) : videoProjectSvg(p, _nullishCoalesce(time, () => ( controller.getState().time)), svgOptions);
4522
+ const notes = [...missing.map((m) => `${m}; that LUT is left out of the picture.`), ...lutSrcs.length > missing.length ? ["LUTs are drawn approximately in SVG (curves and a colour matrix); the preview and export apply them exactly."] : []];
3927
4523
  if (format === "png") {
3928
4524
  if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_video_frame with format "svg".');
3929
4525
  const size = /viewBox="0 0 ([\d.]+) ([\d.]+)"/.exec(svg);
3930
4526
  const image = await options.rasterize(svg, { width: Number(size[1]), height: Number(size[2]) });
3931
- return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }] };
4527
+ return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }, ...notes.length ? [{ type: "text", text: notes.join(" ") }] : []] };
3932
4528
  }
3933
- return { svg };
4529
+ return { svg, ...notes.length ? { notes } : {} };
3934
4530
  }
3935
4531
  };
3936
4532
  const LOUDNESS_TARGET = {
@@ -4160,14 +4756,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
4160
4756
  const at = findVideoClip(p, id);
4161
4757
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4162
4758
  const { clip } = at;
4163
- if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _105 => _105.assets, 'access', _106 => _106.find, 'call', _107 => _107((a) => a.id === clip.asset), 'optionalAccess', _108 => _108.kind]) === "audio")
4759
+ if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _140 => _140.assets, 'access', _141 => _141.find, 'call', _142 => _142((a) => a.id === clip.asset), 'optionalAccess', _143 => _143.kind]) === "audio")
4164
4760
  throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
4165
4761
  }
4166
4762
  };
4167
4763
  const applyGradePreset = {
4168
4764
  name: "apply_video_grade_preset",
4169
4765
  title: "Apply a colour look",
4170
- description: "Gives clips a named colour look at a strength (one undo step), replacing their grades: warm, cool, punchy (more contrast and colour), faded (lifted and muted), black-and-white, bleach-bypass (desaturated, contrasty), day-for-night (dark and blue). Returns each clip's resulting grade; tweak it with update_video_clip set.grade.",
4766
+ description: "Gives clips a named colour look at a strength (one undo step), replacing their grade settings (a LUT stays): warm, cool, punchy (more contrast and colour), faded (lifted and muted), black-and-white, bleach-bypass (desaturated, contrasty), day-for-night (dark and blue). Returns each clip's resulting grade; tweak it with update_video_clip set.grade.",
4171
4767
  inputSchema: object(
4172
4768
  {
4173
4769
  clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to grade." },
@@ -4179,9 +4775,11 @@ function createVideoEditorToolProvider(controller, options = {}) {
4179
4775
  annotations: { title: "Apply a colour look", ...WRITE_IDEMPOTENT },
4180
4776
  handler: ({ clips, preset, amount }) => {
4181
4777
  gradeTargets(clips);
4182
- const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
4183
- const edits = clips.map((clip) => grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] });
4184
- return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _109 => _109(r.project, id), 'optionalAccess', _110 => _110.clip, 'access', _111 => _111.grade]), () => ( null)) })) }));
4778
+ const edits = clips.map((clip) => {
4779
+ const grade = videoGradeLook(findVideoClip(project(), clip).clip.grade, preset, _nullishCoalesce(amount, () => ( 1)));
4780
+ return grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] };
4781
+ });
4782
+ return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _144 => _144(r.project, id), 'optionalAccess', _145 => _145.clip, 'access', _146 => _146.grade]), () => ( null)) })) }));
4185
4783
  }
4186
4784
  };
4187
4785
  const copyGrade = {
@@ -4197,6 +4795,43 @@ function createVideoEditorToolProvider(controller, options = {}) {
4197
4795
  return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
4198
4796
  }
4199
4797
  };
4798
+ const setLut = {
4799
+ name: "set_video_lut",
4800
+ title: "Apply a LUT",
4801
+ description: "Gives clips a 3D LUT (a .cube file) at a strength, applied after the rest of their grade (one undo step); their other grade settings stay. Reads the file first and fails, changing nothing, if it cannot be loaded or is not a 3D .cube. Returns the LUT's title and size and each clip's resulting grade. Remove it with remove_video_lut.",
4802
+ inputSchema: object(
4803
+ {
4804
+ clips: { type: "array", items: CLIP_ID, minItems: 1, uniqueItems: true, description: "Clips on video tracks to grade." },
4805
+ src: LUT_SRC,
4806
+ strength: LUT_STRENGTH,
4807
+ name: { type: "string", minLength: 1, description: "A label for the look (default the file's TITLE)." }
4808
+ },
4809
+ ["clips", "src"]
4810
+ ),
4811
+ annotations: { title: "Apply a LUT", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
4812
+ handler: async ({ clips, src, strength, name }) => {
4813
+ if (strength === 0) throw new Error("A strength of 0 changes nothing; pass a strength above 0, or remove the LUT with remove_video_lut.");
4814
+ gradeTargets(clips);
4815
+ const lut = await controller.luts.load(src);
4816
+ const label = _nullishCoalesce(name, () => ( lut.title));
4817
+ const setting = { src, ...strength !== void 0 && strength !== 1 ? { strength } : {}, ...label ? { name: label } : {} };
4818
+ return apply(videoLutEdits(project(), clips, setting), "Apply LUT", (r) => ({ lut: { src, ...lut.title ? { title: lut.title } : {}, size: lut.size }, clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _147 => _147(r.project, id), 'optionalAccess', _148 => _148.clip, 'access', _149 => _149.grade]), () => ( null)) })) }));
4819
+ }
4820
+ };
4821
+ const removeLut = {
4822
+ name: "remove_video_lut",
4823
+ title: "Remove a LUT",
4824
+ description: "Takes the LUT off clips' grades (one undo step), keeping their other grade settings. Fails if none of them has one. Returns each clip's resulting grade.",
4825
+ inputSchema: object({ clips: { type: "array", items: CLIP_ID, minItems: 1, uniqueItems: true, description: "Clips whose LUT to remove." } }, ["clips"]),
4826
+ annotations: { title: "Remove a LUT", ...WRITE_IDEMPOTENT },
4827
+ handler: ({ clips }) => {
4828
+ gradeTargets(clips);
4829
+ const p = project();
4830
+ const withLut = clips.filter((id) => _optionalChain([findVideoClip, 'call', _150 => _150(p, id), 'access', _151 => _151.clip, 'access', _152 => _152.grade, 'optionalAccess', _153 => _153.lut]));
4831
+ if (!withLut.length) throw new Error(`None of ${clips.map((c) => `"${c}"`).join(", ")} has a LUT; get_video_project shows each clip's grade.`);
4832
+ return apply(videoLutEdits(p, withLut, null), "Remove LUT", (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _154 => _154(r.project, id), 'optionalAccess', _155 => _155.clip, 'access', _156 => _156.grade]), () => ( null)) })) }));
4833
+ }
4834
+ };
4200
4835
  const TRANSCRIPT_TRACK = { ...TRACK_ID, description: "Track whose words to read (default: the first audio track with a transcribed clip, else the first video track with one)." };
4201
4836
  const RANGE_OUT = (r) => ({ from: round(r.from), to: round(r.to), text: r.text });
4202
4837
  const timelineWords = (track) => {
@@ -4248,7 +4883,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4248
4883
  handler: ({ track, from, to, limit }) => {
4249
4884
  const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
4250
4885
  const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
4251
- return { track: _nullishCoalesce(_optionalChain([words, 'access', _112 => _112[0], 'optionalAccess', _113 => _113.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
4886
+ return { track: _nullishCoalesce(_optionalChain([words, 'access', _157 => _157[0], 'optionalAccess', _158 => _158.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
4252
4887
  }
4253
4888
  };
4254
4889
  const findText = {
@@ -4310,7 +4945,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4310
4945
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4311
4946
  const { clip } = at;
4312
4947
  if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
4313
- if (_optionalChain([project, 'call', _114 => _114(), 'access', _115 => _115.assets, 'access', _116 => _116.find, 'call', _117 => _117((a) => a.id === clip.asset), 'optionalAccess', _118 => _118.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
4948
+ if (_optionalChain([project, 'call', _159 => _159(), 'access', _160 => _160.assets, 'access', _161 => _161.find, 'call', _162 => _162((a) => a.id === clip.asset), 'optionalAccess', _163 => _163.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
4314
4949
  return clip;
4315
4950
  };
4316
4951
  const rampOut = (r, id) => {
@@ -4639,7 +5274,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4639
5274
  }
4640
5275
  };
4641
5276
  const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
4642
- const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
5277
+ const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
4643
5278
  return {
4644
5279
  id: _nullishCoalesce(options.id, () => ( "video")),
4645
5280
  namespace: options.namespace,
@@ -4657,226 +5292,6 @@ function createVideoEditorToolProvider(controller, options = {}) {
4657
5292
  };
4658
5293
  }
4659
5294
 
4660
- // src/video-editor/ui.ts
4661
- var STEPS = [40, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 15e3, 3e4, 6e4, 12e4, 3e5, 6e5];
4662
- function videoRulerTicks(duration, pxPerSecond, fps) {
4663
- const pxPerMs = pxPerSecond / 1e3;
4664
- const major = _nullishCoalesce(STEPS.find((s) => s * pxPerMs >= 80), () => ( STEPS[STEPS.length - 1]));
4665
- const minor = STEPS.slice().reverse().find((s) => s < major && major % s === 0 && s * pxPerMs >= 10);
4666
- const step = _nullishCoalesce(minor, () => ( major));
4667
- const ticks = [];
4668
- for (let t = 0; t <= duration + 1e-6; t += step) {
4669
- const isMajor = Math.abs(t / major - Math.round(t / major)) < 1e-9;
4670
- ticks.push({ time: t, x: t * pxPerMs, major: isMajor, ...isMajor ? { label: formatVideoTimecode(t, fps).replace(/^00:/, "") } : {} });
4671
- }
4672
- return ticks;
4673
- }
4674
- function videoClipShowsWaveform(clip, asset, mode = "audio") {
4675
- if (clip.type !== "media" || mode === "none" || !videoAssetHasSound(asset)) return false;
4676
- return mode === "all" || asset.kind === "audio";
4677
- }
4678
- var VIDEO_CLIP_ANIMATION_LABELS = {
4679
- fade: "Fade",
4680
- rise: "Rise",
4681
- drop: "Drop",
4682
- "slide-left": "Slide left",
4683
- "slide-right": "Slide right",
4684
- zoom: "Zoom",
4685
- pop: "Pop"
4686
- };
4687
- var VIDEO_TRANSITION_LABELS = {
4688
- crossfade: "Crossfade",
4689
- dip: "Dip to colour",
4690
- wipe: "Wipe",
4691
- push: "Push",
4692
- slide: "Slide",
4693
- iris: "Iris"
4694
- };
4695
- var videoTransitionHasDirection = (type) => type === "wipe" || type === "push" || type === "slide";
4696
- var VIDEO_BLEND_MODE_LABELS = {
4697
- normal: "Normal",
4698
- multiply: "Multiply",
4699
- screen: "Screen",
4700
- overlay: "Overlay",
4701
- darken: "Darken",
4702
- lighten: "Lighten",
4703
- "color-dodge": "Colour dodge",
4704
- "color-burn": "Colour burn",
4705
- "hard-light": "Hard light",
4706
- "soft-light": "Soft light",
4707
- difference: "Difference",
4708
- exclusion: "Exclusion",
4709
- add: "Add"
4710
- };
4711
- var VIDEO_FADE_CURVE_LABELS = {
4712
- linear: "Linear",
4713
- "equal-power": "Equal power",
4714
- "s-curve": "S-curve",
4715
- exponential: "Exponential",
4716
- logarithmic: "Logarithmic"
4717
- };
4718
- function videoClipTint(clip, asset) {
4719
- if (clip.type === "text") return "#a855f7";
4720
- if (clip.type === "color") return clip.color;
4721
- if (!asset) return "#ef4444";
4722
- return asset.kind === "audio" ? "#10b981" : asset.kind === "image" ? "#f59e0b" : "#3b82f6";
4723
- }
4724
- function videoClipLabel(clip, asset) {
4725
- if (clip.name) return clip.name;
4726
- if (clip.type === "text") return clip.text.split("\n")[0];
4727
- if (clip.type === "color") return clip.color;
4728
- const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _119 => _119.name]), () => ( _optionalChain([asset, 'optionalAccess', _120 => _120.src, 'access', _121 => _121.replace, 'call', _122 => _122(/^.*[\\/]/, "")]))), () => ( clip.asset));
4729
- const speed = videoClipSpeed(clip);
4730
- return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
4731
- }
4732
- function planVideoClipDrag(project, input) {
4733
- const at = findVideoClip(project, input.clip);
4734
- if (!at) return null;
4735
- const { clip, track } = at;
4736
- const snap = _nullishCoalesce(input.snap, () => ( ((t) => t)));
4737
- if (input.mode === "move") {
4738
- const raw = Math.max(0, clip.start + input.delta);
4739
- const head = snap(raw) - raw;
4740
- const tail = snap(raw + clip.duration) - (raw + clip.duration);
4741
- const shifts = [head, tail].filter((d) => Math.abs(d) > 1e-9);
4742
- const start = Math.max(0, raw + (shifts.length ? shifts.reduce((a, b) => Math.abs(b) < Math.abs(a) ? b : a) : 0));
4743
- const target = _nullishCoalesce(input.track, () => ( track.id));
4744
- const moved = Math.abs(start - clip.start) > 1e-6 || target !== track.id;
4745
- const mode = input.placement && input.placement !== "refuse" ? { mode: input.placement } : {};
4746
- return { start, duration: clip.duration, track: target, edit: moved ? { op: "move_clip", clip: clip.id, start, ...target !== track.id ? { track: target } : {}, ...mode } : null };
4747
- }
4748
- const minimum = 1e3 / project.fps;
4749
- if (input.mode === "trim-start") {
4750
- const to2 = Math.min(snap(Math.max(0, clip.start + input.delta)), videoClipEnd(clip) - minimum);
4751
- const changed2 = Math.abs(to2 - clip.start) > 1e-6;
4752
- const plan = input.ripple ? { start: clip.start, duration: clip.duration - (to2 - clip.start) } : { start: to2, duration: videoClipEnd(clip) - to2 };
4753
- return { ...plan, track: track.id, edit: changed2 ? { op: "trim_clip", clip: clip.id, edge: "start", to: to2, ...input.ripple ? { ripple: true } : {} } : null };
4754
- }
4755
- const to = Math.max(snap(videoClipEnd(clip) + input.delta), clip.start + minimum);
4756
- const changed = Math.abs(to - videoClipEnd(clip)) > 1e-6;
4757
- return { start: clip.start, duration: to - clip.start, track: track.id, edit: changed ? { op: "trim_clip", clip: clip.id, edge: "end", to, ...input.ripple ? { ripple: true } : {} } : null };
4758
- }
4759
- function videoDropTracks(project, clipId) {
4760
- const at = findVideoClip(project, clipId);
4761
- if (!at) return [];
4762
- return project.tracks.filter((t) => t.kind === at.track.kind && !t.locked).map((t) => t.id);
4763
- }
4764
- function videoDragLabel(edit) {
4765
- if (edit.op === "move_clip") return edit.mode === "insert" ? "Insert clip" : edit.mode === "overwrite" ? "Overwrite clip" : "Move clip";
4766
- return edit.op === "trim_clip" && edit.ripple ? "Ripple trim" : "Trim clip";
4767
- }
4768
- function videoAssetDropEdit(project, assetId, options = {}) {
4769
- const asset = videoAssetMap(project).get(assetId);
4770
- if (!asset) return null;
4771
- const kind = asset.kind === "audio" ? "audio" : "video";
4772
- const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _123 => _123.tracks, 'access', _124 => _124.find, 'call', _125 => _125((t) => t.kind === kind && !t.locked), 'optionalAccess', _126 => _126.id])));
4773
- if (!track) return null;
4774
- return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
4775
- }
4776
- function videoAssetKindOf(typeOrName) {
4777
- const s = typeOrName.toLowerCase().replace(/[?#].*$/, "");
4778
- if (s.startsWith("video/") || /\.(mp4|m4v|mov|webm|ogv|mkv)$/.test(s)) return "video";
4779
- if (s.startsWith("audio/") || /\.(mp3|m4a|aac|wav|ogg|oga|flac|opus)$/.test(s)) return "audio";
4780
- if (s.startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg|bmp)$/.test(s)) return "image";
4781
- return null;
4782
- }
4783
- function probeVideoMedia(src, kind, timeout = 15e3) {
4784
- return new Promise((resolve, reject) => {
4785
- if (typeof document === "undefined") return reject(new Error("Media can only be probed in a browser."));
4786
- const timer = setTimeout(() => reject(new Error(`"${src}" took too long to load.`)), timeout);
4787
- const done = (probe) => {
4788
- clearTimeout(timer);
4789
- resolve(probe);
4790
- };
4791
- const failed = () => {
4792
- clearTimeout(timer);
4793
- reject(new Error(`"${src}" could not be loaded as ${kind}.`));
4794
- };
4795
- if (kind === "image") {
4796
- const img = new Image();
4797
- img.onload = () => done({ kind, width: img.naturalWidth, height: img.naturalHeight });
4798
- img.onerror = failed;
4799
- img.src = src;
4800
- return;
4801
- }
4802
- const el = document.createElement(kind);
4803
- el.preload = "metadata";
4804
- el.onloadedmetadata = () => {
4805
- const duration = Number.isFinite(el.duration) ? Math.round(el.duration * 1e3) : void 0;
4806
- const v = el;
4807
- done({ kind, ...duration ? { duration } : {}, ...kind === "video" && v.videoWidth ? { width: v.videoWidth, height: v.videoHeight } : {} });
4808
- };
4809
- el.onerror = failed;
4810
- el.src = src;
4811
- });
4812
- }
4813
- var KEY_LABELS = {
4814
- opacity: { label: "Opacity", step: 0.05 },
4815
- volume: { label: "Volume", step: 0.05 },
4816
- x: { label: "X", step: 1 },
4817
- y: { label: "Y", step: 1 },
4818
- scale: { label: "Scale", step: 0.05 },
4819
- rotation: { label: "Rotation", step: 1 }
4820
- };
4821
- var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _127 => _127.transform, 'optionalAccess', _128 => _128[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4822
- var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
4823
- function videoKeyframeRows(project, clipId, time) {
4824
- const at = findVideoClip(project, clipId);
4825
- if (!at) return [];
4826
- const { clip } = at;
4827
- const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
4828
- const visual = !(_optionalChain([asset, 'optionalAccess', _129 => _129.kind]) === "audio" || at.track.kind === "audio");
4829
- const audible = clip.type === "media" && videoAssetHasSound(asset);
4830
- const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
4831
- const clipTime = playheadInClip(clip, time);
4832
- return properties.map((property) => {
4833
- const prop = animatableOf(clip, property);
4834
- const keys = isVideoKeyframes(prop) ? prop.keys : [];
4835
- const limits = VIDEO_KEYFRAME_LIMITS[property];
4836
- return {
4837
- property,
4838
- ...KEY_LABELS[property],
4839
- value: Math.round(videoValueAt(prop, clipTime, limits.default) * 1e3) / 1e3,
4840
- keyed: keys.length > 0,
4841
- keys: keys.length,
4842
- onKey: keys.some((k) => Math.abs(k.t - clipTime) < 0.5),
4843
- clipTime,
4844
- ...limits.min !== void 0 ? { min: limits.min } : {},
4845
- ...limits.max !== void 0 ? { max: limits.max } : {}
4846
- };
4847
- });
4848
- }
4849
- function plainValueEdit(clip, property, value) {
4850
- if (!VIDEO_TRANSFORM_PROPERTIES.includes(property))
4851
- return value === void 0 ? { op: "update_clip", clip: clip.id, remove: [property] } : { op: "update_clip", clip: clip.id, set: { [property]: value } };
4852
- const transform = { ...clip.transform, [property]: value };
4853
- if (value === void 0) delete transform[property];
4854
- return Object.keys(transform).length ? { op: "update_clip", clip: clip.id, set: { transform } } : { op: "update_clip", clip: clip.id, remove: ["transform"] };
4855
- }
4856
- function videoKeyframeValueEdit(project, clipId, property, time, value) {
4857
- const at = findVideoClip(project, clipId);
4858
- if (!at) return null;
4859
- const prop = animatableOf(at.clip, property);
4860
- if (!isVideoKeyframes(prop)) return plainValueEdit(at.clip, property, value);
4861
- if (value === void 0) return null;
4862
- return { op: "set_keyframes", clip: clipId, property, keys: setVideoKey(prop, { t: playheadInClip(at.clip, time), v: value }, VIDEO_KEYFRAME_LIMITS[property].default).keys };
4863
- }
4864
- function videoKeyframeToggleEdit(project, clipId, property, time) {
4865
- const at = findVideoClip(project, clipId);
4866
- if (!at) return null;
4867
- const prop = animatableOf(at.clip, property);
4868
- const t = playheadInClip(at.clip, time);
4869
- const fallback = VIDEO_KEYFRAME_LIMITS[property].default;
4870
- if (isVideoKeyframes(prop)) {
4871
- const key = prop.keys.find((k) => Math.abs(k.t - t) < 0.5);
4872
- if (key) {
4873
- const rest = removeVideoKey(prop, key.t);
4874
- return isVideoKeyframes(rest) ? { op: "set_keyframes", clip: clipId, property, keys: rest.keys } : plainValueEdit(at.clip, property, rest === fallback ? void 0 : rest);
4875
- }
4876
- }
4877
- const v = videoValueAt(prop, t, fallback);
4878
- return { op: "set_keyframes", clip: clipId, property, keys: isVideoKeyframes(prop) ? setVideoKey(prop, { t, v }, fallback).keys : [{ t, v }] };
4879
- }
4880
5295
 
4881
5296
 
4882
5297
 
@@ -5067,5 +5482,17 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
5067
5482
 
5068
5483
 
5069
5484
 
5070
- exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoChroma = videoChroma; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
5485
+
5486
+
5487
+
5488
+
5489
+
5490
+
5491
+
5492
+
5493
+
5494
+
5495
+
5496
+
5497
+ exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoLutCache = VideoLutCache; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.applyVideoLut = applyVideoLut; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoCubeLut = parseVideoCubeLut; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoGradeLut = parseVideoGradeLut; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoChroma = videoChroma; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeLook = videoGradeLook; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoLutEdits = videoLutEdits; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoLutToCube = videoLutToCube; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
5071
5498
  //# sourceMappingURL=index.cjs.map