@pixerate/editor-svelte 0.177.0 → 0.179.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/paint-editor/PaintCanvas.svelte +7 -0
- package/dist/paint-editor/PaintEffectsPanel.svelte +45 -2
- package/dist/video-editor/EffectControls.svelte +9 -3
- package/dist/video-editor/EffectControls.svelte.d.ts +9 -2
- package/dist/video-editor/PathControls.svelte +32 -0
- package/dist/video-editor/PathControls.svelte.d.ts +13 -0
- package/dist/video-editor/VideoEffectsPanel.svelte +2 -0
- package/dist/video-editor/VideoPreview.svelte +45 -8
- package/package.json +2 -2
|
@@ -102,6 +102,13 @@
|
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
// Live effect paint plays while the canvas is mounted (idle when nothing is live or it is paused).
|
|
106
|
+
$effect(() => {
|
|
107
|
+
const c = controller;
|
|
108
|
+
const player = untrack(() => c.playEffects());
|
|
109
|
+
return () => player.stop();
|
|
110
|
+
});
|
|
111
|
+
|
|
105
112
|
// Wheel needs a non-passive listener to prevent page scroll (see GOTCHAS).
|
|
106
113
|
$effect(() => {
|
|
107
114
|
const el = viewport;
|
|
@@ -25,8 +25,16 @@
|
|
|
25
25
|
effectPaint: editor.current.effectPaint,
|
|
26
26
|
rendering: editor.current.effectPaintRendering,
|
|
27
27
|
color: editor.current.color,
|
|
28
|
+
animation: controller.getAnimation(),
|
|
29
|
+
playback: editor.current.effectPlayback,
|
|
28
30
|
};
|
|
29
31
|
});
|
|
32
|
+
/** Paints with the effect paint again with some of it changed (the other settings kept). */
|
|
33
|
+
const repaint = (patch: { values?: Record<string, unknown>; map?: unknown; live?: boolean }) => {
|
|
34
|
+
const fx = view.effectPaint;
|
|
35
|
+
if (!fx) return;
|
|
36
|
+
run(() => controller.setEffectPaint({ effect: fx.effect, values: fx.values as never, time: fx.time, map: fx.map, live: fx.live, ...(patch as object) }));
|
|
37
|
+
};
|
|
30
38
|
const thumbnail = $derived.by(() => {
|
|
31
39
|
const { effectPaint, rendering } = view;
|
|
32
40
|
return effectPaint && !rendering ? untrack(() => controller.effectPaintThumbnail(48)) : null;
|
|
@@ -129,9 +137,44 @@
|
|
|
129
137
|
{provider}
|
|
130
138
|
effect={fx.effect}
|
|
131
139
|
values={fx.values}
|
|
132
|
-
onchange={(k, v) =>
|
|
133
|
-
onpreset={(values) =>
|
|
140
|
+
onchange={(k, v) => repaint({ values: { ...fx.values, [k]: v } })}
|
|
141
|
+
onpreset={(values) => repaint({ values: { ...fx.values, ...values } })}
|
|
134
142
|
/>
|
|
143
|
+
<div class="flex flex-wrap items-center gap-2" data-testid="effect-paint-placement">
|
|
144
|
+
<label class="flex items-center gap-1" title="Lay the effect along each stroke instead of fixing it to the canvas">
|
|
145
|
+
<input type="checkbox" checked={Boolean(fx.map)} onchange={(e) => repaint({ map: e.currentTarget.checked ? { by: 'stroke', length: 300, repeat: 'repeat' } : { by: 'canvas' } })} />
|
|
146
|
+
Along the stroke
|
|
147
|
+
</label>
|
|
148
|
+
<label class="flex items-center gap-1" title="Strokes on a stroke layer keep playing the effect">
|
|
149
|
+
<input type="checkbox" checked={Boolean(fx.live)} onchange={(e) => repaint({ live: e.currentTarget.checked })} />
|
|
150
|
+
Live
|
|
151
|
+
</label>
|
|
152
|
+
</div>
|
|
153
|
+
{#if fx.map}
|
|
154
|
+
{@const map = fx.map}
|
|
155
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
156
|
+
<label class="flex items-center gap-1">
|
|
157
|
+
<span class="text-slate-400">Length</span>
|
|
158
|
+
<input type="number" aria-label="Effect length along the stroke" min="16" max="4096" value={map.length} onchange={(e) => { const length = Number(e.currentTarget.value); if (length !== map.length) repaint({ map: { ...map, length } }); }} class="w-16 rounded bg-slate-800 px-1 py-0.5" />
|
|
159
|
+
</label>
|
|
160
|
+
<select aria-label="Effect repeat" value={map.repeat} onchange={(e) => repaint({ map: { ...map, repeat: e.currentTarget.value } })} class="rounded bg-slate-800 px-1 py-0.5">
|
|
161
|
+
<option value="repeat">Repeat</option>
|
|
162
|
+
<option value="mirror">Mirror</option>
|
|
163
|
+
<option value="clamp">Clamp</option>
|
|
164
|
+
</select>
|
|
165
|
+
</div>
|
|
166
|
+
{/if}
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
169
|
+
{#if view.animation}
|
|
170
|
+
{@const animation = view.animation}
|
|
171
|
+
<div class="flex items-center gap-2 rounded border border-slate-700 p-1" data-effect-part="playback">
|
|
172
|
+
<span class="flex-1 truncate">Live paint: {animation.strokes} {animation.strokes === 1 ? 'stroke' : 'strokes'}</span>
|
|
173
|
+
<button type="button" class={button} aria-pressed={view.playback.playing} onclick={() => attempt(() => controller.setEffectPlayback({ playing: !view.playback.playing }), (m) => (error = m))}>{view.playback.playing ? 'Pause' : 'Play'}</button>
|
|
174
|
+
<label class="flex items-center gap-1">
|
|
175
|
+
<span class="text-slate-400">fps</span>
|
|
176
|
+
<input type="number" aria-label="Live paint frame rate" min="1" max="60" value={view.playback.fps} onchange={(e) => attempt(() => controller.setEffectPlayback({ fps: Math.round(Number(e.currentTarget.value)) }), (m) => (error = m))} class="w-12 rounded bg-slate-800 px-1 py-0.5" />
|
|
177
|
+
</label>
|
|
135
178
|
</div>
|
|
136
179
|
{/if}
|
|
137
180
|
{#if view.layerEffect && view.activeId}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import { isVideoKeyframes, type VideoEffectValues } from '@pixerate/editor/video-editor';
|
|
2
|
+
import { pathControlsOf, type EffectProvider } from '@pixerate/editor/effects';
|
|
3
|
+
import { isVideoKeyframes, type VideoClip, type VideoEditorController, type VideoEffectEditResult, type VideoEffectValues } from '@pixerate/editor/video-editor';
|
|
4
|
+
import PathControls from './PathControls.svelte';
|
|
4
5
|
import EffectParameters from '../effects/EffectParameters.svelte';
|
|
5
6
|
import { effectDescriptionOf } from '../effects/effects.svelte.js';
|
|
6
7
|
|
|
@@ -12,9 +13,11 @@
|
|
|
12
13
|
disabled: boolean;
|
|
13
14
|
onchange: (control: string, value: unknown, gesture: string | null) => void;
|
|
14
15
|
onpreset: (values: Readonly<Record<string, unknown>>) => void;
|
|
16
|
+
/** For an effect on a clip: where its path comes from, when it draws along one. */
|
|
17
|
+
path?: { controller: VideoEditorController; clip: VideoClip; instance: string; report: (r: VideoEffectEditResult | Promise<VideoEffectEditResult>) => void };
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
let { provider, effect, name, values, disabled, onchange, onpreset }: Props = $props();
|
|
20
|
+
let { provider, effect, name, values, disabled, onchange, onpreset, path }: Props = $props();
|
|
18
21
|
const loaded = effectDescriptionOf(() => provider, () => effect);
|
|
19
22
|
const keyed = $derived(new Set(Object.entries(values ?? {}).filter(([, v]) => isVideoKeyframes(v)).map(([k]) => k)));
|
|
20
23
|
</script>
|
|
@@ -28,5 +31,8 @@
|
|
|
28
31
|
<p class="text-[11px] text-muted-foreground">Loading…</p>
|
|
29
32
|
{:else}
|
|
30
33
|
{@const description = loaded.description}
|
|
34
|
+
{#if path && pathControlsOf(description).length}
|
|
35
|
+
<PathControls controller={path.controller} clip={path.clip} instance={path.instance} report={path.report} {provider} {disabled} />
|
|
36
|
+
{/if}
|
|
31
37
|
<EffectParameters {description} values={values ?? {}} keyframed={keyed} {disabled} {onchange} onreset={(k) => onchange(k, description.defaults[k], null)} {onpreset} />
|
|
32
38
|
{/if}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type VideoEffectValues } from '@pixerate/editor/video-editor';
|
|
1
|
+
import { type EffectProvider } from '@pixerate/editor/effects';
|
|
2
|
+
import { type VideoClip, type VideoEditorController, type VideoEffectEditResult, type VideoEffectValues } from '@pixerate/editor/video-editor';
|
|
3
3
|
interface Props {
|
|
4
4
|
provider: EffectProvider | null;
|
|
5
5
|
effect: string;
|
|
@@ -8,6 +8,13 @@ interface Props {
|
|
|
8
8
|
disabled: boolean;
|
|
9
9
|
onchange: (control: string, value: unknown, gesture: string | null) => void;
|
|
10
10
|
onpreset: (values: Readonly<Record<string, unknown>>) => void;
|
|
11
|
+
/** For an effect on a clip: where its path comes from, when it draws along one. */
|
|
12
|
+
path?: {
|
|
13
|
+
controller: VideoEditorController;
|
|
14
|
+
clip: VideoClip;
|
|
15
|
+
instance: string;
|
|
16
|
+
report: (r: VideoEffectEditResult | Promise<VideoEffectEditResult>) => void;
|
|
17
|
+
};
|
|
11
18
|
}
|
|
12
19
|
declare const EffectControls: import("svelte").Component<Props, {}, "">;
|
|
13
20
|
type EffectControls = ReturnType<typeof EffectControls>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EffectProvider } from '@pixerate/editor/effects';
|
|
3
|
+
import type { VideoClip, VideoEditorController, VideoEffectEditResult } from '@pixerate/editor/video-editor';
|
|
4
|
+
import { videoEditorStateOf } from './createVideoEditor.svelte.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
controller: VideoEditorController;
|
|
8
|
+
clip: VideoClip;
|
|
9
|
+
instance: string;
|
|
10
|
+
provider: EffectProvider;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
report: (r: VideoEffectEditResult | Promise<VideoEffectEditResult>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { controller, clip, instance, provider, disabled, report }: Props = $props();
|
|
16
|
+
const view = videoEditorStateOf(() => controller);
|
|
17
|
+
const drawing = $derived(view.current.pathDrawing?.clip === clip.id && view.current.pathDrawing.instance === instance);
|
|
18
|
+
let words = $state('');
|
|
19
|
+
const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<!-- Where an effect's path comes from, for an effect that draws along one: drawn on the preview, a mask outline, words, or none. -->
|
|
23
|
+
<div class="flex flex-wrap items-center gap-1" data-effect-part="path">
|
|
24
|
+
<span class="text-muted-foreground">Path</span>
|
|
25
|
+
<button type="button" class={btn} {disabled} aria-pressed={drawing} title="Draw the path on the preview" onclick={() => (drawing ? controller.cancelPathDrawing() : controller.startPathDrawing({ clip: clip.id, instance }))}>{drawing ? 'Drawing… (Esc)' : 'Draw path'}</button>
|
|
26
|
+
<button type="button" class={btn} disabled={disabled || !clip.mask} title={clip.mask ? "Run it along this clip's mask outline" : 'Give the clip a mask first'} onclick={() => report(controller.setEffectPath(clip.id, instance, { mask: { clip: clip.id } }))}>Use mask outline</button>
|
|
27
|
+
{#if provider.pathFromText}
|
|
28
|
+
<input class="w-24 rounded border border-border bg-transparent px-1 py-0.5" aria-label="Words to write along the path" placeholder="Words" bind:value={words} {disabled} />
|
|
29
|
+
<button type="button" class={btn} disabled={disabled || !words.trim()} onclick={() => report(controller.setEffectPath(clip.id, instance, { text: words }))}>Write</button>
|
|
30
|
+
{/if}
|
|
31
|
+
<button type="button" class={btn} {disabled} onclick={() => report(controller.setEffectPath(clip.id, instance, { d: '' }))}>Clear path</button>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EffectProvider } from '@pixerate/editor/effects';
|
|
2
|
+
import type { VideoClip, VideoEditorController, VideoEffectEditResult } from '@pixerate/editor/video-editor';
|
|
3
|
+
interface Props {
|
|
4
|
+
controller: VideoEditorController;
|
|
5
|
+
clip: VideoClip;
|
|
6
|
+
instance: string;
|
|
7
|
+
provider: EffectProvider;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
report: (r: VideoEffectEditResult | Promise<VideoEffectEditResult>) => void;
|
|
10
|
+
}
|
|
11
|
+
declare const PathControls: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type PathControls = ReturnType<typeof PathControls>;
|
|
13
|
+
export default PathControls;
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
{disabled}
|
|
93
93
|
onchange={(k, v, g) => report(controller.updateEffectValues(c.id, 'generator', { [k]: v } as VideoEffectValues, merge(g)))}
|
|
94
94
|
onpreset={(values) => report(controller.updateEffectValues(c.id, 'generator', values as VideoEffectValues))}
|
|
95
|
+
path={{ controller, clip: c, instance: 'generator', report }}
|
|
95
96
|
/>
|
|
96
97
|
</div>
|
|
97
98
|
{/if}
|
|
@@ -165,6 +166,7 @@
|
|
|
165
166
|
{disabled}
|
|
166
167
|
onchange={(k, v, g) => report(controller.updateClipEffect(clip!.id, e.id, { values: { [k]: v as never } }, merge(g)))}
|
|
167
168
|
onpreset={(values) => report(controller.updateClipEffect(clip!.id, e.id, { values: values as never }))}
|
|
169
|
+
path={{ controller, clip: clip!, instance: e.id, report }}
|
|
168
170
|
/>
|
|
169
171
|
</div>
|
|
170
172
|
{/if}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import { formatVideoTimecode, VideoPreviewEngine, type VideoEditorController } from '@pixerate/editor/video-editor';
|
|
3
|
+
import { formatVideoTimecode, VideoPathGesture, VideoPreviewEngine, type VideoEditorController } from '@pixerate/editor/video-editor';
|
|
4
4
|
import { videoEditorStateOf } from './createVideoEditor.svelte.js';
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
@@ -24,13 +24,50 @@
|
|
|
24
24
|
const engine = untrack(() => new VideoPreviewEngine(source, el, options));
|
|
25
25
|
return () => engine.destroy();
|
|
26
26
|
});
|
|
27
|
+
// Drawing a path for an effect (the effects panel's Draw path): the preview takes the pointer.
|
|
28
|
+
const pathDrawing = $derived(view.current.pathDrawing);
|
|
29
|
+
const gesture = $derived(new VideoPathGesture({ width: project.width, height: project.height }));
|
|
30
|
+
let drawn = $state<number[]>([]);
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (!pathDrawing) return;
|
|
33
|
+
const c = controller;
|
|
34
|
+
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && c.cancelPathDrawing();
|
|
35
|
+
window.addEventListener('keydown', onKey);
|
|
36
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
37
|
+
});
|
|
27
38
|
</script>
|
|
28
39
|
|
|
29
40
|
<!-- The frame at the playhead, drawn on a canvas with the project's aspect ratio; media plays in step with the timeline. -->
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
<div style="position: relative">
|
|
42
|
+
<canvas
|
|
43
|
+
bind:this={canvas}
|
|
44
|
+
class="block w-full bg-black {className}"
|
|
45
|
+
style="aspect-ratio:{project.width} / {project.height};{style}"
|
|
46
|
+
data-video-preview=""
|
|
47
|
+
aria-label="{project.name ?? 'Video'} at {formatVideoTimecode(view.current.time, project.fps)}"
|
|
48
|
+
></canvas>
|
|
49
|
+
{#if pathDrawing}
|
|
50
|
+
<svg
|
|
51
|
+
viewBox="0 0 {project.width} {project.height}"
|
|
52
|
+
style="position: absolute; inset: 0; width: 100%; height: 100%; cursor: crosshair; touch-action: none"
|
|
53
|
+
role="application"
|
|
54
|
+
aria-label="Draw the effect's path"
|
|
55
|
+
data-video-path-drawing=""
|
|
56
|
+
onpointerdown={(e) => {
|
|
57
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
58
|
+
gesture.down(e, e.currentTarget.getBoundingClientRect());
|
|
59
|
+
drawn = [...gesture.points];
|
|
60
|
+
}}
|
|
61
|
+
onpointermove={(e) => {
|
|
62
|
+
if (gesture.move(e, e.currentTarget.getBoundingClientRect())) drawn = [...gesture.points];
|
|
63
|
+
}}
|
|
64
|
+
onpointerup={() => {
|
|
65
|
+
const points = gesture.up();
|
|
66
|
+
drawn = [];
|
|
67
|
+
if (points) void controller.finishPathDrawing(points);
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<polyline points={drawn.join(' ')} fill="none" stroke="#22d3ee" stroke-width={Math.max(2, project.width / 300)} stroke-linecap="round" stroke-linejoin="round" />
|
|
71
|
+
</svg>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.179.0",
|
|
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.
|
|
168
|
+
"@pixerate/editor": "0.242.0"
|
|
169
169
|
},
|
|
170
170
|
"peerDependencies": {
|
|
171
171
|
"@dagrejs/dagre": "^1.1.4",
|