@pixerate/editor-svelte 0.58.0 → 0.60.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.
@@ -1,5 +1,16 @@
1
1
  <script lang="ts">
2
- import { findVideoClip, formatVideoTimecode, VIDEO_TRANSITION_TYPES, type VideoEdit, type VideoEditorController, type VideoTransitionType } from '@pixerate/editor/video-editor';
2
+ import {
3
+ findVideoClip,
4
+ formatVideoTimecode,
5
+ VIDEO_TRANSITION_TYPES,
6
+ videoKeyframeRows,
7
+ videoKeyframeToggleEdit,
8
+ videoKeyframeValueEdit,
9
+ type VideoEdit,
10
+ type VideoEditorController,
11
+ type VideoKeyframeRow,
12
+ type VideoTransitionType
13
+ } from '@pixerate/editor/video-editor';
3
14
  import { videoEditorStateOf } from './createVideoEditor.svelte.js';
4
15
 
5
16
  interface Props {
@@ -17,6 +28,7 @@
17
28
  let localError = $state<{ text: string; revision: number } | null>(null);
18
29
  // A local error shows until the editor changes; then the editor's own error (if any) shows.
19
30
  const message = $derived((localError && localError.revision === editor.revision ? localError.text : null) ?? editor.error);
31
+ const rows = $derived(clip ? videoKeyframeRows(editor.project, clip.id, editor.time) : []);
20
32
  const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50';
21
33
 
22
34
  function apply(edit: VideoEdit, label: string) {
@@ -47,6 +59,28 @@
47
59
  }
48
60
  set(key, key === 'text' ? raw : raw.trim(), label);
49
61
  }
