@pixerate/editor-svelte 0.61.0 → 0.63.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/ClipInspector.svelte +99 -2
- package/dist/video-editor/ClipInspector.svelte.d.ts.map +1 -1
- package/dist/video-editor/VideoTimeline.svelte +27 -3
- package/dist/video-editor/VideoTimeline.svelte.d.ts +3 -1
- package/dist/video-editor/VideoTimeline.svelte.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -2,14 +2,32 @@
|
|
|
2
2
|
import {
|
|
3
3
|
findVideoClip,
|
|
4
4
|
formatVideoTimecode,
|
|
5
|
+
VIDEO_CLIP_ANIMATION_EFFECTS,
|
|
6
|
+
VIDEO_CLIP_ANIMATION_LABELS,
|
|
7
|
+
normalizeVideoGrade,
|
|
8
|
+
VIDEO_GRADE_CONTROLS,
|
|
9
|
+
VIDEO_GRADE_PRESET_NAMES,
|
|
10
|
+
videoGradePreset,
|
|
11
|
+
VIDEO_BLEND_MODE_LABELS,
|
|
12
|
+
VIDEO_BLEND_MODES,
|
|
13
|
+
VIDEO_FADE_CURVE_LABELS,
|
|
14
|
+
VIDEO_FADE_CURVES,
|
|
15
|
+
VIDEO_TRANSITION_LABELS,
|
|
5
16
|
VIDEO_TRANSITION_TYPES,
|
|
17
|
+
VIDEO_WIPE_DIRECTIONS,
|
|
18
|
+
videoTransitionHasDirection,
|
|
6
19
|
videoKeyframeRows,
|
|
20
|
+
videoTransitionAudioCurve,
|
|
7
21
|
videoKeyframeToggleEdit,
|
|
8
22
|
videoKeyframeValueEdit,
|
|
9
23
|
type VideoEdit,
|
|
10
24
|
type VideoEditorController,
|
|
25
|
+
type VideoFadeCurve,
|
|
26
|
+
type VideoGradeControl,
|
|
27
|
+
type VideoGradePreset,
|
|
11
28
|
type VideoKeyframeRow,
|
|
12
|
-
type VideoTransitionType
|
|
29
|
+
type VideoTransitionType,
|
|
30
|
+
type VideoWipeDirection
|
|
13
31
|
} from '@pixerate/editor/video-editor';
|
|
14
32
|
import { videoEditorStateOf } from './createVideoEditor.svelte.js';
|
|
15
33
|
|
|
@@ -86,6 +104,27 @@
|
|
|
86
104
|
if (!type) apply({ op: 'set_transition', clip: clip.id, transition: null }, 'Remove transition');
|
|
87
105
|
else apply({ op: 'set_transition', clip: clip.id, transition: { ...clip.transition, type: type as VideoTransitionType, duration: clip.transition?.duration ?? 500 } }, 'Set transition');
|
|
88
106
|
}
|
|
107
|
+
function gradeValue(c: VideoGradeControl, raw: string) {
|
|
108
|
+
if (!clip) return;
|
|
109
|
+
const n = raw.trim() ? Number(raw) : 0;
|
|
110
|
+
if (!Number.isFinite(n)) {
|
|
111
|
+
localError = { text: `${c.label} must be a number from ${c.min} to ${c.max}.`, revision: editor.revision };
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Out-of-range values are clamped to the control's range.
|
|
115
|
+
set('grade', normalizeVideoGrade({ ...clip.grade, [c.key]: n }), `Change ${c.label.toLowerCase()}`);
|
|
116
|
+
}
|
|
117
|
+
function gradePreset(name: string) {
|
|
118
|
+
const grade = clip && name ? videoGradePreset(name as VideoGradePreset) : undefined;
|
|
119
|
+
if (clip && grade) apply({ op: 'update_clip', clip: clip.id, set: { grade } }, `Apply ${name} look`);
|
|
120
|
+
}
|
|
121
|
+
function transitionAudioCurve(curve: string) {
|
|
122
|
+
if (!clip?.transition) return;
|
|
123
|
+
const { audioCurve: _, ...rest } = clip.transition;
|
|
124
|
+
// The type's own default is stored as no curve, so it follows the type if that changes.
|
|
125
|
+
const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve as VideoFadeCurve };
|
|
126
|
+
apply({ op: 'set_transition', clip: clip.id, transition: next }, 'Change transition sound');
|
|
127
|
+
}
|
|
89
128
|
function transitionDuration(raw: string) {
|
|
90
129
|
if (!clip?.transition) return;
|
|
91
130
|
const n = Number(raw);
|
|
@@ -123,6 +162,38 @@
|
|
|
123
162
|
<span class="tabular-nums" data-field="start">{formatVideoTimecode(clip.start, fps)}</span>
|
|
124
163
|
<span class="text-muted-foreground">Duration</span>
|
|
125
164
|
<span class="tabular-nums" data-field="duration">{formatVideoTimecode(clip.duration, fps)} ({(clip.duration / 1000).toFixed(2)} s)</span>
|
|
165
|
+
{#if at?.track.kind === 'video'}
|
|
166
|
+
<span class="text-muted-foreground">Blend</span>
|
|
167
|
+
<select class={input} aria-label="Blend mode" {disabled} value={clip.blend ?? 'normal'} onchange={(e) => set('blend', e.currentTarget.value === 'normal' ? undefined : e.currentTarget.value, 'Change blend mode')} data-field="blend">
|
|
168
|
+
{#each VIDEO_BLEND_MODES as m (m)}
|
|
169
|
+
<option value={m}>{VIDEO_BLEND_MODE_LABELS[m]}</option>
|
|
170
|
+
{/each}
|
|
171
|
+
</select>
|
|
172
|
+
{/if}
|
|
173
|
+
{#if at?.track.kind === 'video'}
|
|
174
|
+
<span class="text-muted-foreground">Look</span>
|
|
175
|
+
<select class={input} aria-label="Apply a look" {disabled} value="" onchange={(e) => gradePreset(e.currentTarget.value)} data-field="gradePreset">
|
|
176
|
+
<option value="">{clip.grade ? 'graded' : 'none'}</option>
|
|
177
|
+
{#each VIDEO_GRADE_PRESET_NAMES as name (name)}
|
|
178
|
+
<option value={name}>{name.replace(/-/g, ' ')}</option>
|
|
179
|
+
{/each}
|
|
180
|
+
</select>
|
|
181
|
+
{#each VIDEO_GRADE_CONTROLS as c (c.key)}
|
|
182
|
+
<span class="text-muted-foreground">{c.label}</span>
|
|
183
|
+
<input class={input} type="number" min={c.min} max={c.max} step={c.step} placeholder="0" aria-label={c.label} {disabled} value={clip.grade?.[c.key] ?? ''} onchange={(e) => gradeValue(c, e.currentTarget.value)} data-field="grade.{c.key}" />
|
|
184
|
+
{/each}
|
|
185
|
+
{/if}
|
|
186
|
+
{#if at?.track.kind === 'video'}
|
|
187
|
+
{#each [{ key: 'animateIn', label: 'Animate in', change: 'Change entrance' }, { key: 'animateOut', label: 'Animate out', change: 'Change exit' }] as const as row (row.key)}
|
|
188
|
+
<span class="text-muted-foreground">{row.label}</span>
|
|
189
|
+
<select class={input} aria-label={row.label} {disabled} value={clip[row.key]?.effect ?? ''} onchange={(e) => set(row.key, e.currentTarget.value ? { ...clip?.[row.key], effect: e.currentTarget.value } : undefined, row.change)} data-field={row.key}>
|
|
190
|
+
<option value="">none</option>
|
|
191
|
+
{#each VIDEO_CLIP_ANIMATION_EFFECTS as effect (effect)}
|
|
192
|
+
<option value={effect}>{VIDEO_CLIP_ANIMATION_LABELS[effect]}</option>
|
|
193
|
+
{/each}
|
|
194
|
+
</select>
|
|
195
|
+
{/each}
|
|
196
|
+
{/if}
|
|
126
197
|
<span class="text-muted-foreground">Fade in</span>
|
|
127
198
|
<input class={input} type="number" min="0" step="100" placeholder="0 ms" aria-label="Fade in" {disabled} value={clip.fadeIn ?? ''} onchange={(e) => number('fadeIn', e.currentTarget.value, 'Change fade in', { min: 0, what: 'Fade in must be a number of ms, 0 or more.' })} data-field="fadeIn" />
|
|
128
199
|
<span class="text-muted-foreground">Fade out</span>
|
|
@@ -149,6 +220,16 @@
|
|
|
149
220
|
<input class={input} type="number" min="0.05" step="0.05" placeholder="1" aria-label="Speed" {disabled} value={clip.speed ?? ''} onchange={(e) => number('speed', e.currentTarget.value, 'Change speed', { above: 0, what: 'Speed must be a number above 0 (1 = normal speed).' })} data-field="speed" />
|
|
150
221
|
<span class="text-muted-foreground">Muted</span>
|
|
151
222
|
<input type="checkbox" class="justify-self-start" aria-label="Muted" {disabled} checked={!!clip.muted} onchange={(e) => set('muted', e.currentTarget.checked || undefined, e.currentTarget.checked ? 'Mute clip' : 'Unmute clip')} data-field="muted" />
|
|
223
|
+
{#each [{ key: 'fadeInCurve', on: clip.fadeIn, label: 'Fade in sound', aria: 'Fade in curve', change: 'Change fade in curve' }, { key: 'fadeOutCurve', on: clip.fadeOut, label: 'Fade out sound', aria: 'Fade out curve', change: 'Change fade out curve' }] as const as row (row.key)}
|
|
224
|
+
{#if row.on}
|
|
225
|
+
<span class="text-muted-foreground">{row.label}</span>
|
|
226
|
+
<select class={input} aria-label={row.aria} {disabled} value={clip[row.key] ?? 'linear'} onchange={(e) => set(row.key, e.currentTarget.value === 'linear' ? undefined : e.currentTarget.value, row.change)} data-field={row.key}>
|
|
227
|
+
{#each VIDEO_FADE_CURVES as c (c)}
|
|
228
|
+
<option value={c}>{VIDEO_FADE_CURVE_LABELS[c]}</option>
|
|
229
|
+
{/each}
|
|
230
|
+
</select>
|
|
231
|
+
{/if}
|
|
232
|
+
{/each}
|
|
152
233
|
{:else if clip.type === 'text'}
|
|
153
234
|
<span class="text-muted-foreground">Text</span>
|
|
154
235
|
<textarea class={input} rows="2" aria-label="Text" {disabled} value={clip.text} onchange={(e) => text('text', e.currentTarget.value, 'Change text', true)} data-field="text"></textarea>
|
|
@@ -167,7 +248,7 @@
|
|
|
167
248
|
<select class={input} aria-label="Transition" {disabled} value={clip.transition?.type ?? ''} onchange={(e) => setTransition(e.currentTarget.value)} data-field="transition">
|
|
168
249
|
<option value="">none</option>
|
|
169
250
|
{#each VIDEO_TRANSITION_TYPES as t (t)}
|
|
170
|
-
<option value={t}>{t}</option>
|
|
251
|
+
<option value={t}>{VIDEO_TRANSITION_LABELS[t]}</option>
|
|
171
252
|
{/each}
|
|
172
253
|
</select>
|
|
173
254
|
{#if clip.transition}
|
|
@@ -175,6 +256,22 @@
|
|
|
175
256
|
<span class="text-[10px] text-muted-foreground">ms</span>
|
|
176
257
|
{/if}
|
|
177
258
|
</div>
|
|
259
|
+
{#if clip.transition && videoTransitionHasDirection(clip.transition.type)}
|
|
260
|
+
<span class="text-muted-foreground">Direction</span>
|
|
261
|
+
<select class={input} aria-label="Transition direction" {disabled} value={clip.transition.direction ?? 'right'} onchange={(e) => clip?.transition && apply({ op: 'set_transition', clip: clip.id, transition: { ...clip.transition, direction: e.currentTarget.value as VideoWipeDirection } }, 'Change transition direction')} data-field="transitionDirection">
|
|
262
|
+
{#each VIDEO_WIPE_DIRECTIONS as d (d)}
|
|
263
|
+
<option value={d}>{d}</option>
|
|
264
|
+
{/each}
|
|
265
|
+
</select>
|
|
266
|
+
{/if}
|
|
267
|
+
{#if clip.transition}
|
|
268
|
+
<span class="text-muted-foreground">Transition sound</span>
|
|
269
|
+
<select class={input} aria-label="Transition sound curve" {disabled} value={videoTransitionAudioCurve(clip.transition)} onchange={(e) => transitionAudioCurve(e.currentTarget.value)} data-field="transitionAudioCurve">
|
|
270
|
+
{#each VIDEO_FADE_CURVES as c (c)}
|
|
271
|
+
<option value={c}>{VIDEO_FADE_CURVE_LABELS[c]}</option>
|
|
272
|
+
{/each}
|
|
273
|
+
</select>
|
|
274
|
+
{/if}
|
|
178
275
|
</div>
|
|
179
276
|
{/if}
|
|
180
277
|
{#if message}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClipInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/ClipInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"ClipInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/ClipInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAsBH,KAAK,qBAAqB,EAO3B,MAAM,+BAA+B,CAAC;AAIvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4OH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
videoDragLabel,
|
|
7
7
|
videoAssetMap,
|
|
8
8
|
videoClipLabel,
|
|
9
|
+
videoClipShowsWaveform,
|
|
10
|
+
videoClipWaveformPath,
|
|
9
11
|
videoClipTint,
|
|
10
12
|
videoDropTracks,
|
|
11
13
|
videoRulerTicks,
|
|
14
|
+
type VideoAsset,
|
|
12
15
|
type VideoClip,
|
|
13
16
|
type VideoClipDragMode,
|
|
14
17
|
type VideoClipDragPlan,
|
|
15
18
|
type VideoEditorController,
|
|
19
|
+
type VideoMediaClip,
|
|
16
20
|
type VideoTrack,
|
|
21
|
+
type VideoWaveformMode,
|
|
17
22
|
} from '@pixerate/editor/video-editor';
|
|
18
23
|
import { onDestroy } from 'svelte';
|
|
19
24
|
import { videoEditorStateOf } from './createVideoEditor.svelte.js';
|
|
@@ -26,10 +31,22 @@
|
|
|
26
31
|
onpxpersecondchange?: (pxPerSecond: number) => void;
|
|
27
32
|
/** Allow moving and trimming clips, the track toggles and the edit keys (default true). False still scrubs, selects and plays. */
|
|
28
33
|
editable?: boolean;
|
|
34
|
+
/** Draw waveforms in audio clips (default), in every clip with sound (`all`), or not at all. Sound is decoded once per asset through `controller.audioPeaks`. */
|
|
35
|
+
waveforms?: VideoWaveformMode;
|
|
29
36
|
class?: string;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
let { controller, pxPerSecond = $bindable(100), onpxpersecondchange, editable = true, class: className = '' }: Props = $props();
|
|
39
|
+
let { controller, pxPerSecond = $bindable(100), onpxpersecondchange, editable = true, waveforms = 'audio', class: className = '' }: Props = $props();
|
|
40
|
+
/** Bumped when more sound peaks have loaded, so waveforms redraw. */
|
|
41
|
+
let peaksVersion = $state(0);
|
|
42
|
+
$effect(() => controller.audioPeaks.subscribe(() => peaksVersion++));
|
|
43
|
+
const CLIP_HEIGHT = 40;
|
|
44
|
+
/** The waveform path for a clip, starting to load its asset's sound the first time. */
|
|
45
|
+
function waveformPath(clip: VideoMediaClip, asset: VideoAsset, w: number): string {
|
|
46
|
+
void peaksVersion;
|
|
47
|
+
const peaks = controller.audioPeaks.get(asset);
|
|
48
|
+
return peaks ? videoClipWaveformPath(clip, peaks, w, CLIP_HEIGHT) : '';
|
|
49
|
+
}
|
|
33
50
|
const view = videoEditorStateOf(() => controller);
|
|
34
51
|
const editor = $derived(view.current);
|
|
35
52
|
const project = $derived(editor.project);
|
|
@@ -320,10 +337,17 @@
|
|
|
320
337
|
data-dragging={item.dragging ? '' : undefined}
|
|
321
338
|
onpointerdown={(e) => onclipdown(e, clip, track)}
|
|
322
339
|
>
|
|
340
|
+
{#if clip.type === 'media' && videoClipShowsWaveform(clip, asset, waveforms)}
|
|
341
|
+
{@const w = Math.max(2, x(clip.duration))}
|
|
342
|
+
{@const d = waveformPath(clip, asset, w)}
|
|
343
|
+
{#if d}
|
|
344
|
+
<svg class="pointer-events-none absolute inset-0" width={w} height={CLIP_HEIGHT} aria-hidden="true" data-waveform={clip.id}><path {d} fill="currentColor" fill-opacity="0.45" /></svg>
|
|
345
|
+
{/if}
|
|
346
|
+
{/if}
|
|
323
347
|
{#if clip.transition}
|
|
324
|
-
<span class="shrink-0 rounded bg-black/40 px-1 text-[9px]" title="{clip.transition.type} {clip.transition.duration} ms" data-transition={clip.transition.type}>⋈</span>
|
|
348
|
+
<span class="relative shrink-0 rounded bg-black/40 px-1 text-[9px]" title="{clip.transition.type} {clip.transition.duration} ms" data-transition={clip.transition.type}>⋈</span>
|
|
325
349
|
{/if}
|
|
326
|
-
<span class="truncate drop-shadow">{videoClipLabel(clip, asset)}</span>
|
|
350
|
+
<span class="relative truncate drop-shadow">{videoClipLabel(clip, asset)}</span>
|
|
327
351
|
{#if canEdit && !track.locked}
|
|
328
352
|
<div class="absolute inset-y-0 left-0 w-1.5 cursor-ew-resize bg-white/0 hover:bg-white/40" data-edge="start" aria-hidden="true"></div>
|
|
329
353
|
<div class="absolute inset-y-0 right-0 w-1.5 cursor-ew-resize bg-white/0 hover:bg-white/40" data-edge="end" aria-hidden="true"></div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type VideoEditorController } from '@pixerate/editor/video-editor';
|
|
1
|
+
import { type VideoEditorController, type VideoWaveformMode } from '@pixerate/editor/video-editor';
|
|
2
2
|
interface Props {
|
|
3
3
|
controller: VideoEditorController;
|
|
4
4
|
/** Zoom: pixels per second of timeline (default 100). Bindable. */
|
|
@@ -7,6 +7,8 @@ interface Props {
|
|
|
7
7
|
onpxpersecondchange?: (pxPerSecond: number) => void;
|
|
8
8
|
/** Allow moving and trimming clips, the track toggles and the edit keys (default true). False still scrubs, selects and plays. */
|
|
9
9
|
editable?: boolean;
|
|
10
|
+
/** Draw waveforms in audio clips (default), in every clip with sound (`all`), or not at all. Sound is decoded once per asset through `controller.audioPeaks`. */
|
|
11
|
+
waveforms?: VideoWaveformMode;
|
|
10
12
|
class?: string;
|
|
11
13
|
}
|
|
12
14
|
declare const VideoTimeline: import("svelte").Component<Props, {}, "pxPerSecond">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/VideoTimeline.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"VideoTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/VideoTimeline.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAgBH,KAAK,qBAAqB,EAG1B,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAKvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,kIAAkI;IAClI,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iKAAiK;IACjK,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2RH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"@tiptap/extension-bubble-menu": "^2.11.5",
|
|
132
132
|
"@tiptap/extension-placeholder": "^2.11.5",
|
|
133
133
|
"@tiptap/starter-kit": "^2.11.5",
|
|
134
|
-
"@pixerate/editor": "0.
|
|
134
|
+
"@pixerate/editor": "0.84.0"
|
|
135
135
|
},
|
|
136
136
|
"peerDependencies": {
|
|
137
137
|
"@dagrejs/dagre": "^1.1.4",
|