@pixerate/editor 0.86.0 → 0.88.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.
- package/dist/video-editor/index.cjs +1342 -458
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +400 -11
- package/dist/video-editor/index.d.ts +400 -11
- package/dist/video-editor/index.js +1251 -367
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +23 -2
|
@@ -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;
|
|
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
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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 = (
|
|
1093
|
-
constructor(p) {;
|
|
1373
|
+
var Editor = (_class2 = class {
|
|
1374
|
+
constructor(p) {;_class2.prototype.__init5.call(this);
|
|
1094
1375
|
this.p = p;
|
|
1095
1376
|
}
|
|
1096
1377
|
|
|
1097
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
},
|
|
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',
|
|
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',
|
|
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
|
|
1801
|
-
var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI *
|
|
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 =
|
|
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',
|
|
1821
|
-
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
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',
|
|
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:
|
|
1838
|
-
if (tail && time >= videoClipEnd(clip) - tail) a.outgoing = { progress:
|
|
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',
|
|
1852
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
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',
|
|
1884
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
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:
|
|
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:
|
|
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 = (
|
|
2062
|
-
constructor(loader = (asset) => decodeVideoAudioPeaks(asset.src)) {;
|
|
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
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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,380 @@ var VideoAudioPeaksCache = (_class2 = class {
|
|
|
2122
2403
|
emit() {
|
|
2123
2404
|
for (const fn of this.listeners) fn();
|
|
2124
2405
|
}
|
|
2125
|
-
},
|
|
2406
|
+
}, _class3);
|
|
2407
|
+
|
|
2408
|
+
// src/video-editor/scopes.ts
|
|
2409
|
+
var VIDEO_SCOPE_KINDS = ["histogram", "waveform", "vectorscope"];
|
|
2410
|
+
var videoLuma = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2411
|
+
function videoChroma(r, g, b) {
|
|
2412
|
+
const y = videoLuma(r, g, b);
|
|
2413
|
+
return [(b - y) / 1.8556, (r - y) / 1.5748];
|
|
2414
|
+
}
|
|
2415
|
+
function eachPixel(pixels, maxPixels, visit) {
|
|
2416
|
+
const { width, height, data } = pixels;
|
|
2417
|
+
if (!(width > 0 && height > 0) || data.length < width * height * 4) throw new Error(`The pixels are not ${width} \xD7 ${height} RGBA: there are ${data.length} values, and ${width * height * 4} are needed.`);
|
|
2418
|
+
const step = Math.max(1, Math.sqrt(width * height / Math.max(1, _nullishCoalesce(maxPixels, () => ( 262144)))));
|
|
2419
|
+
let count = 0;
|
|
2420
|
+
for (let fy = step / 2; fy < height; fy += step) {
|
|
2421
|
+
const y = Math.floor(fy);
|
|
2422
|
+
for (let fx = step / 2; fx < width; fx += step) {
|
|
2423
|
+
const x = Math.floor(fx);
|
|
2424
|
+
const i = (y * width + x) * 4;
|
|
2425
|
+
visit(data[i], data[i + 1], data[i + 2], x, y);
|
|
2426
|
+
count++;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return count;
|
|
2430
|
+
}
|
|
2431
|
+
var binOf = (v, bins) => Math.min(bins - 1, Math.floor((v + 1e-6) / 256 * bins));
|
|
2432
|
+
function videoHistogram(pixels, options = {}) {
|
|
2433
|
+
const bins = Math.max(1, Math.min(256, Math.round(_nullishCoalesce(options.bins, () => ( 256)))));
|
|
2434
|
+
const out = { bins, r: Array(bins).fill(0), g: Array(bins).fill(0), b: Array(bins).fill(0), luma: Array(bins).fill(0), total: 0 };
|
|
2435
|
+
out.total = eachPixel(pixels, options.maxPixels, (r, g, b) => {
|
|
2436
|
+
out.r[binOf(r, bins)]++;
|
|
2437
|
+
out.g[binOf(g, bins)]++;
|
|
2438
|
+
out.b[binOf(b, bins)]++;
|
|
2439
|
+
out.luma[binOf(videoLuma(r, g, b), bins)]++;
|
|
2440
|
+
});
|
|
2441
|
+
return out;
|
|
2442
|
+
}
|
|
2443
|
+
function videoLumaWaveform(pixels, options = {}) {
|
|
2444
|
+
const columns = Math.max(1, Math.min(pixels.width, Math.round(_nullishCoalesce(options.columns, () => ( Math.min(pixels.width, 256))))));
|
|
2445
|
+
const levels = Math.max(2, Math.min(256, Math.round(_nullishCoalesce(options.levels, () => ( 128)))));
|
|
2446
|
+
const data = new Uint32Array(columns * levels);
|
|
2447
|
+
let peak = 0;
|
|
2448
|
+
eachPixel(pixels, options.maxPixels, (r, g, b, x) => {
|
|
2449
|
+
const c = Math.min(columns - 1, Math.floor(x / pixels.width * columns));
|
|
2450
|
+
const i = c * levels + binOf(videoLuma(r, g, b), levels);
|
|
2451
|
+
if (++data[i] > peak) peak = data[i];
|
|
2452
|
+
});
|
|
2453
|
+
return { columns, levels, data, peak };
|
|
2454
|
+
}
|
|
2455
|
+
var videoVectorscopePoint = (cb, cr, size) => [(cb + 0.5) * (size - 1), (0.5 - cr) * (size - 1)];
|
|
2456
|
+
function videoVectorscope(pixels, options = {}) {
|
|
2457
|
+
const size = Math.max(16, Math.min(512, Math.round(_nullishCoalesce(options.size, () => ( 128)))));
|
|
2458
|
+
const data = new Uint32Array(size * size);
|
|
2459
|
+
let peak = 0;
|
|
2460
|
+
eachPixel(pixels, options.maxPixels, (r, g, b) => {
|
|
2461
|
+
const [cb, cr] = videoChroma(r / 255, g / 255, b / 255);
|
|
2462
|
+
const [x, y] = videoVectorscopePoint(cb, cr, size);
|
|
2463
|
+
const i = Math.round(y) * size + Math.round(x);
|
|
2464
|
+
if (++data[i] > peak) peak = data[i];
|
|
2465
|
+
});
|
|
2466
|
+
return { size, data, peak };
|
|
2467
|
+
}
|
|
2468
|
+
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
2469
|
+
var hex2 = (v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0");
|
|
2470
|
+
function videoExposureStats(pixels, options = {}) {
|
|
2471
|
+
const low = (_nullishCoalesce(options.shadowLevel, () => ( 0.02))) * 255;
|
|
2472
|
+
const high = (_nullishCoalesce(options.highlightLevel, () => ( 0.98))) * 255;
|
|
2473
|
+
const lumaBins = new Uint32Array(256);
|
|
2474
|
+
let sumY = 0;
|
|
2475
|
+
let sumR = 0;
|
|
2476
|
+
let sumG = 0;
|
|
2477
|
+
let sumB = 0;
|
|
2478
|
+
let sumSat = 0;
|
|
2479
|
+
let maxSat = 0;
|
|
2480
|
+
let shadows = 0;
|
|
2481
|
+
let highlights = 0;
|
|
2482
|
+
let cr = 0;
|
|
2483
|
+
let cg = 0;
|
|
2484
|
+
let cbl = 0;
|
|
2485
|
+
const total = eachPixel(pixels, options.maxPixels, (r, g, b) => {
|
|
2486
|
+
const y = videoLuma(r, g, b);
|
|
2487
|
+
lumaBins[Math.min(255, Math.round(y))]++;
|
|
2488
|
+
sumY += y;
|
|
2489
|
+
sumR += r;
|
|
2490
|
+
sumG += g;
|
|
2491
|
+
sumB += b;
|
|
2492
|
+
const [u, v] = videoChroma(r / 255, g / 255, b / 255);
|
|
2493
|
+
const sat = Math.min(1, Math.hypot(u, v) / 0.5);
|
|
2494
|
+
sumSat += sat;
|
|
2495
|
+
if (sat > maxSat) maxSat = sat;
|
|
2496
|
+
if (y <= low) shadows++;
|
|
2497
|
+
if (y >= high) highlights++;
|
|
2498
|
+
if (r >= high) cr++;
|
|
2499
|
+
if (g >= high) cg++;
|
|
2500
|
+
if (b >= high) cbl++;
|
|
2501
|
+
});
|
|
2502
|
+
const percentile = (p) => {
|
|
2503
|
+
const target = p * total;
|
|
2504
|
+
let seen = 0;
|
|
2505
|
+
for (let i = 0; i < 256; i++) {
|
|
2506
|
+
seen += lumaBins[i];
|
|
2507
|
+
if (seen >= target && seen > 0) return i / 255;
|
|
2508
|
+
}
|
|
2509
|
+
return 1;
|
|
2510
|
+
};
|
|
2511
|
+
let min = 0;
|
|
2512
|
+
while (min < 255 && !lumaBins[min]) min++;
|
|
2513
|
+
let max = 255;
|
|
2514
|
+
while (max > 0 && !lumaBins[max]) max--;
|
|
2515
|
+
return {
|
|
2516
|
+
pixels: total,
|
|
2517
|
+
luma: { mean: round3(sumY / total / 255), median: round3(percentile(0.5)), min: round3(min / 255), max: round3(max / 255), p1: round3(percentile(0.01)), p99: round3(percentile(0.99)) },
|
|
2518
|
+
clipped: { shadows: round3(shadows / total), highlights: round3(highlights / total), r: round3(cr / total), g: round3(cg / total), b: round3(cbl / total) },
|
|
2519
|
+
saturation: { mean: round3(sumSat / total), max: round3(maxSat) },
|
|
2520
|
+
average: `#${hex2(sumR / total)}${hex2(sumG / total)}${hex2(sumB / total)}`
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
function videoExposureNotes(stats) {
|
|
2524
|
+
const notes = [];
|
|
2525
|
+
const pct = (v2) => `${Math.round(v2 * 1e3) / 10}%`;
|
|
2526
|
+
if (stats.clipped.shadows >= 0.05) notes.push(`${pct(stats.clipped.shadows)} of the picture is crushed to black; raise exposure or lower contrast.`);
|
|
2527
|
+
if (stats.clipped.highlights >= 0.02) notes.push(`${pct(stats.clipped.highlights)} of the picture is blown to white; lower exposure.`);
|
|
2528
|
+
else {
|
|
2529
|
+
const channels = ["r", "g", "b"].filter((k) => stats.clipped[k] >= 0.05);
|
|
2530
|
+
if (channels.length) notes.push(`The ${channels.map((k) => ({ r: "red", g: "green", b: "blue" })[k]).join(" and ")} channel${channels.length > 1 ? "s clip" : " clips"} in ${pct(Math.max(...channels.map((k) => stats.clipped[k])))} of the picture; lower exposure or saturation.`);
|
|
2531
|
+
}
|
|
2532
|
+
if (stats.luma.mean < 0.2 && stats.clipped.shadows < 0.05) notes.push(`The picture is dark (mean luma ${stats.luma.mean}); raise exposure if it is not meant to be.`);
|
|
2533
|
+
if (stats.luma.mean > 0.75 && stats.clipped.highlights < 0.02) notes.push(`The picture is bright (mean luma ${stats.luma.mean}); lower exposure if it is not meant to be.`);
|
|
2534
|
+
if (stats.luma.p99 - stats.luma.p1 < 0.3 && stats.pixels > 1) notes.push(`The picture is flat (most of it lies between luma ${stats.luma.p1} and ${stats.luma.p99}); raise contrast.`);
|
|
2535
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(stats.average.slice(i, i + 2), 16) / 255);
|
|
2536
|
+
const [u, v] = videoChroma(r, g, b);
|
|
2537
|
+
if (Math.hypot(u, v) > 0.04 && stats.saturation.mean < 0.35) {
|
|
2538
|
+
const warm = v - u;
|
|
2539
|
+
notes.push(`The picture has a ${Math.abs(warm) > Math.abs(v + u) * 0.5 ? warm > 0 ? "warm (orange)" : "cool (blue)" : v > 0 ? "magenta" : "green"} cast (average ${stats.average}); adjust temperature and tint.`);
|
|
2540
|
+
}
|
|
2541
|
+
return notes;
|
|
2542
|
+
}
|
|
2543
|
+
var videoVectorscopeAngle = (cb, cr) => (Math.atan2(cr, cb) * 180 / Math.PI % 360 + 360) % 360;
|
|
2544
|
+
function summarizeVideoScopes(pixels, options = {}) {
|
|
2545
|
+
const stats = videoExposureStats(pixels, options);
|
|
2546
|
+
const bins = Math.max(2, Math.min(64, Math.round(_nullishCoalesce(options.bins, () => ( 16)))));
|
|
2547
|
+
const h = videoHistogram(pixels, { ...options, bins });
|
|
2548
|
+
const share = (a) => a.map((v) => round3(v / h.total));
|
|
2549
|
+
const columns = Math.max(1, Math.min(32, Math.round(_nullishCoalesce(options.columns, () => ( 8)))));
|
|
2550
|
+
const w = videoLumaWaveform(pixels, { ...options, columns, levels: 256 });
|
|
2551
|
+
const waveform = Array.from({ length: w.columns }, (_, c) => {
|
|
2552
|
+
const counts = w.data.subarray(c * 256, c * 256 + 256);
|
|
2553
|
+
const n2 = counts.reduce((a, b) => a + b, 0);
|
|
2554
|
+
let lo = 0;
|
|
2555
|
+
while (lo < 255 && !counts[lo]) lo++;
|
|
2556
|
+
let hi = 255;
|
|
2557
|
+
while (hi > 0 && !counts[hi]) hi--;
|
|
2558
|
+
let seen = 0;
|
|
2559
|
+
let median = 0;
|
|
2560
|
+
for (let i = 0; i < 256; i++) {
|
|
2561
|
+
seen += counts[i];
|
|
2562
|
+
if (seen >= n2 / 2 && n2) {
|
|
2563
|
+
median = i;
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return { start: round3(c / w.columns), end: round3((c + 1) / w.columns), min: round3(lo / 255), median: round3(median / 255), max: round3(hi / 255) };
|
|
2568
|
+
}).filter((_, c) => w.data.subarray(c * 256, c * 256 + 256).some((v) => v > 0));
|
|
2569
|
+
const chroma = [];
|
|
2570
|
+
eachPixel(pixels, Math.min(_nullishCoalesce(options.maxPixels, () => ( 65536)), 65536), (r, g, b) => {
|
|
2571
|
+
const [u, v] = videoChroma(r / 255, g / 255, b / 255);
|
|
2572
|
+
chroma.push([u, v, Math.hypot(u, v)]);
|
|
2573
|
+
});
|
|
2574
|
+
chroma.sort((a, b) => b[2] - a[2]);
|
|
2575
|
+
const top = chroma.slice(0, Math.max(1, Math.ceil(chroma.length / 10)));
|
|
2576
|
+
const mu = top.reduce((s, c) => s + c[0], 0) / top.length;
|
|
2577
|
+
const mv = top.reduce((s, c) => s + c[1], 0) / top.length;
|
|
2578
|
+
const strength = Math.min(1, top.reduce((s, c) => s + c[2], 0) / top.length / 0.5);
|
|
2579
|
+
const coherence = Math.hypot(mu, mv) / Math.max(1e-9, top.reduce((s, c) => s + c[2], 0) / top.length);
|
|
2580
|
+
return {
|
|
2581
|
+
stats,
|
|
2582
|
+
notes: videoExposureNotes(stats),
|
|
2583
|
+
histogram: { bins, luma: share(h.luma), r: share(h.r), g: share(h.g), b: share(h.b) },
|
|
2584
|
+
waveform,
|
|
2585
|
+
vectorscope: { hue: strength < 0.02 ? null : Math.round(videoVectorscopeAngle(mu, mv)), strength: round3(strength), spread: round3(1 - coherence) }
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
var VIDEO_VECTORSCOPE_TARGETS = [
|
|
2589
|
+
["R", [0.75, 0, 0]],
|
|
2590
|
+
["Mg", [0.75, 0, 0.75]],
|
|
2591
|
+
["B", [0, 0, 0.75]],
|
|
2592
|
+
["Cy", [0, 0.75, 0.75]],
|
|
2593
|
+
["G", [0, 0.75, 0]],
|
|
2594
|
+
["Yl", [0.75, 0.75, 0]]
|
|
2595
|
+
].map(([name, rgb]) => {
|
|
2596
|
+
const [cb, cr] = videoChroma(rgb[0], rgb[1], rgb[2]);
|
|
2597
|
+
return { name, rgb: [...rgb], cb, cr };
|
|
2598
|
+
});
|
|
2599
|
+
function canvas(width, height) {
|
|
2600
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
2601
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
2602
|
+
data[i] = 12;
|
|
2603
|
+
data[i + 1] = 14;
|
|
2604
|
+
data[i + 2] = 18;
|
|
2605
|
+
data[i + 3] = 255;
|
|
2606
|
+
}
|
|
2607
|
+
return { width, height, data };
|
|
2608
|
+
}
|
|
2609
|
+
function plot(img, x, y, rgb, alpha = 1, add = false) {
|
|
2610
|
+
const xi = Math.round(x);
|
|
2611
|
+
const yi = Math.round(y);
|
|
2612
|
+
if (xi < 0 || yi < 0 || xi >= img.width || yi >= img.height) return;
|
|
2613
|
+
const i = (yi * img.width + xi) * 4;
|
|
2614
|
+
for (let k = 0; k < 3; k++) img.data[i + k] = add ? img.data[i + k] + rgb[k] * alpha : img.data[i + k] * (1 - alpha) + rgb[k] * alpha;
|
|
2615
|
+
}
|
|
2616
|
+
var GRID = [70, 76, 88];
|
|
2617
|
+
var glow = (count, peak) => count ? 0.25 + 0.75 * (Math.log1p(count) / Math.log1p(Math.max(1, peak))) : 0;
|
|
2618
|
+
function videoHistogramImage(h, width = 256, height = 128) {
|
|
2619
|
+
const img = canvas(width, height);
|
|
2620
|
+
for (const q of [0.25, 0.5, 0.75]) for (let y = 0; y < height; y++) plot(img, q * (width - 1), y, GRID);
|
|
2621
|
+
const inner = (a) => a.length > 2 ? a.slice(1, -1) : a;
|
|
2622
|
+
const peak = Math.max(1, ...inner(h.r), ...inner(h.g), ...inner(h.b), ...inner(h.luma));
|
|
2623
|
+
const height01 = (v) => Math.min(1, Math.sqrt(v / peak));
|
|
2624
|
+
for (let x = 0; x < width; x++) {
|
|
2625
|
+
const bin = Math.min(h.bins - 1, Math.floor(x / width * h.bins));
|
|
2626
|
+
const channels = [
|
|
2627
|
+
[h.r, [200, 40, 40]],
|
|
2628
|
+
[h.g, [40, 180, 40]],
|
|
2629
|
+
[h.b, [50, 80, 220]]
|
|
2630
|
+
];
|
|
2631
|
+
for (const [a, rgb] of channels) {
|
|
2632
|
+
const top = height - height01(a[bin]) * height;
|
|
2633
|
+
for (let y = Math.ceil(top); y < height; y++) plot(img, x, y, rgb, 0.8, true);
|
|
2634
|
+
}
|
|
2635
|
+
plot(img, x, height - 1 - height01(h.luma[bin]) * (height - 1), [255, 255, 255]);
|
|
2636
|
+
}
|
|
2637
|
+
return img;
|
|
2638
|
+
}
|
|
2639
|
+
function videoWaveformImage(w, width = 256, height = 128) {
|
|
2640
|
+
const img = canvas(width, height);
|
|
2641
|
+
for (const q of [0, 0.25, 0.5, 0.75, 1]) for (let x = 0; x < width; x++) plot(img, x, (1 - q) * (height - 1), GRID);
|
|
2642
|
+
for (let x = 0; x < width; x++) {
|
|
2643
|
+
const c = Math.min(w.columns - 1, Math.floor(x / width * w.columns));
|
|
2644
|
+
for (let y = 0; y < height; y++) {
|
|
2645
|
+
const level = Math.min(w.levels - 1, Math.floor((height - 1 - y) / height * w.levels));
|
|
2646
|
+
const a = glow(w.data[c * w.levels + level], w.peak);
|
|
2647
|
+
if (a) plot(img, x, y, [120, 255, 140], a);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return img;
|
|
2651
|
+
}
|
|
2652
|
+
function videoVectorscopeImage(v, size = v.size) {
|
|
2653
|
+
const img = canvas(size, size);
|
|
2654
|
+
const c = (size - 1) / 2;
|
|
2655
|
+
for (let a = 0; a < 720; a++) {
|
|
2656
|
+
const t = a / 720 * 2 * Math.PI;
|
|
2657
|
+
plot(img, c + Math.cos(t) * c, c - Math.sin(t) * c, GRID);
|
|
2658
|
+
}
|
|
2659
|
+
const skin = 123 * Math.PI / 180;
|
|
2660
|
+
for (let r = 0; r < c; r++) plot(img, c + Math.cos(skin) * r, c - Math.sin(skin) * r, [140, 110, 90]);
|
|
2661
|
+
for (const t of VIDEO_VECTORSCOPE_TARGETS) {
|
|
2662
|
+
const [x, y] = videoVectorscopePoint(t.cb, t.cr, size);
|
|
2663
|
+
const rgb = t.rgb.map((k) => 80 + k * 200);
|
|
2664
|
+
for (let k = -3; k <= 3; k++) {
|
|
2665
|
+
plot(img, x + k, y - 3, rgb);
|
|
2666
|
+
plot(img, x + k, y + 3, rgb);
|
|
2667
|
+
plot(img, x - 3, y + k, rgb);
|
|
2668
|
+
plot(img, x + 3, y + k, rgb);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
for (let y = 0; y < size; y++)
|
|
2672
|
+
for (let x = 0; x < size; x++) {
|
|
2673
|
+
const sy = Math.min(v.size - 1, Math.floor(y / size * v.size));
|
|
2674
|
+
const sx = Math.min(v.size - 1, Math.floor(x / size * v.size));
|
|
2675
|
+
const a = glow(v.data[sy * v.size + sx], v.peak);
|
|
2676
|
+
if (!a) continue;
|
|
2677
|
+
const cb = sx / (v.size - 1) - 0.5;
|
|
2678
|
+
const cr = 0.5 - sy / (v.size - 1);
|
|
2679
|
+
const r = Math.min(1, Math.max(0, 0.6 + 1.5748 * cr));
|
|
2680
|
+
const g = Math.min(1, Math.max(0, 0.6 - 0.1873 * cb - 0.4681 * cr));
|
|
2681
|
+
const b = Math.min(1, Math.max(0, 0.6 + 1.8556 * cb));
|
|
2682
|
+
plot(img, x, y, [r * 255, g * 255, b * 255], a);
|
|
2683
|
+
}
|
|
2684
|
+
return img;
|
|
2685
|
+
}
|
|
2686
|
+
function videoScopeImage(kind, pixels, width = 256, height = 128, options = {}) {
|
|
2687
|
+
if (kind === "histogram") return videoHistogramImage(videoHistogram(pixels, { ...options, bins: Math.min(256, width) }), width, height);
|
|
2688
|
+
if (kind === "waveform") return videoWaveformImage(videoLumaWaveform(pixels, { ...options, columns: Math.min(pixels.width, width), levels: Math.min(256, height) }), width, height);
|
|
2689
|
+
const size = Math.min(width, height);
|
|
2690
|
+
return videoVectorscopeImage(videoVectorscope(pixels, { ...options, size }), size);
|
|
2691
|
+
}
|
|
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()}
|
|
2696
|
+
/** Gives the canvas to read (the latest preview wins). Returns a function that takes it away again. */
|
|
2697
|
+
attach(reader) {
|
|
2698
|
+
this.reader = reader;
|
|
2699
|
+
this.drawnAt = null;
|
|
2700
|
+
return () => {
|
|
2701
|
+
if (this.reader !== reader) return;
|
|
2702
|
+
this.reader = null;
|
|
2703
|
+
this.drawnAt = null;
|
|
2704
|
+
this.emit();
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
/** Whether a preview is attached. */
|
|
2708
|
+
get attached() {
|
|
2709
|
+
return this.reader !== null;
|
|
2710
|
+
}
|
|
2711
|
+
/** The preview drew the frame at `time`; `complete` is false while a picture was missing or stale. */
|
|
2712
|
+
drawn(time, complete) {
|
|
2713
|
+
this.drawnAt = { time, complete };
|
|
2714
|
+
this.emit();
|
|
2715
|
+
}
|
|
2716
|
+
/** The last frame drawn, with its pixels; `null` without a preview. Throws when the canvas cannot be read. */
|
|
2717
|
+
read() {
|
|
2718
|
+
if (!this.reader) return null;
|
|
2719
|
+
try {
|
|
2720
|
+
return this.reader();
|
|
2721
|
+
} catch (e) {
|
|
2722
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2723
|
+
throw new Error(`The preview's pixels cannot be read (${reason}). Media from another origin must be served with CORS headers (Access-Control-Allow-Origin) for its pixels to be measured.`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
/** Called after each frame the preview draws. */
|
|
2727
|
+
subscribe(listener) {
|
|
2728
|
+
this.listeners.add(listener);
|
|
2729
|
+
return () => {
|
|
2730
|
+
this.listeners.delete(listener);
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* The frame at `time` once it is complete: now, if the last frame drawn was it, else the next
|
|
2735
|
+
* complete drawing of it. Fails after `timeout` ms (default 5000).
|
|
2736
|
+
*/
|
|
2737
|
+
next(time, timeout = 5e3) {
|
|
2738
|
+
const matches = () => this.drawnAt && this.drawnAt.complete && Math.abs(this.drawnAt.time - time) < 0.5;
|
|
2739
|
+
return new Promise((resolve, reject) => {
|
|
2740
|
+
const done = (fn) => {
|
|
2741
|
+
clearTimeout(timer);
|
|
2742
|
+
off();
|
|
2743
|
+
fn();
|
|
2744
|
+
};
|
|
2745
|
+
const check = () => {
|
|
2746
|
+
if (!this.reader) return done(() => reject(new Error("The preview was closed before it drew the frame.")));
|
|
2747
|
+
if (!matches()) return;
|
|
2748
|
+
try {
|
|
2749
|
+
const snap = this.read();
|
|
2750
|
+
done(() => snap ? resolve(snap) : reject(new Error("The preview has no pixels to read.")));
|
|
2751
|
+
} catch (e) {
|
|
2752
|
+
done(() => reject(e));
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
const off = this.subscribe(check);
|
|
2756
|
+
const timer = setTimeout(() => done(() => reject(new Error(`The preview did not finish drawing ${Math.round(time)} ms within ${timeout} ms; its media may still be loading or may have failed to load. Try again, or check the asset sources.`))), timeout);
|
|
2757
|
+
check();
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
emit() {
|
|
2761
|
+
for (const l of [...this.listeners]) l();
|
|
2762
|
+
}
|
|
2763
|
+
}, _class4);
|
|
2764
|
+
function readVideoScopesPanel(feed, kinds = VIDEO_SCOPE_KINDS, options = {}) {
|
|
2765
|
+
let snap;
|
|
2766
|
+
try {
|
|
2767
|
+
snap = feed.read();
|
|
2768
|
+
} catch (e) {
|
|
2769
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
2770
|
+
}
|
|
2771
|
+
if (!snap) return { ok: false, message: feed.attached ? "The preview has not drawn a frame yet." : "Show the preview to see its scopes." };
|
|
2772
|
+
const width = _nullishCoalesce(options.width, () => ( 256));
|
|
2773
|
+
const height = _nullishCoalesce(options.height, () => ( 128));
|
|
2774
|
+
const opts = { maxPixels: _nullishCoalesce(options.maxPixels, () => ( 65536)) };
|
|
2775
|
+
const images = {};
|
|
2776
|
+
for (const kind of kinds) images[kind] = videoScopeImage(kind, snap.pixels, width, height, opts);
|
|
2777
|
+
const stats = videoExposureStats(snap.pixels, opts);
|
|
2778
|
+
return { ok: true, time: snap.time, complete: snap.complete, images, stats, notes: videoExposureNotes(stats) };
|
|
2779
|
+
}
|
|
2126
2780
|
|
|
2127
2781
|
// src/video-editor/transcript.ts
|
|
2128
2782
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -2189,8 +2843,8 @@ function videoTranscriptTrack(project, track) {
|
|
|
2189
2843
|
return track;
|
|
2190
2844
|
}
|
|
2191
2845
|
const assets = videoAssetMap(project);
|
|
2192
|
-
const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
2193
|
-
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess',
|
|
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));
|
|
2194
2848
|
}
|
|
2195
2849
|
function videoTimelineTranscript(project, options = {}) {
|
|
2196
2850
|
const id = videoTranscriptTrack(project, options.track);
|
|
@@ -2200,8 +2854,8 @@ function videoTimelineTranscript(project, options = {}) {
|
|
|
2200
2854
|
const out = [];
|
|
2201
2855
|
for (const clip of sortedVideoClips(track)) {
|
|
2202
2856
|
if (clip.type !== "media") continue;
|
|
2203
|
-
const words = _optionalChain([assets, 'access',
|
|
2204
|
-
if (!_optionalChain([words, 'optionalAccess',
|
|
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;
|
|
2205
2859
|
const speed = videoClipSpeed(clip);
|
|
2206
2860
|
const from = _nullishCoalesce(clip.in, () => ( 0));
|
|
2207
2861
|
const to = from + clip.duration * speed;
|
|
@@ -2312,7 +2966,7 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
2312
2966
|
if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
|
|
2313
2967
|
if (layer.blend) attrs.push(`style="mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}"`);
|
|
2314
2968
|
if (layer.grade) {
|
|
2315
|
-
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));
|
|
2316
2970
|
attrs.push(`filter="url(#g${i})"`);
|
|
2317
2971
|
}
|
|
2318
2972
|
if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
|
|
@@ -2375,69 +3029,96 @@ function videoContactSheetSvg(project, times, options = {}) {
|
|
|
2375
3029
|
});
|
|
2376
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>`;
|
|
2377
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
|
+
}
|
|
2378
3088
|
function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
2379
3089
|
ctx.save();
|
|
2380
3090
|
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
2381
3091
|
ctx.globalAlpha = 1;
|
|
2382
3092
|
ctx.fillStyle = frame.background;
|
|
2383
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));
|
|
2384
3096
|
for (const layer of frame.layers) {
|
|
2385
|
-
const
|
|
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;
|
|
2386
3100
|
ctx.save();
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
ctx.
|
|
2402
|
-
|
|
2403
|
-
ctx.
|
|
2404
|
-
}
|
|
2405
|
-
if (layer.wipe) {
|
|
2406
|
-
const r = videoWipeRect(rect, layer.wipe);
|
|
2407
|
-
ctx.beginPath();
|
|
2408
|
-
ctx.rect(r.x, r.y, r.width, r.height);
|
|
2409
|
-
ctx.clip();
|
|
2410
|
-
}
|
|
2411
|
-
if (layer.type === "color") {
|
|
2412
|
-
ctx.fillStyle = _nullishCoalesce(layer.color, () => ( "#000000"));
|
|
2413
|
-
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
2414
|
-
} else if (layer.type === "text" && layer.text) {
|
|
2415
|
-
const t = layer.text;
|
|
2416
|
-
const lines = t.text.split("\n");
|
|
2417
|
-
const lh = t.fontSize * 1.2;
|
|
2418
|
-
const cx = rect.x + rect.width / 2;
|
|
2419
|
-
const top = rect.y + rect.height / 2 - lh * (lines.length - 1) / 2;
|
|
2420
|
-
ctx.font = `${t.fontWeight} ${t.fontSize}px ${t.fontFamily}`;
|
|
2421
|
-
ctx.textAlign = "center";
|
|
2422
|
-
ctx.textBaseline = "middle";
|
|
2423
|
-
if (t.background) {
|
|
2424
|
-
const w = Math.max(...lines.map((l) => ctx.measureText(l).width)) + t.fontSize;
|
|
2425
|
-
const h = lh * lines.length + t.fontSize * 0.4;
|
|
2426
|
-
ctx.fillStyle = t.background;
|
|
2427
|
-
ctx.fillRect(cx - w / 2, rect.y + rect.height / 2 - h / 2, w, h);
|
|
2428
|
-
}
|
|
2429
|
-
ctx.fillStyle = t.color;
|
|
2430
|
-
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);
|
|
2431
3118
|
} else {
|
|
2432
|
-
|
|
2433
|
-
if (
|
|
2434
|
-
|
|
2435
|
-
ctx.fillStyle = "#334155";
|
|
2436
|
-
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
2437
|
-
ctx.strokeStyle = "#94a3b8";
|
|
2438
|
-
ctx.lineWidth = 2;
|
|
2439
|
-
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
2440
|
-
}
|
|
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);
|
|
2441
3122
|
}
|
|
2442
3123
|
ctx.restore();
|
|
2443
3124
|
}
|
|
@@ -2445,7 +3126,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
|
2445
3126
|
}
|
|
2446
3127
|
|
|
2447
3128
|
// src/video-editor/export.ts
|
|
2448
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
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";
|
|
2449
3130
|
function toVideoEdl(project, options = {}) {
|
|
2450
3131
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
2451
3132
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -2480,7 +3161,7 @@ function toVideoEdl(project, options = {}) {
|
|
|
2480
3161
|
lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
2481
3162
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
2482
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))}`);
|
|
2483
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
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))}`);
|
|
2484
3165
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
2485
3166
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
2486
3167
|
lines.push("");
|
|
@@ -2676,7 +3357,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2676
3357
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
2677
3358
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
2678
3359
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
2679
|
-
if (_optionalChain([next2, 'optionalAccess',
|
|
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}`);
|
|
2680
3361
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
2681
3362
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
2682
3363
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
@@ -2689,7 +3370,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2689
3370
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
2690
3371
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
2691
3372
|
const tf = clip.transform;
|
|
2692
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
3373
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _68 => _68[p]])));
|
|
2693
3374
|
if (clip.type === "text" && options.titleImage) {
|
|
2694
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 })]);
|
|
2695
3376
|
index = inputs.length - 1;
|
|
@@ -2698,7 +3379,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2698
3379
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
2699
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("*");
|
|
2700
3381
|
const coord = (axis) => {
|
|
2701
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
3382
|
+
const v = _optionalChain([tf, 'optionalAccess', _69 => _69[axis]]);
|
|
2702
3383
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
2703
3384
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
2704
3385
|
};
|
|
@@ -2706,7 +3387,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2706
3387
|
const opts = [
|
|
2707
3388
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
2708
3389
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
2709
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
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))))}`,
|
|
2710
3391
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
2711
3392
|
coord("x"),
|
|
2712
3393
|
coord("y"),
|
|
@@ -2714,8 +3395,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2714
3395
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
2715
3396
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
2716
3397
|
];
|
|
2717
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
2718
|
-
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.`);
|
|
2719
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.`);
|
|
2720
3401
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
2721
3402
|
cur = next;
|
|
@@ -2735,6 +3416,20 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2735
3416
|
}
|
|
2736
3417
|
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2737
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
|
+
}
|
|
2738
3433
|
chain.push("format=yuva420p");
|
|
2739
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)'`);
|
|
2740
3435
|
else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
|
|
@@ -2752,10 +3447,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2752
3447
|
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
2753
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)'`);
|
|
2754
3449
|
}
|
|
2755
|
-
const rotation = _optionalChain([placed, 'optionalAccess',
|
|
3450
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _77 => _77.rotation]);
|
|
2756
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`);
|
|
2757
3452
|
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
2758
|
-
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess',
|
|
3453
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _78 => _78.scale]))) {
|
|
2759
3454
|
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
2760
3455
|
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
2761
3456
|
}
|
|
@@ -2774,9 +3469,9 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2774
3469
|
const into = lead ? clip.transition.type : void 0;
|
|
2775
3470
|
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
2776
3471
|
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
2777
|
-
if (_optionalChain([after, 'optionalAccess',
|
|
3472
|
+
if (_optionalChain([after, 'optionalAccess', _79 => _79.transition, 'access', _80 => _80.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
2778
3473
|
const at = (axis, c) => {
|
|
2779
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
3474
|
+
const v = _optionalChain([placed, 'optionalAccess', _81 => _81[axis]]);
|
|
2780
3475
|
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
2781
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}'`;
|
|
2782
3477
|
};
|
|
@@ -2810,7 +3505,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2810
3505
|
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2811
3506
|
else if (options.loudness && !options.loudness.measured)
|
|
2812
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.");
|
|
2813
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
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)))
|
|
2814
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.');
|
|
2815
3510
|
if (animated.size)
|
|
2816
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)" : ""}.`);
|
|
@@ -2845,26 +3540,46 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2845
3540
|
}
|
|
2846
3541
|
|
|
2847
3542
|
// src/video-editor/preview.ts
|
|
2848
|
-
var VideoPreviewEngine = (
|
|
2849
|
-
constructor(source,
|
|
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);
|
|
2850
3545
|
this.source = source;
|
|
2851
3546
|
this.options = options;
|
|
2852
|
-
this.canvas =
|
|
2853
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([
|
|
3547
|
+
this.canvas = canvas2;
|
|
3548
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _91 => _91.getContext, 'optionalCall', _92 => _92("2d")]), () => ( null));
|
|
2854
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
|
+
};
|
|
3553
|
+
const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
|
|
3554
|
+
this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
|
|
3555
|
+
};
|
|
3556
|
+
this.pixelsTarget = _nullishCoalesce(pixels, () => ( null));
|
|
2855
3557
|
this.update();
|
|
2856
3558
|
}
|
|
2857
3559
|
|
|
2858
3560
|
|
|
2859
3561
|
|
|
2860
3562
|
|
|
2861
|
-
|
|
2862
|
-
|
|
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}
|
|
2863
3574
|
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3575
|
+
|
|
3576
|
+
|
|
3577
|
+
/** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
|
|
3578
|
+
readPixels() {
|
|
3579
|
+
const ctx = this.ctx;
|
|
3580
|
+
if (!_optionalChain([ctx, 'optionalAccess', _93 => _93.getImageData]) || !this.drawn) return null;
|
|
3581
|
+
return { ...this.drawn, pixels: ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) };
|
|
3582
|
+
}
|
|
2868
3583
|
createMedia(kind) {
|
|
2869
3584
|
if (this.options.createMedia) return this.options.createMedia(kind);
|
|
2870
3585
|
const el = document.createElement(kind);
|
|
@@ -2900,8 +3615,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2900
3615
|
entry.media.pause();
|
|
2901
3616
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
2902
3617
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
2903
|
-
_optionalChain([entry, 'access',
|
|
2904
|
-
_optionalChain([entry, 'access',
|
|
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()]);
|
|
2905
3620
|
this.media.delete(entry.clip);
|
|
2906
3621
|
}
|
|
2907
3622
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -2935,7 +3650,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2935
3650
|
for (const layer of frame.layers) {
|
|
2936
3651
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
2937
3652
|
const sound = sounding.get(layer.clip);
|
|
2938
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
3653
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _100 => _100.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
2939
3654
|
sounding.delete(layer.clip);
|
|
2940
3655
|
}
|
|
2941
3656
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -2950,13 +3665,15 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2950
3665
|
if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
|
|
2951
3666
|
});
|
|
2952
3667
|
}
|
|
2953
|
-
|
|
3668
|
+
__init21() {this.sourceFor = (layer) => {
|
|
2954
3669
|
if (layer.assetKind === "image" && layer.src) {
|
|
2955
3670
|
const img = this.image(layer.src);
|
|
2956
3671
|
return img.complete ? img : null;
|
|
2957
3672
|
}
|
|
2958
3673
|
const entry = this.media.get(layer.clip);
|
|
2959
|
-
|
|
3674
|
+
if (!entry || entry.media.readyState < 2) return null;
|
|
3675
|
+
if (entry.media.seeking) this.complete = false;
|
|
3676
|
+
return entry.media;
|
|
2960
3677
|
}}
|
|
2961
3678
|
draw(frame) {
|
|
2962
3679
|
if (!this.ctx) return;
|
|
@@ -2967,10 +3684,40 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2967
3684
|
const h = Math.max(1, Math.round(frame.height * resolution));
|
|
2968
3685
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
2969
3686
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
2970
|
-
|
|
2971
|
-
|
|
3687
|
+
this.complete = true;
|
|
3688
|
+
const sourceFor = (layer) => {
|
|
3689
|
+
const source = this.sourceFor(layer);
|
|
3690
|
+
if (!source) this.complete = false;
|
|
3691
|
+
return source;
|
|
3692
|
+
};
|
|
3693
|
+
drawVideoFrame(this.ctx, frame, sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter, lutFor: this.lutFor, scratch: this.scratch });
|
|
3694
|
+
this.drawn = { time: frame.time, complete: this.complete };
|
|
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)]);
|
|
2972
3697
|
}
|
|
2973
|
-
|
|
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) => {
|
|
2974
3721
|
if (this.options.defineSvgFilter === null) return;
|
|
2975
3722
|
if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
|
|
2976
3723
|
if (this.filters.has(id) || typeof document === "undefined") return;
|
|
@@ -2989,35 +3736,42 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2989
3736
|
/** Stops following the editor and releases every media element. */
|
|
2990
3737
|
destroy() {
|
|
2991
3738
|
this.destroyed = true;
|
|
2992
|
-
|
|
3739
|
+
this.detachPixels();
|
|
3740
|
+
this.unsubscribeLuts();
|
|
3741
|
+
_optionalChain([this, 'access', _110 => _110.filterHost, 'optionalAccess', _111 => _111.remove, 'call', _112 => _112()]);
|
|
2993
3742
|
this.filterHost = null;
|
|
2994
3743
|
this.filters.clear();
|
|
2995
3744
|
this.unsubscribe();
|
|
2996
3745
|
for (const entry of [...this.media.values()]) this.release(entry);
|
|
2997
3746
|
this.images.clear();
|
|
2998
3747
|
}
|
|
2999
|
-
},
|
|
3748
|
+
}, _class5);
|
|
3000
3749
|
|
|
3001
3750
|
// src/video-editor/controller.ts
|
|
3002
|
-
var VideoEditorController = (
|
|
3751
|
+
var VideoEditorController = (_class6 = class {
|
|
3003
3752
|
|
|
3004
3753
|
|
|
3005
3754
|
/** Sound levels per asset, loaded on demand for waveforms and agents. */
|
|
3006
3755
|
|
|
3756
|
+
/** The pixels the preview last drew, for scopes and `get_video_scopes` (a `VideoPreviewEngine` fills it). */
|
|
3757
|
+
__init25() {this.previewPixels = new VideoPreviewPixels()}
|
|
3758
|
+
/** Grades' LUTs, loaded on demand for the preview, SVG frames and agents. */
|
|
3007
3759
|
|
|
3008
3760
|
|
|
3009
|
-
__init14() {this.selection = []}
|
|
3010
3761
|
|
|
3011
|
-
|
|
3012
|
-
__init16() {this.revision = 0}
|
|
3013
|
-
__init17() {this.applying = false}
|
|
3762
|
+
__init26() {this.selection = []}
|
|
3014
3763
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3764
|
+
__init27() {this.error = null}
|
|
3765
|
+
__init28() {this.revision = 0}
|
|
3766
|
+
__init29() {this.applying = false}
|
|
3017
3767
|
|
|
3018
|
-
|
|
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);
|
|
3019
3772
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
3020
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);
|
|
3021
3775
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
3022
3776
|
this.issues = checkVideoProject(this.project);
|
|
3023
3777
|
this.readOnly = _nullishCoalesce(options.readOnly, () => ( false));
|
|
@@ -3040,8 +3794,8 @@ var VideoEditorController = (_class4 = class {
|
|
|
3040
3794
|
this.player.pause();
|
|
3041
3795
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
3042
3796
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3797
|
+
__init32() {this.getState = () => this.state}
|
|
3798
|
+
__init33() {this.subscribe = (listener) => {
|
|
3045
3799
|
this.connect();
|
|
3046
3800
|
this.listeners.add(listener);
|
|
3047
3801
|
return () => {
|
|
@@ -3097,7 +3851,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
3097
3851
|
this.applying = false;
|
|
3098
3852
|
}
|
|
3099
3853
|
this.notify();
|
|
3100
|
-
_optionalChain([this, 'access',
|
|
3854
|
+
_optionalChain([this, 'access', _113 => _113.onChange, 'optionalCall', _114 => _114(project)]);
|
|
3101
3855
|
}
|
|
3102
3856
|
commit(label, next) {
|
|
3103
3857
|
const before = this.project;
|
|
@@ -3161,7 +3915,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
3161
3915
|
}
|
|
3162
3916
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
3163
3917
|
select(clips, options = {}) {
|
|
3164
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
3918
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _115 => _115.filter, 'call', _116 => _116((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
3165
3919
|
let next;
|
|
3166
3920
|
if (options.add) {
|
|
3167
3921
|
next = [...this.selection];
|
|
@@ -3228,7 +3982,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
3228
3982
|
}
|
|
3229
3983
|
/** The frame at `time` (default: the playhead) as SVG. */
|
|
3230
3984
|
toSvg(time = this.player.currentTime, options = {}) {
|
|
3231
|
-
return videoProjectSvg(this.project, time, options);
|
|
3985
|
+
return videoProjectSvg(this.project, time, { lutFor: (src) => this.luts.peek(src), ...options });
|
|
3232
3986
|
}
|
|
3233
3987
|
/** The project as an ffmpeg command. */
|
|
3234
3988
|
toFfmpeg(options = {}) {
|
|
@@ -3242,69 +3996,307 @@ var VideoEditorController = (_class4 = class {
|
|
|
3242
3996
|
toEdl(options = {}) {
|
|
3243
3997
|
return toVideoEdl(this.project, options);
|
|
3244
3998
|
}
|
|
3245
|
-
},
|
|
3999
|
+
}, _class6);
|
|
3246
4000
|
|
|
3247
|
-
// src/video-editor/
|
|
3248
|
-
var
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
var
|
|
3266
|
-
|
|
3267
|
-
|
|
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"
|
|
3268
4027
|
};
|
|
3269
|
-
var
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
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"
|
|
3273
4035
|
};
|
|
3274
|
-
var
|
|
3275
|
-
var
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
"
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
["
|
|
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"
|
|
4051
|
+
};
|
|
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"
|
|
4058
|
+
};
|
|
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"],
|
|
3308
4300
|
when
|
|
3309
4301
|
);
|
|
3310
4302
|
var CLIP_COMMON = {
|
|
@@ -3455,14 +4447,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3455
4447
|
return null;
|
|
3456
4448
|
};
|
|
3457
4449
|
const placed = (o) => ({
|
|
3458
|
-
..._optionalChain([o, 'access',
|
|
3459
|
-
..._optionalChain([o, 'access',
|
|
3460
|
-
..._optionalChain([o, 'access',
|
|
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 } : {},
|
|
3461
4453
|
...o.rippled ? { rippled: o.rippled } : {}
|
|
3462
4454
|
});
|
|
3463
4455
|
const keysOut = (p, clip, property) => {
|
|
3464
4456
|
const c = clipOut(p, clip);
|
|
3465
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
4457
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _134 => _134.transform, 'optionalAccess', _135 => _135[property]]) : _optionalChain([c, 'optionalAccess', _136 => _136[property]]);
|
|
3466
4458
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
3467
4459
|
};
|
|
3468
4460
|
const getProject = {
|
|
@@ -3523,14 +4515,18 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3523
4515
|
annotations: { title: "Render a frame", ...READ },
|
|
3524
4516
|
handler: async ({ time, times, columns, format = "svg" }) => {
|
|
3525
4517
|
const p = project();
|
|
3526
|
-
const
|
|
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."] : []];
|
|
3527
4523
|
if (format === "png") {
|
|
3528
4524
|
if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_video_frame with format "svg".');
|
|
3529
4525
|
const size = /viewBox="0 0 ([\d.]+) ([\d.]+)"/.exec(svg);
|
|
3530
4526
|
const image = await options.rasterize(svg, { width: Number(size[1]), height: Number(size[2]) });
|
|
3531
|
-
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(" ") }] : []] };
|
|
3532
4528
|
}
|
|
3533
|
-
return { svg };
|
|
4529
|
+
return { svg, ...notes.length ? { notes } : {} };
|
|
3534
4530
|
}
|
|
3535
4531
|
};
|
|
3536
4532
|
const LOUDNESS_TARGET = {
|
|
@@ -3760,14 +4756,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3760
4756
|
const at = findVideoClip(p, id);
|
|
3761
4757
|
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3762
4758
|
const { clip } = at;
|
|
3763
|
-
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access',
|
|
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")
|
|
3764
4760
|
throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
|
|
3765
4761
|
}
|
|
3766
4762
|
};
|
|
3767
4763
|
const applyGradePreset = {
|
|
3768
4764
|
name: "apply_video_grade_preset",
|
|
3769
4765
|
title: "Apply a colour look",
|
|
3770
|
-
description: "Gives clips a named colour look at a strength (one undo step), replacing their
|
|
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.",
|
|
3771
4767
|
inputSchema: object(
|
|
3772
4768
|
{
|
|
3773
4769
|
clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to grade." },
|
|
@@ -3779,9 +4775,11 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3779
4775
|
annotations: { title: "Apply a colour look", ...WRITE_IDEMPOTENT },
|
|
3780
4776
|
handler: ({ clips, preset, amount }) => {
|
|
3781
4777
|
gradeTargets(clips);
|
|
3782
|
-
const
|
|
3783
|
-
|
|
3784
|
-
|
|
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)) })) }));
|
|
3785
4783
|
}
|
|
3786
4784
|
};
|
|
3787
4785
|
const copyGrade = {
|
|
@@ -3797,16 +4795,53 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3797
4795
|
return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
|
|
3798
4796
|
}
|
|
3799
4797
|
};
|
|
3800
|
-
const
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
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
|
+
};
|
|
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)." };
|
|
4836
|
+
const RANGE_OUT = (r) => ({ from: round(r.from), to: round(r.to), text: r.text });
|
|
4837
|
+
const timelineWords = (track) => {
|
|
4838
|
+
const words = videoTimelineTranscript(project(), { track });
|
|
4839
|
+
if (!words.length) throw new Error(track ? `No clip on track "${track}" plays a transcribed asset; attach one with set_video_transcript.` : "No clip plays a transcribed asset; attach a transcript to the media with set_video_transcript first.");
|
|
4840
|
+
return words;
|
|
4841
|
+
};
|
|
4842
|
+
const setTranscript = {
|
|
4843
|
+
name: "set_video_transcript",
|
|
4844
|
+
title: "Attach a transcript",
|
|
3810
4845
|
description: 'Attaches what is said in a video or audio asset, word by word, so the edit can be cut by text (one undo step). Pass "text" (Whisper-style JSON with word timings, SubRip or WebVTT, as the file says it) or "words" ([{ word, start, end }] in ms of the source). Captions have no word timings, so their words are spread across each cue (approximate). Replaces any transcript the asset had. Returns the word count, whether timings are approximate, and the first and last words.',
|
|
3811
4846
|
inputSchema: object(
|
|
3812
4847
|
{
|
|
@@ -3848,7 +4883,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3848
4883
|
handler: ({ track, from, to, limit }) => {
|
|
3849
4884
|
const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
|
|
3850
4885
|
const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
|
|
3851
|
-
return { track: _nullishCoalesce(_optionalChain([words, 'access',
|
|
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 };
|
|
3852
4887
|
}
|
|
3853
4888
|
};
|
|
3854
4889
|
const findText = {
|
|
@@ -3910,7 +4945,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3910
4945
|
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3911
4946
|
const { clip } = at;
|
|
3912
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.`);
|
|
3913
|
-
if (_optionalChain([project, 'call',
|
|
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.`);
|
|
3914
4949
|
return clip;
|
|
3915
4950
|
};
|
|
3916
4951
|
const rampOut = (r, id) => {
|
|
@@ -4199,8 +5234,47 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4199
5234
|
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
|
|
4200
5235
|
}
|
|
4201
5236
|
};
|
|
4202
|
-
const
|
|
4203
|
-
|
|
5237
|
+
const pixelsAt = async (time) => {
|
|
5238
|
+
if (options.framePixels) {
|
|
5239
|
+
const px = await options.framePixels(time);
|
|
5240
|
+
if (!px) throw new Error(`The host could not render the frame at ${Math.round(time)} ms; use render_video_frame to look at it instead.`);
|
|
5241
|
+
return px;
|
|
5242
|
+
}
|
|
5243
|
+
const feed = controller.previewPixels;
|
|
5244
|
+
if (!feed.attached) throw new Error("No preview is showing this project, so there are no pixels to measure. The host must show a <VideoPreview> for this editor (or pass framePixels to the tool provider); until then use get_video_frame and render_video_frame.");
|
|
5245
|
+
const state = controller.getState();
|
|
5246
|
+
if (Math.abs(time - state.time) < 0.5) return (await feed.next(state.time, options.framePixelsTimeout)).pixels;
|
|
5247
|
+
const { time: before, playing } = state;
|
|
5248
|
+
if (playing) controller.pause();
|
|
5249
|
+
controller.seek(time);
|
|
5250
|
+
try {
|
|
5251
|
+
return (await feed.next(controller.getState().time, options.framePixelsTimeout)).pixels;
|
|
5252
|
+
} finally {
|
|
5253
|
+
controller.seek(before);
|
|
5254
|
+
if (playing) controller.play();
|
|
5255
|
+
}
|
|
5256
|
+
};
|
|
5257
|
+
const getScopes = {
|
|
5258
|
+
name: "get_video_scopes",
|
|
5259
|
+
title: "Measure a frame with scopes",
|
|
5260
|
+
description: "Measures the rendered picture at a moment as colour scopes would, without changing the project: luma (0 = black, 1 = white) mean, median, min, max and 1st / 99th percentiles; the share of pixels crushed to black or blown to white (and per channel); saturation; the average colour (a cast shows as a tint); a coarse luma and RGB histogram; the luma spread of vertical strips left to right (a waveform); the main hue and strength on a vectorscope; and notes on what to fix. Use it before and after grading (update_video_clip grade, apply_video_grade_preset) to check exposure and colour. Needs the pixels the host draws: fails with a reason when no preview is showing.",
|
|
5261
|
+
inputSchema: object({
|
|
5262
|
+
time: MS("Time from the start to measure (default: the playhead). Another time is shown briefly and the playhead put back"),
|
|
5263
|
+
bins: { type: "integer", minimum: 2, maximum: 64, description: "Histogram steps, darkest first (default 16)." },
|
|
5264
|
+
columns: { type: "integer", minimum: 1, maximum: 32, description: "Waveform strips across the picture, left to right (default 8)." }
|
|
5265
|
+
}),
|
|
5266
|
+
annotations: { title: "Measure a frame with scopes", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
5267
|
+
handler: async ({ time, bins, columns }) => {
|
|
5268
|
+
const p = project();
|
|
5269
|
+
const end = videoProjectDuration(p);
|
|
5270
|
+
const t = _nullishCoalesce(time, () => ( controller.getState().time));
|
|
5271
|
+
if (time !== void 0 && time > end) throw new Error(`Time ${time} ms is past the end of the project (${end} ms).`);
|
|
5272
|
+
const pixels = await pixelsAt(t);
|
|
5273
|
+
return { time: t, timecode: formatVideoTimecode(t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
|
|
5274
|
+
}
|
|
5275
|
+
};
|
|
5276
|
+
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
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];
|
|
4204
5278
|
return {
|
|
4205
5279
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
4206
5280
|
namespace: options.namespace,
|
|
@@ -4218,226 +5292,6 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4218
5292
|
};
|
|
4219
5293
|
}
|
|
4220
5294
|
|
|
4221
|
-
// src/video-editor/ui.ts
|
|
4222
|
-
var STEPS = [40, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 15e3, 3e4, 6e4, 12e4, 3e5, 6e5];
|
|
4223
|
-
function videoRulerTicks(duration, pxPerSecond, fps) {
|
|
4224
|
-
const pxPerMs = pxPerSecond / 1e3;
|
|
4225
|
-
const major = _nullishCoalesce(STEPS.find((s) => s * pxPerMs >= 80), () => ( STEPS[STEPS.length - 1]));
|
|
4226
|
-
const minor = STEPS.slice().reverse().find((s) => s < major && major % s === 0 && s * pxPerMs >= 10);
|
|
4227
|
-
const step = _nullishCoalesce(minor, () => ( major));
|
|
4228
|
-
const ticks = [];
|
|
4229
|
-
for (let t = 0; t <= duration + 1e-6; t += step) {
|
|
4230
|
-
const isMajor = Math.abs(t / major - Math.round(t / major)) < 1e-9;
|
|
4231
|
-
ticks.push({ time: t, x: t * pxPerMs, major: isMajor, ...isMajor ? { label: formatVideoTimecode(t, fps).replace(/^00:/, "") } : {} });
|
|
4232
|
-
}
|
|
4233
|
-
return ticks;
|
|
4234
|
-
}
|
|
4235
|
-
function videoClipShowsWaveform(clip, asset, mode = "audio") {
|
|
4236
|
-
if (clip.type !== "media" || mode === "none" || !videoAssetHasSound(asset)) return false;
|
|
4237
|
-
return mode === "all" || asset.kind === "audio";
|
|
4238
|
-
}
|
|
4239
|
-
var VIDEO_CLIP_ANIMATION_LABELS = {
|
|
4240
|
-
fade: "Fade",
|
|
4241
|
-
rise: "Rise",
|
|
4242
|
-
drop: "Drop",
|
|
4243
|
-
"slide-left": "Slide left",
|
|
4244
|
-
"slide-right": "Slide right",
|
|
4245
|
-
zoom: "Zoom",
|
|
4246
|
-
pop: "Pop"
|
|
4247
|
-
};
|
|
4248
|
-
var VIDEO_TRANSITION_LABELS = {
|
|
4249
|
-
crossfade: "Crossfade",
|
|
4250
|
-
dip: "Dip to colour",
|
|
4251
|
-
wipe: "Wipe",
|
|
4252
|
-
push: "Push",
|
|
4253
|
-
slide: "Slide",
|
|
4254
|
-
iris: "Iris"
|
|
4255
|
-
};
|
|
4256
|
-
var videoTransitionHasDirection = (type) => type === "wipe" || type === "push" || type === "slide";
|
|
4257
|
-
var VIDEO_BLEND_MODE_LABELS = {
|
|
4258
|
-
normal: "Normal",
|
|
4259
|
-
multiply: "Multiply",
|
|
4260
|
-
screen: "Screen",
|
|
4261
|
-
overlay: "Overlay",
|
|
4262
|
-
darken: "Darken",
|
|
4263
|
-
lighten: "Lighten",
|
|
4264
|
-
"color-dodge": "Colour dodge",
|
|
4265
|
-
"color-burn": "Colour burn",
|
|
4266
|
-
"hard-light": "Hard light",
|
|
4267
|
-
"soft-light": "Soft light",
|
|
4268
|
-
difference: "Difference",
|
|
4269
|
-
exclusion: "Exclusion",
|
|
4270
|
-
add: "Add"
|
|
4271
|
-
};
|
|
4272
|
-
var VIDEO_FADE_CURVE_LABELS = {
|
|
4273
|
-
linear: "Linear",
|
|
4274
|
-
"equal-power": "Equal power",
|
|
4275
|
-
"s-curve": "S-curve",
|
|
4276
|
-
exponential: "Exponential",
|
|
4277
|
-
logarithmic: "Logarithmic"
|
|
4278
|
-
};
|
|
4279
|
-
function videoClipTint(clip, asset) {
|
|
4280
|
-
if (clip.type === "text") return "#a855f7";
|
|
4281
|
-
if (clip.type === "color") return clip.color;
|
|
4282
|
-
if (!asset) return "#ef4444";
|
|
4283
|
-
return asset.kind === "audio" ? "#10b981" : asset.kind === "image" ? "#f59e0b" : "#3b82f6";
|
|
4284
|
-
}
|
|
4285
|
-
function videoClipLabel(clip, asset) {
|
|
4286
|
-
if (clip.name) return clip.name;
|
|
4287
|
-
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
4288
|
-
if (clip.type === "color") return clip.color;
|
|
4289
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _115 => _115.name]), () => ( _optionalChain([asset, 'optionalAccess', _116 => _116.src, 'access', _117 => _117.replace, 'call', _118 => _118(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
4290
|
-
const speed = videoClipSpeed(clip);
|
|
4291
|
-
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
4292
|
-
}
|
|
4293
|
-
function planVideoClipDrag(project, input) {
|
|
4294
|
-
const at = findVideoClip(project, input.clip);
|
|
4295
|
-
if (!at) return null;
|
|
4296
|
-
const { clip, track } = at;
|
|
4297
|
-
const snap = _nullishCoalesce(input.snap, () => ( ((t) => t)));
|
|
4298
|
-
if (input.mode === "move") {
|
|
4299
|
-
const raw = Math.max(0, clip.start + input.delta);
|
|
4300
|
-
const head = snap(raw) - raw;
|
|
4301
|
-
const tail = snap(raw + clip.duration) - (raw + clip.duration);
|
|
4302
|
-
const shifts = [head, tail].filter((d) => Math.abs(d) > 1e-9);
|
|
4303
|
-
const start = Math.max(0, raw + (shifts.length ? shifts.reduce((a, b) => Math.abs(b) < Math.abs(a) ? b : a) : 0));
|
|
4304
|
-
const target = _nullishCoalesce(input.track, () => ( track.id));
|
|
4305
|
-
const moved = Math.abs(start - clip.start) > 1e-6 || target !== track.id;
|
|
4306
|
-
const mode = input.placement && input.placement !== "refuse" ? { mode: input.placement } : {};
|
|
4307
|
-
return { start, duration: clip.duration, track: target, edit: moved ? { op: "move_clip", clip: clip.id, start, ...target !== track.id ? { track: target } : {}, ...mode } : null };
|
|
4308
|
-
}
|
|
4309
|
-
const minimum = 1e3 / project.fps;
|
|
4310
|
-
if (input.mode === "trim-start") {
|
|
4311
|
-
const to2 = Math.min(snap(Math.max(0, clip.start + input.delta)), videoClipEnd(clip) - minimum);
|
|
4312
|
-
const changed2 = Math.abs(to2 - clip.start) > 1e-6;
|
|
4313
|
-
const plan = input.ripple ? { start: clip.start, duration: clip.duration - (to2 - clip.start) } : { start: to2, duration: videoClipEnd(clip) - to2 };
|
|
4314
|
-
return { ...plan, track: track.id, edit: changed2 ? { op: "trim_clip", clip: clip.id, edge: "start", to: to2, ...input.ripple ? { ripple: true } : {} } : null };
|
|
4315
|
-
}
|
|
4316
|
-
const to = Math.max(snap(videoClipEnd(clip) + input.delta), clip.start + minimum);
|
|
4317
|
-
const changed = Math.abs(to - videoClipEnd(clip)) > 1e-6;
|
|
4318
|
-
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 };
|
|
4319
|
-
}
|
|
4320
|
-
function videoDropTracks(project, clipId) {
|
|
4321
|
-
const at = findVideoClip(project, clipId);
|
|
4322
|
-
if (!at) return [];
|
|
4323
|
-
return project.tracks.filter((t) => t.kind === at.track.kind && !t.locked).map((t) => t.id);
|
|
4324
|
-
}
|
|
4325
|
-
function videoDragLabel(edit) {
|
|
4326
|
-
if (edit.op === "move_clip") return edit.mode === "insert" ? "Insert clip" : edit.mode === "overwrite" ? "Overwrite clip" : "Move clip";
|
|
4327
|
-
return edit.op === "trim_clip" && edit.ripple ? "Ripple trim" : "Trim clip";
|
|
4328
|
-
}
|
|
4329
|
-
function videoAssetDropEdit(project, assetId, options = {}) {
|
|
4330
|
-
const asset = videoAssetMap(project).get(assetId);
|
|
4331
|
-
if (!asset) return null;
|
|
4332
|
-
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
4333
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _119 => _119.tracks, 'access', _120 => _120.find, 'call', _121 => _121((t) => t.kind === kind && !t.locked), 'optionalAccess', _122 => _122.id])));
|
|
4334
|
-
if (!track) return null;
|
|
4335
|
-
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 } : {} };
|
|
4336
|
-
}
|
|
4337
|
-
function videoAssetKindOf(typeOrName) {
|
|
4338
|
-
const s = typeOrName.toLowerCase().replace(/[?#].*$/, "");
|
|
4339
|
-
if (s.startsWith("video/") || /\.(mp4|m4v|mov|webm|ogv|mkv)$/.test(s)) return "video";
|
|
4340
|
-
if (s.startsWith("audio/") || /\.(mp3|m4a|aac|wav|ogg|oga|flac|opus)$/.test(s)) return "audio";
|
|
4341
|
-
if (s.startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg|bmp)$/.test(s)) return "image";
|
|
4342
|
-
return null;
|
|
4343
|
-
}
|
|
4344
|
-
function probeVideoMedia(src, kind, timeout = 15e3) {
|
|
4345
|
-
return new Promise((resolve, reject) => {
|
|
4346
|
-
if (typeof document === "undefined") return reject(new Error("Media can only be probed in a browser."));
|
|
4347
|
-
const timer = setTimeout(() => reject(new Error(`"${src}" took too long to load.`)), timeout);
|
|
4348
|
-
const done = (probe) => {
|
|
4349
|
-
clearTimeout(timer);
|
|
4350
|
-
resolve(probe);
|
|
4351
|
-
};
|
|
4352
|
-
const failed = () => {
|
|
4353
|
-
clearTimeout(timer);
|
|
4354
|
-
reject(new Error(`"${src}" could not be loaded as ${kind}.`));
|
|
4355
|
-
};
|
|
4356
|
-
if (kind === "image") {
|
|
4357
|
-
const img = new Image();
|
|
4358
|
-
img.onload = () => done({ kind, width: img.naturalWidth, height: img.naturalHeight });
|
|
4359
|
-
img.onerror = failed;
|
|
4360
|
-
img.src = src;
|
|
4361
|
-
return;
|
|
4362
|
-
}
|
|
4363
|
-
const el = document.createElement(kind);
|
|
4364
|
-
el.preload = "metadata";
|
|
4365
|
-
el.onloadedmetadata = () => {
|
|
4366
|
-
const duration = Number.isFinite(el.duration) ? Math.round(el.duration * 1e3) : void 0;
|
|
4367
|
-
const v = el;
|
|
4368
|
-
done({ kind, ...duration ? { duration } : {}, ...kind === "video" && v.videoWidth ? { width: v.videoWidth, height: v.videoHeight } : {} });
|
|
4369
|
-
};
|
|
4370
|
-
el.onerror = failed;
|
|
4371
|
-
el.src = src;
|
|
4372
|
-
});
|
|
4373
|
-
}
|
|
4374
|
-
var KEY_LABELS = {
|
|
4375
|
-
opacity: { label: "Opacity", step: 0.05 },
|
|
4376
|
-
volume: { label: "Volume", step: 0.05 },
|
|
4377
|
-
x: { label: "X", step: 1 },
|
|
4378
|
-
y: { label: "Y", step: 1 },
|
|
4379
|
-
scale: { label: "Scale", step: 0.05 },
|
|
4380
|
-
rotation: { label: "Rotation", step: 1 }
|
|
4381
|
-
};
|
|
4382
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _123 => _123.transform, 'optionalAccess', _124 => _124[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
4383
|
-
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
4384
|
-
function videoKeyframeRows(project, clipId, time) {
|
|
4385
|
-
const at = findVideoClip(project, clipId);
|
|
4386
|
-
if (!at) return [];
|
|
4387
|
-
const { clip } = at;
|
|
4388
|
-
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
4389
|
-
const visual = !(_optionalChain([asset, 'optionalAccess', _125 => _125.kind]) === "audio" || at.track.kind === "audio");
|
|
4390
|
-
const audible = clip.type === "media" && videoAssetHasSound(asset);
|
|
4391
|
-
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
4392
|
-
const clipTime = playheadInClip(clip, time);
|
|
4393
|
-
return properties.map((property) => {
|
|
4394
|
-
const prop = animatableOf(clip, property);
|
|
4395
|
-
const keys = isVideoKeyframes(prop) ? prop.keys : [];
|
|
4396
|
-
const limits = VIDEO_KEYFRAME_LIMITS[property];
|
|
4397
|
-
return {
|
|
4398
|
-
property,
|
|
4399
|
-
...KEY_LABELS[property],
|
|
4400
|
-
value: Math.round(videoValueAt(prop, clipTime, limits.default) * 1e3) / 1e3,
|
|
4401
|
-
keyed: keys.length > 0,
|
|
4402
|
-
keys: keys.length,
|
|
4403
|
-
onKey: keys.some((k) => Math.abs(k.t - clipTime) < 0.5),
|
|
4404
|
-
clipTime,
|
|
4405
|
-
...limits.min !== void 0 ? { min: limits.min } : {},
|
|
4406
|
-
...limits.max !== void 0 ? { max: limits.max } : {}
|
|
4407
|
-
};
|
|
4408
|
-
});
|
|
4409
|
-
}
|
|
4410
|
-
function plainValueEdit(clip, property, value) {
|
|
4411
|
-
if (!VIDEO_TRANSFORM_PROPERTIES.includes(property))
|
|
4412
|
-
return value === void 0 ? { op: "update_clip", clip: clip.id, remove: [property] } : { op: "update_clip", clip: clip.id, set: { [property]: value } };
|
|
4413
|
-
const transform = { ...clip.transform, [property]: value };
|
|
4414
|
-
if (value === void 0) delete transform[property];
|
|
4415
|
-
return Object.keys(transform).length ? { op: "update_clip", clip: clip.id, set: { transform } } : { op: "update_clip", clip: clip.id, remove: ["transform"] };
|
|
4416
|
-
}
|
|
4417
|
-
function videoKeyframeValueEdit(project, clipId, property, time, value) {
|
|
4418
|
-
const at = findVideoClip(project, clipId);
|
|
4419
|
-
if (!at) return null;
|
|
4420
|
-
const prop = animatableOf(at.clip, property);
|
|
4421
|
-
if (!isVideoKeyframes(prop)) return plainValueEdit(at.clip, property, value);
|
|
4422
|
-
if (value === void 0) return null;
|
|
4423
|
-
return { op: "set_keyframes", clip: clipId, property, keys: setVideoKey(prop, { t: playheadInClip(at.clip, time), v: value }, VIDEO_KEYFRAME_LIMITS[property].default).keys };
|
|
4424
|
-
}
|
|
4425
|
-
function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
4426
|
-
const at = findVideoClip(project, clipId);
|
|
4427
|
-
if (!at) return null;
|
|
4428
|
-
const prop = animatableOf(at.clip, property);
|
|
4429
|
-
const t = playheadInClip(at.clip, time);
|
|
4430
|
-
const fallback = VIDEO_KEYFRAME_LIMITS[property].default;
|
|
4431
|
-
if (isVideoKeyframes(prop)) {
|
|
4432
|
-
const key = prop.keys.find((k) => Math.abs(k.t - t) < 0.5);
|
|
4433
|
-
if (key) {
|
|
4434
|
-
const rest = removeVideoKey(prop, key.t);
|
|
4435
|
-
return isVideoKeyframes(rest) ? { op: "set_keyframes", clip: clipId, property, keys: rest.keys } : plainValueEdit(at.clip, property, rest === fallback ? void 0 : rest);
|
|
4436
|
-
}
|
|
4437
|
-
}
|
|
4438
|
-
const v = videoValueAt(prop, t, fallback);
|
|
4439
|
-
return { op: "set_keyframes", clip: clipId, property, keys: isVideoKeyframes(prop) ? setVideoKey(prop, { t, v }, fallback).keys : [{ t, v }] };
|
|
4440
|
-
}
|
|
4441
5295
|
|
|
4442
5296
|
|
|
4443
5297
|
|
|
@@ -4610,5 +5464,35 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
4610
5464
|
|
|
4611
5465
|
|
|
4612
5466
|
|
|
4613
|
-
|
|
5467
|
+
|
|
5468
|
+
|
|
5469
|
+
|
|
5470
|
+
|
|
5471
|
+
|
|
5472
|
+
|
|
5473
|
+
|
|
5474
|
+
|
|
5475
|
+
|
|
5476
|
+
|
|
5477
|
+
|
|
5478
|
+
|
|
5479
|
+
|
|
5480
|
+
|
|
5481
|
+
|
|
5482
|
+
|
|
5483
|
+
|
|
5484
|
+
|
|
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;
|
|
4614
5498
|
//# sourceMappingURL=index.cjs.map
|