62
+ /** The value shown in a property's field: blank when it is plain and at its default. */
63
+ function shown(row: VideoKeyframeRow): string | number {
64
+ if (!clip || row.keyed) return row.value;
65
+ const plain = row.property === 'opacity' ? clip.opacity : row.property === 'volume' ? (clip as { volume?: unknown }).volume : clip.transform?.[row.property];
66
+ return plain === undefined ? '' : row.value;
67
+ }
68
+ /** Typing a value: sets the key at the playhead when the property is keyed, else the plain value (empty resets it). */
69
+ function animatable(row: VideoKeyframeRow, raw: string) {
70
+ if (!clip) return;
71
+ const n = Number(raw);
72
+ if (raw.trim() !== '' && (!Number.isFinite(n) || (row.min !== undefined && n < row.min) || (row.max !== undefined && n > row.max))) {
73
+ localError = { text: `${row.label} must be ${row.max !== undefined ? `between ${row.min} and ${row.max}` : row.min !== undefined ? `${row.min} or more` : 'a number'}.`, revision: editor.revision };
74
+ return;
75
+ }
76
+ const edit = videoKeyframeValueEdit(editor.project, clip.id, row.property, editor.time, raw.trim() === '' ? undefined : n);
77
+ if (edit) apply(edit, row.keyed ? `Keyframe ${row.label.toLowerCase()}` : `Change ${row.label.toLowerCase()}`);
78
+ }
79
+ function toggleKey(row: VideoKeyframeRow) {
80
+ if (!clip) return;
81
+ const edit = videoKeyframeToggleEdit(editor.project, clip.id, row.property, editor.time);
82
+ if (edit) apply(edit, `${row.onKey ? 'Remove' : 'Add'} ${row.label.toLowerCase()} keyframe`);
83
+ }
50
84
  function setTransition(type: string) {
51
85
  if (!clip) return;
52
86
  if (!type) apply({ op: 'set_transition', clip: clip.id, transition: null }, 'Remove transition');
@@ -64,7 +98,9 @@
64
98
  </script>
65
99
 
66
100
  <!--
67
- The selected clip: its name, timing, opacity and fades; volume, speed and mute for media; text,
101
+ The selected clip: its name, timing and fades; opacity, volume and transform, each with a keyframe
102
+ toggle at the playhead (◆ adds a key holding the value there or removes the one there; typing a
103
+ value on a keyed property sets the key at the playhead); speed and mute for media; text,
68
104
  colours and size for titles; the colour of a colour clip; and the transition into it. Every
69
105
  committed change is one undo step.
70
106
  -->
@@ -87,15 +123,28 @@
87
123
  <span class="tabular-nums" data-field="start">{formatVideoTimecode(clip.start, fps)}</span>
88
124
  <span class="text-muted-foreground">Duration</span>
89
125
  <span class="tabular-nums" data-field="duration">{formatVideoTimecode(clip.duration, fps)} ({(clip.duration / 1000).toFixed(2)} s)</span>
90
- <span class="text-muted-foreground">Opacity</span>
91
- <input class={input} type="number" min="0" max="1" step="0.05" placeholder="1" aria-label="Opacity" {disabled} value={clip.opacity ?? ''} onchange={(e) => number('opacity', e.currentTarget.value, 'Change opacity', { min: 0, max: 1, what: 'Opacity must be between 0 and 1.' })} data-field="opacity" />
92
126
  <span class="text-muted-foreground">Fade in</span>
93
127
  <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" />
94
128
  <span class="text-muted-foreground">Fade out</span>
95
129
  <input class={input} type="number" min="0" step="100" placeholder="0 ms" aria-label="Fade out" {disabled} value={clip.fadeOut ?? ''} onchange={(e) => number('fadeOut', e.currentTarget.value, 'Change fade out', { min: 0, what: 'Fade out must be a number of ms, 0 or more.' })} data-field="fadeOut" />
130
+ {#each rows as row (row.property)}
131
+ <span class="text-muted-foreground">{row.label}</span>
132
+ <div class="flex min-w-0 items-center gap-1" data-keyframe-row={row.property}>
133
+ <input class={input} type="number" min={row.min} max={row.max} step={row.step} placeholder={String(row.value)} aria-label={row.label} {disabled} value={shown(row)} onchange={(e) => animatable(row, e.currentTarget.value)} data-field={row.property} />
134
+ <button
135
+ type="button"
136
+ class="shrink-0 rounded px-1 disabled:opacity-50 {row.onKey ? 'text-amber-500' : row.keyed ? 'text-foreground' : 'text-muted-foreground'}"
137
+ aria-label="{row.onKey ? 'Remove' : 'Add'} {row.label.toLowerCase()} keyframe at playhead"
138
+ aria-pressed={row.onKey}
139
+ title={row.keyed ? `${row.keys} key${row.keys === 1 ? '' : 's'}; ${row.onKey ? 'remove the one' : 'add one'} at the playhead` : 'Animate: add a key at the playhead'}
140
+ {disabled}
141
+ onclick={() => toggleKey(row)}
142
+ data-keyframe-toggle={row.property}>{row.onKey ? '◆' : '◇'}</button
143
+ >
144
+ {#if row.keyed}<span class="shrink-0 text-[10px] tabular-nums text-muted-foreground" data-keyframe-count={row.property}>{row.keys}</span>{/if}
145
+ </div>
146
+ {/each}
96
147
  {#if clip.type === 'media'}
97
- <span class="text-muted-foreground">Volume</span>
98
- <input class={input} type="number" min="0" step="0.05" placeholder="1" aria-label="Volume" {disabled} value={clip.volume ?? ''} onchange={(e) => number('volume', e.currentTarget.value, 'Change volume', { min: 0, what: 'Volume must be 0 or more (1 = unchanged).' })} data-field="volume" />
99
148
  <span class="text-muted-foreground">Speed</span>
100
149
  <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" />
101
150
  <span class="text-muted-foreground">Muted</span>
@@ -1 +1 @@
1
- {"version":3,"file":"ClipInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/ClipInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8E,KAAK,qBAAqB,EAA4B,MAAM,+BAA+B,CAAC;AAI/K,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkIH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"ClipInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/ClipInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAQH,KAAK,qBAAqB,EAG3B,MAAM,+BAA+B,CAAC;AAIvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6JH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -3,6 +3,7 @@
3
3
  findVideoClip,
4
4
  formatVideoTimecode,
5
5
  planVideoClipDrag,
6
+ videoDragLabel,
6
7
  videoAssetMap,
7
8
  videoClipLabel,
8
9
  videoClipTint,
@@ -119,6 +120,7 @@
119
120
  delta: (dx * 1000) / px,
120
121
  track: d.mode === 'move' ? trackAt(e.clientY, clip) : undefined,
121
122
  ripple: e.altKey,
123
+ placement: e.altKey ? 'insert' : e.shiftKey ? 'overwrite' : 'refuse',
122
124
  snap: (t) => controller.snap(t, { exclude: [clip] }).time,
123
125
  });
124
126
  drag = { ...d, moved: true, plan };
@@ -131,7 +133,7 @@
131
133
  if (!d) return;
132
134
  if (d.moved) {
133
135
  const edit = d.plan?.edit;
134
- if (edit) controller.apply(edit, edit.op === 'move_clip' ? 'Move clip' : edit.op === 'trim_clip' && edit.ripple ? 'Ripple trim' : 'Trim clip');
136
+ if (edit) controller.apply(edit, videoDragLabel(edit));
135
137
  } else if (!d.additive) controller.select([d.clip]);
136
138
  }
137
139
 
@@ -210,7 +212,8 @@
210
212
  <!--
211
213
  The timeline: a ruler to scrub (with markers), the playhead, and a row per track with its
212
214
  toggles and clips. Click a clip to select it (shift / ⌘ to add), drag it to move it (onto
213
- another track of its kind), drag an edge to trim (alt: ripple). Keys: Space plays, ←/→ step a
215
+ another track of its kind; alt: insert, pushing later material along on every sync-locked track;
216
+ shift: overwrite what is underneath), drag an edge to trim (alt: ripple). Keys: Space plays, ←/→ step a
214
217
  frame, S splits, Delete deletes (shift: ripple), ⌘Z / ⇧⌘Z undo and redo. Every edit is one undo step.
215
218
  -->
216
219
  <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
@@ -1 +1 @@
1
- {"version":3,"file":"VideoTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/VideoTimeline.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAYH,KAAK,qBAAqB,EAE3B,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,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyQH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"VideoTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/VideoTimeline.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAaH,KAAK,qBAAqB,EAE3B,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,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0QH,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.58.0",
3
+ "version": "0.60.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.78.0"
134
+ "@pixerate/editor": "0.80.0"
135
135
  },
136
136
  "peerDependencies": {
137
137
  "@dagrejs/dagre": "^1.1.4",