@pixerate/editor-svelte 0.170.0 → 0.171.1

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.
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import { findVideoClip, videoClipCameraMove, type VideoCameraMoveResult, type VideoEditorController } from '@pixerate/editor/video-editor';
3
+ import type { CameraMoveRole, EffectSummary } from '@pixerate/editor/effects';
4
+ import EffectParameters from '../effects/EffectParameters.svelte';
5
+ import { effectDescriptionOf } from '../effects/effects.svelte.js';
6
+ import { videoEditorStateOf } from './createVideoEditor.svelte.js';
7
+
8
+ interface Props {
9
+ controller: VideoEditorController;
10
+ /** The clip whose moves to edit. */
11
+ clip: string;
12
+ class?: string;
13
+ }
14
+
15
+ let { controller, clip, class: className = '' }: Props = $props();
16
+ const view = videoEditorStateOf(() => controller);
17
+ const editor = $derived(view.current);
18
+ const at = $derived(findVideoClip(editor.project, clip));
19
+ const current = $derived(at && at.track.kind !== 'audio' && at.clip.type !== 'adjustment' ? at.clip : undefined);
20
+ const disabled = $derived(editor.readOnly || !!at?.track.locked);
21
+ const roles: { role: CameraMoveRole; label: string }[] = [
22
+ { role: 'transition', label: 'Move in' },
23
+ { role: 'hold', label: 'While holding' }
24
+ ];
25
+ const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs';
26
+
27
+ // The provider's camera moves of each role, loaded when the provider changes.
28
+ let moves = $state<Record<CameraMoveRole, EffectSummary[]>>({ transition: [], hold: [] });
29
+ $effect(() => {
30
+ void editor.effectsProvider;
31
+ let live = true;
32
+ void Promise.all([controller.cameraMoves('transition'), controller.cameraMoves('hold')]).then(
33
+ ([transition, hold]) => live && (moves = { transition, hold }),
34
+ () => live && (moves = { transition: [], hold: [] })
35
+ );
36
+ return () => (live = false);
37
+ });
38
+ const inMove = $derived(videoClipCameraMove(current, 'transition'));
39
+ const holdMove = $derived(videoClipCameraMove(current, 'hold'));
40
+ const inDescription = effectDescriptionOf(() => controller.effectsProvider, () => inMove?.move);
41
+ const holdDescription = effectDescriptionOf(() => controller.effectsProvider, () => holdMove?.move);
42
+ let message = $state<string | null>(null);
43
+
44
+ function report(r: Promise<VideoCameraMoveResult>) {
45
+ void r.then((x) => (message = x.ok ? null : x.error));
46
+ }
47
+ function pick(role: CameraMoveRole, id: string) {
48
+ report(id ? controller.setClipCameraMove(clip, role, id) : controller.clearClipCameraMove(clip, role));
49
+ }
50
+ // Changes over the move as saved; null removes a field (a changed value drops the preset's name).
51
+ function set(role: CameraMoveRole, changes: { values?: Record<string, unknown>; preset?: string | null; ms?: number | null }, merge?: string | null) {
52
+ const move = videoClipCameraMove(current, role);
53
+ if (!move) return;
54
+ const next: Record<string, unknown> = { ...move, ...changes };
55
+ for (const k of Object.keys(next)) if (next[k] === null || (k === 'values' && !Object.keys(next[k] as object).length)) delete next[k];
56
+ report(controller.setClipCameraMove(clip, role, next as never, { merge: merge ?? undefined }));
57
+ }
58
+ function length(raw: string) {
59
+ if (raw.trim() === '') return set('transition', { ms: null });
60
+ const n = Number(raw);
61
+ if (Number.isFinite(n) && n >= 0) set('transition', { ms: n });
62
+ else message = 'Length is a number of ms, 0 or more.';
63
+ }
64
+ function reset(role: CameraMoveRole, k: string) {
65
+ const { [k]: _gone, ...rest } = videoClipCameraMove(current, role)?.values ?? {};
66
+ set(role, { values: rest, preset: null });
67
+ }
68
+ function zoom(raw: string) {
69
+ if (!current?.camera) return;
70
+ const role: CameraMoveRole = current.camera.in !== undefined ? 'transition' : 'hold';
71
+ const z = Number(raw);
72
+ if (raw.trim() !== '' && !(z > 0)) {
73
+ message = 'Zoom is a number above 0 (1 shows the whole picture).';
74
+ return;
75
+ }
76
+ report(controller.setClipCameraMove(clip, role, videoClipCameraMove(current, role)!, { framing: raw.trim() === '' ? null : { ...current.camera.framing, zoom: z } }));
77
+ }
78
+ </script>
79
+
80
+ <!--
81
+ A clip's camera moves from the effects provider: the move into the clip and the move across the
82
+ rest of it, with the zoom they frame, a move in's length, presets and controls. The moves are
83
+ baked into the clip's transform keys. Hidden with the reason when the provider has no camera
84
+ moves. Every change is one undo step (a slider drag is one).
85
+ -->
86
+ {#if current}
87
+ {#if !moves.transition.length && !moves.hold.length}
88
+ {#if current.camera}
89
+ <p class="text-[10px] text-muted-foreground {className}" data-camera-moves="unavailable">
90
+ This clip's camera moves are baked into its keys; {editor.effectsProvider ? `${editor.effectsProvider.name} has no camera moves` : 'no effects provider is set'}, so they cannot be changed here.
91
+ </p>
92
+ {/if}
93
+ {:else}
94
+ <div class="flex flex-col gap-1 text-xs {className}" data-camera-moves="">
95
+ {#each roles as { role, label } (role)}
96
+ {@const move = role === 'transition' ? inMove : holdMove}
97
+ {@const loaded = role === 'transition' ? inDescription : holdDescription}
98
+ {@const known = moves[role].some((m) => m.id === move?.move)}
99
+ <label class="grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2">
100
+ <span class="text-muted-foreground">{label}</span>
101
+ <select class={input} aria-label={label} {disabled} value={move?.move ?? ''} onchange={(e) => pick(role, e.currentTarget.value)}>
102
+ <option value="">none</option>
103
+ {#if move && !known}<option value={move.move}>{move.move} (not in the provider)</option>{/if}
104
+ {#each moves[role] as m (m.id)}
105
+ <option value={m.id}>{m.name}</option>
106
+ {/each}
107
+ </select>
108
+ </label>
109
+ {#if move && known && !disabled}
110
+ {#if loaded.error}
111
+ <p class="text-[11px] text-amber-500">{loaded.error}</p>
112
+ {:else if loaded.description}
113
+ <div class="flex flex-col gap-1 border-l border-border pl-2" data-camera-move-controls={role}>
114
+ {#if role === 'transition'}
115
+ <label class="flex items-center gap-1">
116
+ <span class="w-16 text-muted-foreground">Length</span>
117
+ <input class={input} type="number" min="0" step="50" aria-label="Move length" placeholder={String(loaded.description.camera?.durationMs ?? '')} value={move.ms ?? ''} onchange={(e) => length(e.currentTarget.value)} />
118
+ <span class="text-[10px] text-muted-foreground">ms</span>
119
+ </label>
120
+ {/if}
121
+ <EffectParameters
122
+ description={loaded.description}
123
+ values={move.values ?? {}}
124
+ onchange={(k, v, g) => set(role, { values: { ...(move.values ?? {}), [k]: v }, preset: null }, g)}
125
+ onreset={(k) => reset(role, k)}
126
+ onpresetname={(name) => set(role, { preset: name, values: {} })}
127
+ />
128
+ </div>
129
+ {/if}
130
+ {/if}
131
+ {/each}
132
+ {#if current.camera && !disabled}
133
+ <label class="grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2">
134
+ <span class="text-muted-foreground">Camera zoom</span>
135
+ <input class={input} type="number" min="0.1" step="0.1" aria-label="Camera zoom" placeholder="1" value={current.camera.framing?.zoom ?? ''} onchange={(e) => zoom(e.currentTarget.value)} />
136
+ </label>
137
+ {/if}
138
+ {#if message}<p class="text-[11px] text-destructive" role="alert">{message}</p>{/if}
139
+ </div>
140
+ {/if}
141
+ {/if}
@@ -0,0 +1,10 @@
1
+ import { type VideoEditorController } from '@pixerate/editor/video-editor';
2
+ interface Props {
3
+ controller: VideoEditorController;
4
+ /** The clip whose moves to edit. */
5
+ clip: string;
6
+ class?: string;
7
+ }
8
+ declare const ClipCameraMoves: import("svelte").Component<Props, {}, "">;
9
+ type ClipCameraMoves = ReturnType<typeof ClipCameraMoves>;
10
+ export default ClipCameraMoves;
@@ -46,6 +46,7 @@
46
46
  type VideoWipeDirection
47
47
  } from '@pixerate/editor/video-editor';
48
48
  import { videoEditorStateOf } from './createVideoEditor.svelte.js';
49
+ import ClipCameraMoves from './ClipCameraMoves.svelte';
49
50
 
50
51
  interface Props {
51
52
  controller: VideoEditorController;
@@ -238,7 +239,8 @@
238
239
  The selected clip: its name, timing and fades; opacity, volume and transform, each with a keyframe
239
240
  toggle at the playhead (◆ adds a key holding the value there or removes the one there; typing a
240
241
  value on a keyed property sets the key at the playhead); speed and mute for media; text,
241
- colours and size for titles; the colour of a colour clip; and the transition into it. Every
242
+ colours and size for titles; the colour of a colour clip; the transition into it; and its camera
243
+ moves (ClipCameraMoves). Every
242
244
  committed change is one undo step.
243
245
  -->
244
246
  <div class="flex flex-col gap-1.5 text-xs {className}" data-clip-inspector="" data-clip={clip?.id}>
@@ -444,6 +446,7 @@
444
446
  {/if}
445
447
  </div>
446
448
  {/if}
449
+ {#if clip}<ClipCameraMoves {controller} clip={clip.id} />{/if}
447
450
  {#if message}
448
451
  <p class="text-red-500" role="alert" data-edit-error="">{message}</p>
449
452
  {/if}
@@ -5,6 +5,7 @@ import type VideoScopesComponent from "./VideoScopes.svelte";
5
5
  import type VideoTransportComponent from "./VideoTransport.svelte";
6
6
  import type VideoTimelineComponent from "./VideoTimeline.svelte";
7
7
  import type ClipInspectorComponent from "./ClipInspector.svelte";
8
+ import type ClipCameraMovesComponent from "./ClipCameraMoves.svelte";
8
9
  import type MediaBinComponent from "./MediaBin.svelte";
9
10
  import type TranscriptPanelComponent from "./TranscriptPanel.svelte";
10
11
  import type TranscriptEditorComponent from "./TranscriptEditor.svelte";
@@ -16,6 +17,7 @@ export { default as VideoScopes } from "./VideoScopes.svelte";
16
17
  export { default as VideoTransport } from "./VideoTransport.svelte";
17
18
  export { default as VideoTimeline } from "./VideoTimeline.svelte";
18
19
  export { default as ClipInspector } from "./ClipInspector.svelte";
20
+ export { default as ClipCameraMoves } from "./ClipCameraMoves.svelte";
19
21
  export { default as MediaBin } from "./MediaBin.svelte";
20
22
  export { default as TranscriptPanel } from "./TranscriptPanel.svelte";
21
23
  export { default as TranscriptEditor } from "./TranscriptEditor.svelte";
@@ -28,6 +30,7 @@ export type VideoScopesProps = ComponentProps<typeof VideoScopesComponent>;
28
30
  export type VideoTransportProps = ComponentProps<typeof VideoTransportComponent>;
29
31
  export type VideoTimelineProps = ComponentProps<typeof VideoTimelineComponent>;
30
32
  export type ClipInspectorProps = ComponentProps<typeof ClipInspectorComponent>;
33
+ export type ClipCameraMovesProps = ComponentProps<typeof ClipCameraMovesComponent>;
31
34
  export type MediaBinProps = ComponentProps<typeof MediaBinComponent>;
32
35
  export type TranscriptPanelProps = ComponentProps<typeof TranscriptPanelComponent>;
33
36
  export type TranscriptEditorProps = ComponentProps<typeof TranscriptEditorComponent>;
@@ -4,6 +4,7 @@ export { default as VideoScopes } from "./VideoScopes.svelte";
4
4
  export { default as VideoTransport } from "./VideoTransport.svelte";
5
5
  export { default as VideoTimeline } from "./VideoTimeline.svelte";
6
6
  export { default as ClipInspector } from "./ClipInspector.svelte";
7
+ export { default as ClipCameraMoves } from "./ClipCameraMoves.svelte";
7
8
  export { default as MediaBin } from "./MediaBin.svelte";
8
9
  export { default as TranscriptPanel } from "./TranscriptPanel.svelte";
9
10
  export { default as TranscriptEditor } from "./TranscriptEditor.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.170.0",
3
+ "version": "0.171.1",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -165,7 +165,7 @@
165
165
  "**/*.css"
166
166
  ],
167
167
  "dependencies": {
168
- "@pixerate/editor": "0.230.0"
168
+ "@pixerate/editor": "0.231.1"
169
169
  },
170
170
  "peerDependencies": {
171
171
  "@dagrejs/dagre": "^1.1.4",