@pixerate/editor-react 0.56.0 → 0.58.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.
|
@@ -41,11 +41,11 @@ function useVideoEditor(controllerOrOptions = {}) {
|
|
|
41
41
|
return { controller, state, ...actions };
|
|
42
42
|
}
|
|
43
43
|
function useVideoEditorTools(registry, controller, options = {}) {
|
|
44
|
-
const { id, namespace, readOnly, rasterize } = options;
|
|
44
|
+
const { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout } = options;
|
|
45
45
|
useEffect(() => {
|
|
46
46
|
if (!registry) return;
|
|
47
|
-
return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize }));
|
|
48
|
-
}, [registry, controller, id, namespace, readOnly, rasterize]);
|
|
47
|
+
return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
|
|
48
|
+
}, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/video-editor/VideoPreview.tsx
|
|
@@ -76,28 +76,96 @@ function VideoPreview({ controller, resolution, onFrame, className = "", style }
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// src/video-editor/VideoScopes.tsx
|
|
80
|
+
import { useEffect as useEffect3, useRef as useRef3, useState } from "react";
|
|
81
|
+
import { readVideoScopesPanel, VIDEO_SCOPE_KINDS } from "@pixerate/editor/video-editor";
|
|
82
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
83
|
+
var LABELS = { histogram: "Histogram", waveform: "Luma waveform", vectorscope: "Vectorscope" };
|
|
84
|
+
var pct = (v) => `${Math.round(v * 1e3) / 10}%`;
|
|
85
|
+
function VideoScopes({ controller, scopes = VIDEO_SCOPE_KINDS, width = 256, height = 128, className = "" }) {
|
|
86
|
+
const canvases = useRef3({});
|
|
87
|
+
const [reading, setReading] = useState(() => ({ ok: false, message: "Show the preview to see its scopes." }));
|
|
88
|
+
const key = scopes.join(",");
|
|
89
|
+
useEffect3(() => {
|
|
90
|
+
const feed = controller.previewPixels;
|
|
91
|
+
let frame = 0;
|
|
92
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
|
|
93
|
+
const caf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : (id) => clearTimeout(id);
|
|
94
|
+
const draw = () => {
|
|
95
|
+
frame = 0;
|
|
96
|
+
const next = readVideoScopesPanel(feed, scopes, { width, height });
|
|
97
|
+
setReading(next);
|
|
98
|
+
if (!next.ok) return;
|
|
99
|
+
for (const kind of scopes) {
|
|
100
|
+
const img = next.images[kind];
|
|
101
|
+
const ctx = canvases.current[kind]?.getContext("2d");
|
|
102
|
+
if (img && ctx && typeof ImageData !== "undefined") ctx.putImageData(new ImageData(img.data, img.width, img.height), 0, 0);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const schedule = () => {
|
|
106
|
+
if (!frame) frame = raf(draw);
|
|
107
|
+
};
|
|
108
|
+
schedule();
|
|
109
|
+
const off = feed.subscribe(schedule);
|
|
110
|
+
return () => {
|
|
111
|
+
off();
|
|
112
|
+
if (frame) caf(frame);
|
|
113
|
+
};
|
|
114
|
+
}, [controller, key, width, height]);
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-video-scopes": "", children: [
|
|
116
|
+
/* @__PURE__ */ jsx2("div", { className: "flex flex-wrap gap-2", children: scopes.map((kind) => {
|
|
117
|
+
const w = kind === "vectorscope" ? height : width;
|
|
118
|
+
return /* @__PURE__ */ jsxs("figure", { className: "m-0 flex min-w-0 max-w-full flex-col gap-1", children: [
|
|
119
|
+
/* @__PURE__ */ jsx2("canvas", { ref: (el) => void (canvases.current[kind] = el), width: w, height, className: "rounded border border-border bg-black", style: { width: w, maxWidth: "100%", height: "auto", aspectRatio: `${w} / ${height}` }, "aria-label": LABELS[kind], "data-scope": kind }),
|
|
120
|
+
/* @__PURE__ */ jsx2("figcaption", { className: "text-[11px] text-muted-foreground", children: LABELS[kind] })
|
|
121
|
+
] }, kind);
|
|
122
|
+
}) }),
|
|
123
|
+
reading.ok ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", "data-scope-stats": "", children: [
|
|
124
|
+
/* @__PURE__ */ jsxs("div", { className: "tabular-nums", children: [
|
|
125
|
+
"Luma ",
|
|
126
|
+
reading.stats.luma.mean.toFixed(2),
|
|
127
|
+
" mean \xB7 ",
|
|
128
|
+
reading.stats.luma.p1.toFixed(2),
|
|
129
|
+
"\u2013",
|
|
130
|
+
reading.stats.luma.p99.toFixed(2),
|
|
131
|
+
" \xB7 crushed ",
|
|
132
|
+
pct(reading.stats.clipped.shadows),
|
|
133
|
+
" \xB7 blown ",
|
|
134
|
+
pct(reading.stats.clipped.highlights),
|
|
135
|
+
" \xB7 average",
|
|
136
|
+
" ",
|
|
137
|
+
/* @__PURE__ */ jsx2("span", { className: "inline-block h-2.5 w-2.5 rounded-sm align-middle", style: { background: reading.stats.average } }),
|
|
138
|
+
" ",
|
|
139
|
+
reading.stats.average,
|
|
140
|
+
!reading.complete && /* @__PURE__ */ jsx2("span", { className: "text-muted-foreground", children: " \xB7 loading" })
|
|
141
|
+
] }),
|
|
142
|
+
reading.notes.map((n) => /* @__PURE__ */ jsx2("div", { className: "text-amber-500", children: n }, n))
|
|
143
|
+
] }) : /* @__PURE__ */ jsx2("div", { className: "text-muted-foreground", "data-scope-message": "", children: reading.message })
|
|
144
|
+
] });
|
|
145
|
+
}
|
|
146
|
+
|
|
79
147
|
// src/video-editor/VideoTransport.tsx
|
|
80
148
|
import { formatVideoTimecode } from "@pixerate/editor/video-editor";
|
|
81
|
-
import { Fragment, jsx as
|
|
149
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
82
150
|
var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
83
151
|
function VideoTransport({ controller, editable = true, className = "" }) {
|
|
84
152
|
const state = useVideoEditorState(controller);
|
|
85
153
|
const { time, duration, playing, project, selection, readOnly } = state;
|
|
86
154
|
const canEdit = editable && !readOnly;
|
|
87
|
-
return /* @__PURE__ */
|
|
88
|
-
/* @__PURE__ */
|
|
89
|
-
/* @__PURE__ */
|
|
90
|
-
/* @__PURE__ */
|
|
91
|
-
/* @__PURE__ */
|
|
155
|
+
return /* @__PURE__ */ jsxs2("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
|
|
156
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
|
|
157
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", title: "Play / pause (Space)", disabled: duration <= 0, "data-play": "", children: playing ? "Pause" : "Play" }),
|
|
158
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
|
|
159
|
+
/* @__PURE__ */ jsxs2("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
|
|
92
160
|
formatVideoTimecode(time, project.fps),
|
|
93
161
|
" / ",
|
|
94
162
|
formatVideoTimecode(duration, project.fps)
|
|
95
163
|
] }),
|
|
96
|
-
editable && /* @__PURE__ */
|
|
97
|
-
/* @__PURE__ */
|
|
98
|
-
/* @__PURE__ */
|
|
99
|
-
/* @__PURE__ */
|
|
100
|
-
/* @__PURE__ */
|
|
164
|
+
editable && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
165
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-1 h-4 w-px bg-border" }),
|
|
166
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
|
|
167
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
|
|
168
|
+
/* @__PURE__ */ jsx3(
|
|
101
169
|
"button",
|
|
102
170
|
{
|
|
103
171
|
type: "button",
|
|
@@ -109,14 +177,14 @@ function VideoTransport({ controller, editable = true, className = "" }) {
|
|
|
109
177
|
children: "Ripple delete"
|
|
110
178
|
}
|
|
111
179
|
),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.undo(), disabled: !canEdit || !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
|
|
181
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.redo(), disabled: !canEdit || !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" })
|
|
114
182
|
] })
|
|
115
183
|
] });
|
|
116
184
|
}
|
|
117
185
|
|
|
118
186
|
// src/video-editor/VideoTimeline.tsx
|
|
119
|
-
import { useEffect as
|
|
187
|
+
import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
120
188
|
import {
|
|
121
189
|
planVideoClipDrag,
|
|
122
190
|
videoDragLabel,
|
|
@@ -129,7 +197,7 @@ import {
|
|
|
129
197
|
videoRulerTicks,
|
|
130
198
|
formatVideoSeconds
|
|
131
199
|
} from "@pixerate/editor/video-editor";
|
|
132
|
-
import { Fragment as Fragment2, jsx as
|
|
200
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
133
201
|
var HEADER = 150;
|
|
134
202
|
var RULER = 24;
|
|
135
203
|
var ROW = 44;
|
|
@@ -143,25 +211,25 @@ function ClipWaveform({ controller, clip, asset, width, height }) {
|
|
|
143
211
|
() => controller.audioPeaks.peek(asset),
|
|
144
212
|
() => void 0
|
|
145
213
|
);
|
|
146
|
-
|
|
214
|
+
useEffect4(() => {
|
|
147
215
|
controller.audioPeaks.load(asset).catch(() => {
|
|
148
216
|
});
|
|
149
217
|
}, [controller, asset]);
|
|
150
218
|
const d = useMemo2(() => peaks ? videoClipWaveformPath(clip, peaks, width, height) : "", [peaks, clip, width, height]);
|
|
151
219
|
if (!d) return null;
|
|
152
|
-
return /* @__PURE__ */
|
|
220
|
+
return /* @__PURE__ */ jsx4("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ jsx4("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
|
|
153
221
|
}
|
|
154
222
|
var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
155
223
|
function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
|
|
156
224
|
const state = useVideoEditorState(controller);
|
|
157
225
|
const { project, duration, time, selection, readOnly, error } = state;
|
|
158
|
-
const [ownZoom, setOwnZoom] =
|
|
226
|
+
const [ownZoom, setOwnZoom] = useState2(pxPerSecond ?? 100);
|
|
159
227
|
const px = onPxPerSecondChange ? pxPerSecond ?? 100 : ownZoom;
|
|
160
228
|
const canEdit = editable && !readOnly;
|
|
161
|
-
const [drag, setDrag] =
|
|
162
|
-
const dragRef =
|
|
163
|
-
const rows =
|
|
164
|
-
const ruler =
|
|
229
|
+
const [drag, setDrag] = useState2(null);
|
|
230
|
+
const dragRef = useRef4(null);
|
|
231
|
+
const rows = useRef4(/* @__PURE__ */ new Map());
|
|
232
|
+
const ruler = useRef4(null);
|
|
165
233
|
const assets = videoAssetMap(project);
|
|
166
234
|
const msToPx = (ms) => ms * px / 1e3;
|
|
167
235
|
const span = Math.max(duration + 5e3, 1e4);
|
|
@@ -192,10 +260,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
192
260
|
dragRef.current = next;
|
|
193
261
|
setDrag(next);
|
|
194
262
|
};
|
|
195
|
-
const live =
|
|
263
|
+
const live = useRef4({ planFor, selection });
|
|
196
264
|
live.current = { planFor, selection };
|
|
197
|
-
const stopDrag =
|
|
198
|
-
|
|
265
|
+
const stopDrag = useRef4(null);
|
|
266
|
+
useEffect4(() => () => stopDrag.current?.(), []);
|
|
199
267
|
const onClipPointerDown = (e, clip, track) => {
|
|
200
268
|
if (e.button !== void 0 && e.button !== 0) return;
|
|
201
269
|
e.stopPropagation();
|
|
@@ -273,7 +341,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
273
341
|
}).filter((x) => x !== null)
|
|
274
342
|
)
|
|
275
343
|
}));
|
|
276
|
-
const toggle = (track, field2, label) => /* @__PURE__ */
|
|
344
|
+
const toggle = (track, field2, label) => /* @__PURE__ */ jsx4(
|
|
277
345
|
"button",
|
|
278
346
|
{
|
|
279
347
|
type: "button",
|
|
@@ -287,12 +355,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
287
355
|
children: field2 === "muted" ? "M" : field2 === "hidden" ? "H" : "L"
|
|
288
356
|
}
|
|
289
357
|
);
|
|
290
|
-
return /* @__PURE__ */
|
|
291
|
-
!onPxPerSecondChange && /* @__PURE__ */
|
|
358
|
+
return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
|
|
359
|
+
!onPxPerSecondChange && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
|
|
292
360
|
"Zoom",
|
|
293
|
-
/* @__PURE__ */
|
|
361
|
+
/* @__PURE__ */ jsx4("input", { type: "range", min: MIN_ZOOM, max: MAX_ZOOM, step: 5, value: ownZoom, "aria-label": "Timeline zoom", onChange: (e) => setOwnZoom(Number(e.target.value)), "data-zoom": "" })
|
|
294
362
|
] }),
|
|
295
|
-
/* @__PURE__ */
|
|
363
|
+
/* @__PURE__ */ jsx4(
|
|
296
364
|
"div",
|
|
297
365
|
{
|
|
298
366
|
className: "relative overflow-x-auto rounded border border-border outline-none focus-visible:ring-2 focus-visible:ring-rose-500/50",
|
|
@@ -301,10 +369,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
301
369
|
"aria-label": "Video timeline",
|
|
302
370
|
onKeyDown,
|
|
303
371
|
"data-video-timeline": "",
|
|
304
|
-
children: /* @__PURE__ */
|
|
305
|
-
/* @__PURE__ */
|
|
306
|
-
/* @__PURE__ */
|
|
307
|
-
/* @__PURE__ */
|
|
372
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "relative", style: { width: HEADER + width }, children: [
|
|
373
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex", style: { height: RULER }, children: [
|
|
374
|
+
/* @__PURE__ */ jsx4("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
|
|
375
|
+
/* @__PURE__ */ jsxs3(
|
|
308
376
|
"div",
|
|
309
377
|
{
|
|
310
378
|
ref: ruler,
|
|
@@ -327,8 +395,8 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
327
395
|
if (e.buttons & 1) seekAt(e.clientX);
|
|
328
396
|
},
|
|
329
397
|
children: [
|
|
330
|
-
ticks.map((t) => /* @__PURE__ */
|
|
331
|
-
(project.markers ?? []).map((m) => /* @__PURE__ */
|
|
398
|
+
ticks.map((t) => /* @__PURE__ */ jsx4("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: t.x }, "data-tick": t.time, children: t.label && /* @__PURE__ */ jsx4("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
|
|
399
|
+
(project.markers ?? []).map((m) => /* @__PURE__ */ jsx4(
|
|
332
400
|
"div",
|
|
333
401
|
{
|
|
334
402
|
className: "absolute top-0 h-2.5 w-2.5 -translate-x-1/2 rotate-45",
|
|
@@ -342,19 +410,19 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
342
410
|
}
|
|
343
411
|
)
|
|
344
412
|
] }),
|
|
345
|
-
placed.map(({ track, clips }) => /* @__PURE__ */
|
|
346
|
-
/* @__PURE__ */
|
|
347
|
-
/* @__PURE__ */
|
|
348
|
-
/* @__PURE__ */
|
|
349
|
-
/* @__PURE__ */
|
|
413
|
+
placed.map(({ track, clips }) => /* @__PURE__ */ jsxs3("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
|
|
414
|
+
/* @__PURE__ */ jsxs3("div", { className: "sticky left-0 z-20 flex shrink-0 flex-col justify-center gap-0.5 border-r border-border bg-background px-2 text-xs", style: { width: HEADER }, "data-track-header": "", children: [
|
|
415
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
|
|
416
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate font-medium", children: track.name ?? track.id }),
|
|
417
|
+
/* @__PURE__ */ jsx4("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
|
|
350
418
|
] }),
|
|
351
|
-
editable && /* @__PURE__ */
|
|
419
|
+
editable && /* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
|
|
352
420
|
toggle(track, "muted", "Mute"),
|
|
353
421
|
track.kind === "video" && toggle(track, "hidden", "Hide"),
|
|
354
422
|
toggle(track, "locked", "Lock")
|
|
355
423
|
] })
|
|
356
424
|
] }),
|
|
357
|
-
/* @__PURE__ */
|
|
425
|
+
/* @__PURE__ */ jsx4(
|
|
358
426
|
"div",
|
|
359
427
|
{
|
|
360
428
|
ref: (el) => {
|
|
@@ -371,7 +439,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
371
439
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
372
440
|
const selected = selection.includes(clip.id);
|
|
373
441
|
const label = videoClipLabel(clip, asset);
|
|
374
|
-
return /* @__PURE__ */
|
|
442
|
+
return /* @__PURE__ */ jsxs3(
|
|
375
443
|
"div",
|
|
376
444
|
{
|
|
377
445
|
className: `absolute top-1 bottom-1 select-none touch-none overflow-hidden rounded text-[11px] text-white ${canEdit && !home.locked ? "cursor-grab" : "cursor-default"} ${dragging ? "opacity-80 shadow-lg" : ""}`,
|
|
@@ -390,15 +458,15 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
390
458
|
"aria-selected": selected,
|
|
391
459
|
onPointerDown: (e) => onClipPointerDown(e, clip, home),
|
|
392
460
|
children: [
|
|
393
|
-
videoClipShowsWaveform(clip, asset, waveforms) && /* @__PURE__ */
|
|
394
|
-
/* @__PURE__ */
|
|
395
|
-
clip.transition && /* @__PURE__ */
|
|
461
|
+
videoClipShowsWaveform(clip, asset, waveforms) && /* @__PURE__ */ jsx4(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
|
|
462
|
+
/* @__PURE__ */ jsx4("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
|
|
463
|
+
clip.transition && /* @__PURE__ */ jsxs3("span", { className: "pointer-events-none absolute left-1 bottom-0.5 rounded bg-black/40 px-1 text-[9px] leading-3", title: `${clip.transition.type} ${clip.transition.duration} ms`, "data-transition": clip.transition.type, children: [
|
|
396
464
|
"\u29D3 ",
|
|
397
465
|
clip.transition.type
|
|
398
466
|
] }),
|
|
399
|
-
canEdit && !home.locked && /* @__PURE__ */
|
|
400
|
-
/* @__PURE__ */
|
|
401
|
-
/* @__PURE__ */
|
|
467
|
+
canEdit && !home.locked && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
468
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute inset-y-0 left-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "start", "aria-hidden": "true" }),
|
|
469
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute inset-y-0 right-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "end", "aria-hidden": "true" })
|
|
402
470
|
] })
|
|
403
471
|
]
|
|
404
472
|
},
|
|
@@ -408,15 +476,16 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
408
476
|
}
|
|
409
477
|
)
|
|
410
478
|
] }, track.id)),
|
|
411
|
-
/* @__PURE__ */
|
|
479
|
+
/* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute top-0 bottom-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: HEADER + msToPx(time), zIndex: 15 }, "data-playhead": "" })
|
|
412
480
|
] })
|
|
413
481
|
}
|
|
414
482
|
),
|
|
415
|
-
error && /* @__PURE__ */
|
|
483
|
+
error && /* @__PURE__ */ jsx4("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
|
|
416
484
|
] });
|
|
417
485
|
}
|
|
418
486
|
|
|
419
487
|
// src/video-editor/ClipInspector.tsx
|
|
488
|
+
import { useState as useState3 } from "react";
|
|
420
489
|
import {
|
|
421
490
|
findVideoClip,
|
|
422
491
|
formatVideoSeconds as formatVideoSeconds2,
|
|
@@ -425,7 +494,8 @@ import {
|
|
|
425
494
|
normalizeVideoGrade,
|
|
426
495
|
VIDEO_GRADE_CONTROLS,
|
|
427
496
|
VIDEO_GRADE_PRESET_NAMES,
|
|
428
|
-
|
|
497
|
+
videoGradeLook,
|
|
498
|
+
videoLutEdits,
|
|
429
499
|
VIDEO_BLEND_MODE_LABELS,
|
|
430
500
|
VIDEO_BLEND_MODES,
|
|
431
501
|
VIDEO_FADE_CURVE_LABELS,
|
|
@@ -442,7 +512,7 @@ import {
|
|
|
442
512
|
videoKeyframeToggleEdit,
|
|
443
513
|
videoKeyframeValueEdit
|
|
444
514
|
} from "@pixerate/editor/video-editor";
|
|
445
|
-
import { Fragment as Fragment3, jsx as
|
|
515
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
446
516
|
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
447
517
|
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
448
518
|
var DEFAULT_TRANSITION = 500;
|
|
@@ -456,7 +526,7 @@ function Commit({
|
|
|
456
526
|
if (raw !== value) onCommit(raw);
|
|
457
527
|
};
|
|
458
528
|
if (multiline)
|
|
459
|
-
return /* @__PURE__ */
|
|
529
|
+
return /* @__PURE__ */ jsx5(
|
|
460
530
|
"textarea",
|
|
461
531
|
{
|
|
462
532
|
className: field,
|
|
@@ -468,11 +538,64 @@ function Commit({
|
|
|
468
538
|
},
|
|
469
539
|
value
|
|
470
540
|
);
|
|
471
|
-
return /* @__PURE__ */
|
|
541
|
+
return /* @__PURE__ */ jsx5("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
|
|
542
|
+
}
|
|
543
|
+
function LutControls({ controller, clip, disabled }) {
|
|
544
|
+
const [message, setMessage] = useState3(null);
|
|
545
|
+
const lut = clip.grade?.lut;
|
|
546
|
+
const apply = (setting, label) => {
|
|
547
|
+
const r = controller.apply(videoLutEdits(controller.getProject(), [clip.id], setting), label);
|
|
548
|
+
setMessage(r.ok ? null : r.error);
|
|
549
|
+
};
|
|
550
|
+
const choose = async (src) => {
|
|
551
|
+
if (!src) return lut && apply(null, "Remove LUT");
|
|
552
|
+
try {
|
|
553
|
+
const table = await controller.luts.load(src);
|
|
554
|
+
apply({ src, ...lut?.strength !== void 0 ? { strength: lut.strength } : {}, ...table.title ? { name: table.title } : {} }, "Apply LUT");
|
|
555
|
+
} catch (e) {
|
|
556
|
+
setMessage(e instanceof Error ? e.message : String(e));
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const readFile = async (file) => {
|
|
560
|
+
if (!file) return;
|
|
561
|
+
try {
|
|
562
|
+
const table = controller.luts.set(file.name, await file.text());
|
|
563
|
+
apply({ src: file.name, ...lut?.strength !== void 0 ? { strength: lut.strength } : {}, ...table.title ? { name: table.title } : {} }, "Apply LUT");
|
|
564
|
+
} catch (e) {
|
|
565
|
+
setMessage(`${file.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
569
|
+
/* @__PURE__ */ jsx5(Row, { label: "LUT", name: "grade.lut", children: /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 gap-1", children: [
|
|
570
|
+
/* @__PURE__ */ jsx5(Commit, { value: lut?.src ?? "", placeholder: "look.cube", "aria-label": "LUT file", disabled, onCommit: (raw) => void choose(raw.trim()) }),
|
|
571
|
+
/* @__PURE__ */ jsxs4("label", { className: `${btn2} cursor-pointer whitespace-nowrap`, title: "Load a .cube file", children: [
|
|
572
|
+
"Load",
|
|
573
|
+
/* @__PURE__ */ jsx5("input", { type: "file", accept: ".cube", className: "sr-only", disabled, "aria-label": "Load a .cube LUT", onChange: (e) => void readFile(e.currentTarget.files?.[0]) })
|
|
574
|
+
] }),
|
|
575
|
+
lut && /* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, "aria-label": "Remove LUT", onClick: () => apply(null, "Remove LUT"), children: "\u2715" })
|
|
576
|
+
] }) }),
|
|
577
|
+
lut && /* @__PURE__ */ jsx5(Row, { label: "LUT strength", name: "grade.lut.strength", children: /* @__PURE__ */ jsx5(
|
|
578
|
+
Commit,
|
|
579
|
+
{
|
|
580
|
+
type: "number",
|
|
581
|
+
min: 0,
|
|
582
|
+
max: 1,
|
|
583
|
+
step: 0.05,
|
|
584
|
+
value: String(lut.strength ?? 1),
|
|
585
|
+
"aria-label": "LUT strength",
|
|
586
|
+
disabled,
|
|
587
|
+
onCommit: (raw) => {
|
|
588
|
+
const n = Number(raw);
|
|
589
|
+
if (raw.trim() && Number.isFinite(n)) apply(n > 0 ? { ...lut, strength: Math.min(1, n) } : null, n > 0 ? "Change LUT strength" : "Remove LUT");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
) }),
|
|
593
|
+
message && /* @__PURE__ */ jsx5("p", { className: "text-xs text-red-500", role: "alert", "data-lut-error": "", children: message })
|
|
594
|
+
] });
|
|
472
595
|
}
|
|
473
596
|
function Row({ label, name, children }) {
|
|
474
|
-
return /* @__PURE__ */
|
|
475
|
-
/* @__PURE__ */
|
|
597
|
+
return /* @__PURE__ */ jsxs4("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
|
|
598
|
+
/* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: label }),
|
|
476
599
|
children
|
|
477
600
|
] });
|
|
478
601
|
}
|
|
@@ -480,7 +603,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
480
603
|
const state = useVideoEditorState(controller);
|
|
481
604
|
const { project, selection, readOnly, time } = state;
|
|
482
605
|
if (selection.length !== 1)
|
|
483
|
-
return /* @__PURE__ */
|
|
606
|
+
return /* @__PURE__ */ jsx5("p", { className: `text-xs text-muted-foreground ${className}`, "data-clip-inspector": "", children: selection.length ? `${selection.length} clips selected. Select one clip to edit it.` : "Select a clip on the timeline to edit it." });
|
|
484
607
|
const at = findVideoClip(project, selection[0]);
|
|
485
608
|
if (!at) return null;
|
|
486
609
|
const clip = at.clip;
|
|
@@ -497,24 +620,24 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
497
620
|
};
|
|
498
621
|
const text = (key, label) => {
|
|
499
622
|
const value = clip[key];
|
|
500
|
-
return /* @__PURE__ */
|
|
623
|
+
return /* @__PURE__ */ jsx5(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
|
|
501
624
|
};
|
|
502
625
|
const number = (key, label, attrs = {}) => {
|
|
503
626
|
const value = clip[key];
|
|
504
|
-
return /* @__PURE__ */
|
|
627
|
+
return /* @__PURE__ */ jsx5(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
|
|
505
628
|
};
|
|
506
629
|
const transition = clip.transition;
|
|
507
|
-
return /* @__PURE__ */
|
|
508
|
-
/* @__PURE__ */
|
|
509
|
-
/* @__PURE__ */
|
|
510
|
-
/* @__PURE__ */
|
|
511
|
-
at.track.locked && /* @__PURE__ */
|
|
630
|
+
return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
|
|
631
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
632
|
+
/* @__PURE__ */ jsx5("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
|
|
633
|
+
/* @__PURE__ */ jsx5("code", { className: "text-muted-foreground", children: clip.id }),
|
|
634
|
+
at.track.locked && /* @__PURE__ */ jsx5("span", { className: "text-amber-500", children: "locked track" })
|
|
512
635
|
] }),
|
|
513
|
-
/* @__PURE__ */
|
|
514
|
-
/* @__PURE__ */
|
|
515
|
-
/* @__PURE__ */
|
|
516
|
-
videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */
|
|
517
|
-
/* @__PURE__ */
|
|
636
|
+
/* @__PURE__ */ jsx5(Row, { label: "Name", name: "name", children: text("name", "Name") }),
|
|
637
|
+
/* @__PURE__ */ jsx5(Row, { label: "Start", name: "start", children: /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.start) }) }),
|
|
638
|
+
/* @__PURE__ */ jsx5(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.duration) }) }),
|
|
639
|
+
videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */ jsx5(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
|
|
640
|
+
/* @__PURE__ */ jsx5(
|
|
518
641
|
Commit,
|
|
519
642
|
{
|
|
520
643
|
type: "number",
|
|
@@ -533,7 +656,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
533
656
|
}
|
|
534
657
|
}
|
|
535
658
|
),
|
|
536
|
-
/* @__PURE__ */
|
|
659
|
+
/* @__PURE__ */ jsx5(
|
|
537
660
|
"button",
|
|
538
661
|
{
|
|
539
662
|
type: "button",
|
|
@@ -550,11 +673,11 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
550
673
|
children: row.onKey ? "\u25C6" : "\u25C7"
|
|
551
674
|
}
|
|
552
675
|
),
|
|
553
|
-
row.keyed && /* @__PURE__ */
|
|
676
|
+
row.keyed && /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
|
|
554
677
|
] }) }, row.property)),
|
|
555
|
-
at.track.kind === "video" && /* @__PURE__ */
|
|
556
|
-
at.track.kind === "video" && /* @__PURE__ */
|
|
557
|
-
/* @__PURE__ */
|
|
678
|
+
at.track.kind === "video" && /* @__PURE__ */ jsx5(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ jsx5("select", { className: field, value: clip.blend ?? "normal", disabled, "aria-label": "Blend mode", onChange: (e) => set("blend", e.target.value === "normal" ? void 0 : e.target.value, "Change blend mode"), children: VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ jsx5("option", { value: m, children: VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
|
|
679
|
+
at.track.kind === "video" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
680
|
+
/* @__PURE__ */ jsx5(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ jsxs4(
|
|
558
681
|
"select",
|
|
559
682
|
{
|
|
560
683
|
className: field,
|
|
@@ -562,16 +685,16 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
562
685
|
disabled,
|
|
563
686
|
"aria-label": "Apply a look",
|
|
564
687
|
onChange: (e) => {
|
|
565
|
-
const grade =
|
|
688
|
+
const grade = videoGradeLook(clip.grade, e.target.value);
|
|
566
689
|
if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
|
|
567
690
|
},
|
|
568
691
|
children: [
|
|
569
|
-
/* @__PURE__ */
|
|
570
|
-
VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */
|
|
692
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: clip.grade ? "Graded" : "None" }),
|
|
693
|
+
VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ jsx5("option", { value: name, children: name.replace(/-/g, " ") }, name))
|
|
571
694
|
]
|
|
572
695
|
}
|
|
573
696
|
) }),
|
|
574
|
-
VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */
|
|
697
|
+
VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ jsx5(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ jsx5(
|
|
575
698
|
Commit,
|
|
576
699
|
{
|
|
577
700
|
type: "number",
|
|
@@ -589,9 +712,10 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
589
712
|
set("grade", grade, `Change ${c.label.toLowerCase()}`);
|
|
590
713
|
}
|
|
591
714
|
}
|
|
592
|
-
) }, c.key))
|
|
715
|
+
) }, c.key)),
|
|
716
|
+
/* @__PURE__ */ jsx5(LutControls, { controller, clip, disabled })
|
|
593
717
|
] }),
|
|
594
|
-
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */
|
|
718
|
+
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ jsx5(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ jsxs4(
|
|
595
719
|
"select",
|
|
596
720
|
{
|
|
597
721
|
className: field,
|
|
@@ -603,20 +727,20 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
603
727
|
set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
|
|
604
728
|
},
|
|
605
729
|
children: [
|
|
606
|
-
/* @__PURE__ */
|
|
607
|
-
VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */
|
|
730
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
|
|
731
|
+
VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ jsx5("option", { value: effect, children: VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
|
|
608
732
|
]
|
|
609
733
|
}
|
|
610
734
|
) }, key)),
|
|
611
|
-
/* @__PURE__ */
|
|
612
|
-
/* @__PURE__ */
|
|
613
|
-
clip.type === "media" && /* @__PURE__ */
|
|
614
|
-
/* @__PURE__ */
|
|
615
|
-
/* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
735
|
+
/* @__PURE__ */ jsx5(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
736
|
+
/* @__PURE__ */ jsx5(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
737
|
+
clip.type === "media" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
738
|
+
/* @__PURE__ */ jsx5(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
|
|
739
|
+
/* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: "ramped" }),
|
|
740
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, onClick: () => controller.apply({ op: "set_time_remap", clip: clip.id, keys: null, ripple: true }, "Remove speed ramp"), children: "Remove ramp" })
|
|
617
741
|
] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
|
|
618
|
-
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */
|
|
619
|
-
/* @__PURE__ */
|
|
742
|
+
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ jsx5(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ jsxs4("span", { className: "flex flex-wrap gap-1", children: [
|
|
743
|
+
/* @__PURE__ */ jsx5(
|
|
620
744
|
"button",
|
|
621
745
|
{
|
|
622
746
|
type: "button",
|
|
@@ -627,11 +751,11 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
627
751
|
children: "Freeze 1 s here"
|
|
628
752
|
}
|
|
629
753
|
),
|
|
630
|
-
/* @__PURE__ */
|
|
754
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply(videoSpeedRampEdit(clip, videoSlowMotionRamp(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
|
|
631
755
|
] }) }),
|
|
632
|
-
/* @__PURE__ */
|
|
756
|
+
/* @__PURE__ */ jsx5(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ jsx5("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) }),
|
|
633
757
|
["fadeInCurve", "fadeOutCurve"].map(
|
|
634
|
-
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */
|
|
758
|
+
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ jsx5(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ jsx5(
|
|
635
759
|
"select",
|
|
636
760
|
{
|
|
637
761
|
className: field,
|
|
@@ -642,19 +766,19 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
642
766
|
const curve = e.target.value;
|
|
643
767
|
set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
|
|
644
768
|
},
|
|
645
|
-
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */
|
|
769
|
+
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx5("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
646
770
|
}
|
|
647
771
|
) }, key) : null
|
|
648
772
|
)
|
|
649
773
|
] }),
|
|
650
|
-
clip.type === "text" && /* @__PURE__ */
|
|
651
|
-
/* @__PURE__ */
|
|
652
|
-
/* @__PURE__ */
|
|
653
|
-
/* @__PURE__ */
|
|
654
|
-
/* @__PURE__ */
|
|
774
|
+
clip.type === "text" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
775
|
+
/* @__PURE__ */ jsx5(Row, { label: "Text", name: "text", children: /* @__PURE__ */ jsx5(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
|
|
776
|
+
/* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
|
|
777
|
+
/* @__PURE__ */ jsx5(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
|
|
778
|
+
/* @__PURE__ */ jsx5(Row, { label: "Background", name: "background", children: text("background", "Background") })
|
|
655
779
|
] }),
|
|
656
|
-
clip.type === "color" && /* @__PURE__ */
|
|
657
|
-
/* @__PURE__ */
|
|
780
|
+
clip.type === "color" && /* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ jsx5(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
|
|
781
|
+
/* @__PURE__ */ jsx5(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ jsxs4(
|
|
658
782
|
"select",
|
|
659
783
|
{
|
|
660
784
|
className: field,
|
|
@@ -669,12 +793,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
669
793
|
);
|
|
670
794
|
},
|
|
671
795
|
children: [
|
|
672
|
-
/* @__PURE__ */
|
|
673
|
-
VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */
|
|
796
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
|
|
797
|
+
VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: VIDEO_TRANSITION_LABELS[t] }, t))
|
|
674
798
|
]
|
|
675
799
|
}
|
|
676
800
|
) }),
|
|
677
|
-
transition && /* @__PURE__ */
|
|
801
|
+
transition && /* @__PURE__ */ jsx5(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ jsx5(
|
|
678
802
|
Commit,
|
|
679
803
|
{
|
|
680
804
|
type: "number",
|
|
@@ -689,7 +813,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
689
813
|
}
|
|
690
814
|
}
|
|
691
815
|
) }),
|
|
692
|
-
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */
|
|
816
|
+
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */ jsx5(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ jsx5(
|
|
693
817
|
"select",
|
|
694
818
|
{
|
|
695
819
|
className: field,
|
|
@@ -697,10 +821,10 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
697
821
|
disabled,
|
|
698
822
|
"aria-label": "Transition direction",
|
|
699
823
|
onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
|
|
700
|
-
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */
|
|
824
|
+
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ jsx5("option", { value: d, children: d }, d))
|
|
701
825
|
}
|
|
702
826
|
) }),
|
|
703
|
-
transition && /* @__PURE__ */
|
|
827
|
+
transition && /* @__PURE__ */ jsx5(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ jsx5(
|
|
704
828
|
"select",
|
|
705
829
|
{
|
|
706
830
|
className: field,
|
|
@@ -713,30 +837,30 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
713
837
|
const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve };
|
|
714
838
|
controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
|
|
715
839
|
},
|
|
716
|
-
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */
|
|
840
|
+
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx5("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
717
841
|
}
|
|
718
842
|
) })
|
|
719
843
|
] });
|
|
720
844
|
}
|
|
721
845
|
|
|
722
846
|
// src/video-editor/MediaBin.tsx
|
|
723
|
-
import { useState as
|
|
847
|
+
import { useState as useState4 } from "react";
|
|
724
848
|
import {
|
|
725
849
|
formatVideoSeconds as formatVideoSeconds3,
|
|
726
850
|
probeVideoMedia,
|
|
727
851
|
videoAssetDropEdit,
|
|
728
852
|
videoAssetKindOf
|
|
729
853
|
} from "@pixerate/editor/video-editor";
|
|
730
|
-
import { Fragment as Fragment4, jsx as
|
|
854
|
+
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
731
855
|
var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
732
856
|
function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMedia, className = "" }) {
|
|
733
857
|
const { project, readOnly } = useVideoEditorState(controller);
|
|
734
858
|
const canEdit = editable && !readOnly;
|
|
735
|
-
const [url, setUrl] =
|
|
736
|
-
const [urlKind, setUrlKind] =
|
|
737
|
-
const [busy, setBusy] =
|
|
738
|
-
const [problem, setProblem] =
|
|
739
|
-
const [over, setOver] =
|
|
859
|
+
const [url, setUrl] = useState4("");
|
|
860
|
+
const [urlKind, setUrlKind] = useState4("");
|
|
861
|
+
const [busy, setBusy] = useState4(0);
|
|
862
|
+
const [problem, setProblem] = useState4(null);
|
|
863
|
+
const [over, setOver] = useState4(false);
|
|
740
864
|
const addSource = async (src, kind, name) => {
|
|
741
865
|
setBusy((n) => n + 1);
|
|
742
866
|
try {
|
|
@@ -784,7 +908,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
784
908
|
if (added) controller.select([added]);
|
|
785
909
|
}
|
|
786
910
|
};
|
|
787
|
-
return /* @__PURE__ */
|
|
911
|
+
return /* @__PURE__ */ jsxs5(
|
|
788
912
|
"div",
|
|
789
913
|
{
|
|
790
914
|
className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
|
|
@@ -802,20 +926,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
802
926
|
void addFiles(e.dataTransfer.files);
|
|
803
927
|
},
|
|
804
928
|
children: [
|
|
805
|
-
/* @__PURE__ */
|
|
806
|
-
project.assets.length ? /* @__PURE__ */
|
|
807
|
-
/* @__PURE__ */
|
|
808
|
-
/* @__PURE__ */
|
|
809
|
-
asset.duration !== void 0 && /* @__PURE__ */
|
|
810
|
-
editable && /* @__PURE__ */
|
|
811
|
-
] }, asset.id)) }) : /* @__PURE__ */
|
|
929
|
+
/* @__PURE__ */ jsx6("div", { className: "text-xs font-medium", children: "Media" }),
|
|
930
|
+
project.assets.length ? /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
|
|
931
|
+
/* @__PURE__ */ jsx6("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
|
|
932
|
+
/* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
|
|
933
|
+
asset.duration !== void 0 && /* @__PURE__ */ jsx6("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
|
|
934
|
+
editable && /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
|
|
935
|
+
] }, asset.id)) }) : /* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted-foreground", children: [
|
|
812
936
|
"No media yet.",
|
|
813
937
|
editable ? " Choose or drop files, or add a URL." : ""
|
|
814
938
|
] }),
|
|
815
|
-
editable && /* @__PURE__ */
|
|
816
|
-
/* @__PURE__ */
|
|
939
|
+
editable && /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
940
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
|
|
817
941
|
"Choose or drop video, audio or image files",
|
|
818
|
-
/* @__PURE__ */
|
|
942
|
+
/* @__PURE__ */ jsx6(
|
|
819
943
|
"input",
|
|
820
944
|
{
|
|
821
945
|
type: "file",
|
|
@@ -832,7 +956,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
832
956
|
}
|
|
833
957
|
)
|
|
834
958
|
] }),
|
|
835
|
-
/* @__PURE__ */
|
|
959
|
+
/* @__PURE__ */ jsxs5(
|
|
836
960
|
"form",
|
|
837
961
|
{
|
|
838
962
|
className: "flex items-center gap-1",
|
|
@@ -841,7 +965,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
841
965
|
void addUrl();
|
|
842
966
|
},
|
|
843
967
|
children: [
|
|
844
|
-
/* @__PURE__ */
|
|
968
|
+
/* @__PURE__ */ jsx6(
|
|
845
969
|
"input",
|
|
846
970
|
{
|
|
847
971
|
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
@@ -852,26 +976,26 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
852
976
|
onChange: (e) => setUrl(e.target.value)
|
|
853
977
|
}
|
|
854
978
|
),
|
|
855
|
-
/* @__PURE__ */
|
|
856
|
-
/* @__PURE__ */
|
|
857
|
-
/* @__PURE__ */
|
|
858
|
-
/* @__PURE__ */
|
|
859
|
-
/* @__PURE__ */
|
|
979
|
+
/* @__PURE__ */ jsxs5("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", value: urlKind, disabled: !canEdit, "aria-label": "Media kind", onChange: (e) => setUrlKind(e.target.value), children: [
|
|
980
|
+
/* @__PURE__ */ jsx6("option", { value: "", children: "Auto" }),
|
|
981
|
+
/* @__PURE__ */ jsx6("option", { value: "video", children: "Video" }),
|
|
982
|
+
/* @__PURE__ */ jsx6("option", { value: "audio", children: "Audio" }),
|
|
983
|
+
/* @__PURE__ */ jsx6("option", { value: "image", children: "Image" })
|
|
860
984
|
] }),
|
|
861
|
-
/* @__PURE__ */
|
|
985
|
+
/* @__PURE__ */ jsx6("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
862
986
|
]
|
|
863
987
|
}
|
|
864
988
|
)
|
|
865
989
|
] }),
|
|
866
|
-
busy > 0 && /* @__PURE__ */
|
|
867
|
-
problem && /* @__PURE__ */
|
|
990
|
+
busy > 0 && /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
|
|
991
|
+
problem && /* @__PURE__ */ jsx6("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
|
|
868
992
|
]
|
|
869
993
|
}
|
|
870
994
|
);
|
|
871
995
|
}
|
|
872
996
|
|
|
873
997
|
// src/video-editor/TranscriptPanel.tsx
|
|
874
|
-
import
|
|
998
|
+
import React6, { useMemo as useMemo3, useState as useState5 } from "react";
|
|
875
999
|
import {
|
|
876
1000
|
findVideoFillers,
|
|
877
1001
|
findVideoTranscriptPauses,
|
|
@@ -881,15 +1005,15 @@ import {
|
|
|
881
1005
|
videoWordAt,
|
|
882
1006
|
videoWordsRange
|
|
883
1007
|
} from "@pixerate/editor/video-editor";
|
|
884
|
-
import { Fragment as Fragment5, jsx as
|
|
1008
|
+
import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
885
1009
|
var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
886
1010
|
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
887
1011
|
const state = useVideoEditorState(controller);
|
|
888
1012
|
const { project, time, readOnly } = state;
|
|
889
1013
|
const canEdit = editable && !readOnly;
|
|
890
|
-
const [anchor, setAnchor] =
|
|
891
|
-
const [focus, setFocus] =
|
|
892
|
-
const [message, setMessage] =
|
|
1014
|
+
const [anchor, setAnchor] = useState5(null);
|
|
1015
|
+
const [focus, setFocus] = useState5(null);
|
|
1016
|
+
const [message, setMessage] = useState5(null);
|
|
893
1017
|
const words = useMemo3(() => {
|
|
894
1018
|
try {
|
|
895
1019
|
return videoTimelineTranscript(project, { track });
|
|
@@ -916,9 +1040,9 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
916
1040
|
setMessage(e instanceof Error ? e.message : String(e));
|
|
917
1041
|
}
|
|
918
1042
|
};
|
|
919
|
-
return /* @__PURE__ */
|
|
920
|
-
words.length > 0 ? /* @__PURE__ */
|
|
921
|
-
/* @__PURE__ */
|
|
1043
|
+
return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
|
|
1044
|
+
words.length > 0 ? /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1045
|
+
/* @__PURE__ */ jsx7(
|
|
922
1046
|
"div",
|
|
923
1047
|
{
|
|
924
1048
|
className: "max-h-48 overflow-y-auto leading-6",
|
|
@@ -934,8 +1058,8 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
934
1058
|
},
|
|
935
1059
|
children: words.map((w, i) => {
|
|
936
1060
|
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
937
|
-
return /* @__PURE__ */
|
|
938
|
-
/* @__PURE__ */
|
|
1061
|
+
return /* @__PURE__ */ jsxs6(React6.Fragment, { children: [
|
|
1062
|
+
/* @__PURE__ */ jsx7(
|
|
939
1063
|
"span",
|
|
940
1064
|
{
|
|
941
1065
|
role: "option",
|
|
@@ -961,51 +1085,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
961
1085
|
})
|
|
962
1086
|
}
|
|
963
1087
|
),
|
|
964
|
-
canEdit && /* @__PURE__ */
|
|
965
|
-
/* @__PURE__ */
|
|
966
|
-
/* @__PURE__ */
|
|
967
|
-
/* @__PURE__ */
|
|
1088
|
+
canEdit && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-1", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
|
|
1090
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: btn4, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
|
|
1091
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: btn4, onClick: () => cut(findVideoTranscriptPauses(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
|
|
968
1092
|
] })
|
|
969
|
-
] }) : /* @__PURE__ */
|
|
970
|
-
canEdit && sound.length > 0 && /* @__PURE__ */
|
|
971
|
-
/* @__PURE__ */
|
|
972
|
-
/* @__PURE__ */
|
|
973
|
-
/* @__PURE__ */
|
|
1093
|
+
] }) : /* @__PURE__ */ jsx7("p", { className: "text-muted-foreground", "data-transcript-empty": "", children: "No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text." }),
|
|
1094
|
+
canEdit && sound.length > 0 && /* @__PURE__ */ jsxs6("details", { children: [
|
|
1095
|
+
/* @__PURE__ */ jsx7("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
|
|
1096
|
+
/* @__PURE__ */ jsx7("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-2", children: [
|
|
1097
|
+
/* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1 truncate", children: [
|
|
974
1098
|
a.name ?? a.id,
|
|
975
1099
|
" ",
|
|
976
1100
|
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
977
1101
|
] }),
|
|
978
|
-
/* @__PURE__ */
|
|
1102
|
+
/* @__PURE__ */ jsx7("input", { type: "file", accept: ".json,.srt,.vtt,application/json,text/vtt", "aria-label": `Load a transcript for ${a.name ?? a.id}`, className: "w-40 text-[10px]", onChange: (e) => void load(a.id, e.target.files?.[0]) })
|
|
979
1103
|
] }, a.id)) })
|
|
980
1104
|
] }),
|
|
981
|
-
message && /* @__PURE__ */
|
|
1105
|
+
message && /* @__PURE__ */ jsx7("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
982
1106
|
] });
|
|
983
1107
|
}
|
|
984
1108
|
|
|
985
1109
|
// src/video-editor/VideoEditor.tsx
|
|
986
|
-
import { useEffect as
|
|
987
|
-
import { jsx as
|
|
988
|
-
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
|
|
1110
|
+
import { useEffect as useEffect5 } from "react";
|
|
1111
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1112
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
|
|
989
1113
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
990
|
-
|
|
1114
|
+
useEffect5(() => {
|
|
991
1115
|
if (editable) return;
|
|
992
1116
|
const was = controller.getState().readOnly;
|
|
993
1117
|
controller.setReadOnly(true);
|
|
994
1118
|
return () => controller.setReadOnly(was);
|
|
995
1119
|
}, [controller, editable]);
|
|
996
1120
|
const { issues } = state;
|
|
997
|
-
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */
|
|
998
|
-
return /* @__PURE__ */
|
|
999
|
-
/* @__PURE__ */
|
|
1000
|
-
editable && /* @__PURE__ */
|
|
1001
|
-
/* @__PURE__ */
|
|
1002
|
-
/* @__PURE__ */
|
|
1003
|
-
/* @__PURE__ */
|
|
1121
|
+
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx8("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx8("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
|
|
1122
|
+
return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
1123
|
+
/* @__PURE__ */ jsxs7("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
1124
|
+
editable && /* @__PURE__ */ jsx8(MediaBin, { controller, className: "min-w-0" }),
|
|
1125
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
1126
|
+
/* @__PURE__ */ jsx8(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
1127
|
+
/* @__PURE__ */ jsx8(VideoTransport, { controller, editable }),
|
|
1128
|
+
showScopes && /* @__PURE__ */ jsx8(VideoScopes, { controller })
|
|
1004
1129
|
] }),
|
|
1005
|
-
editable && /* @__PURE__ */
|
|
1130
|
+
editable && /* @__PURE__ */ jsx8(ClipInspector, { controller, className: "min-w-0" })
|
|
1006
1131
|
] }),
|
|
1007
|
-
/* @__PURE__ */
|
|
1008
|
-
showTranscript && /* @__PURE__ */
|
|
1132
|
+
/* @__PURE__ */ jsx8(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
1133
|
+
showTranscript && /* @__PURE__ */ jsx8(TranscriptPanel, { controller, editable }),
|
|
1009
1134
|
issueList
|
|
1010
1135
|
] });
|
|
1011
1136
|
}
|
|
@@ -1015,6 +1140,7 @@ export {
|
|
|
1015
1140
|
TranscriptPanel,
|
|
1016
1141
|
VideoEditor,
|
|
1017
1142
|
VideoPreview,
|
|
1143
|
+
VideoScopes,
|
|
1018
1144
|
VideoTimeline,
|
|
1019
1145
|
VideoTransport,
|
|
1020
1146
|
useVideoEditor,
|