@pixerate/editor-svelte 0.168.2 → 0.170.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/animation-editor/AnimationStage.svelte +6 -2
- package/dist/animation-editor/ShotCameraMoves.svelte +122 -0
- package/dist/animation-editor/ShotCameraMoves.svelte.d.ts +10 -0
- package/dist/animation-editor/ShotInspector.svelte +11 -7
- package/dist/animation-editor/index.d.ts +3 -0
- package/dist/animation-editor/index.js +1 -0
- package/dist/image-editor/BrushOptionsBar.svelte +15 -11
- package/dist/image-editor/BrushOptionsBar.svelte.d.ts +2 -0
- package/dist/image-editor/EdgeColorPanel.svelte +5 -6
- package/dist/image-editor/ImageEditorCanvas.svelte +18 -4
- package/dist/image-editor/ImageEditorCanvas.svelte.d.ts +8 -2
- package/dist/image-editor/LayerPanel.svelte +59 -8
- package/dist/image-editor/LayerPanel.svelte.d.ts +17 -1
- package/dist/image-editor/RefineEdgePanel.svelte +4 -7
- package/dist/image-editor/SelectionOptionsBar.svelte +12 -8
- package/dist/image-editor/SelectionOptionsBar.svelte.d.ts +2 -0
- package/dist/image-editor/SeparateSubjectPanel.svelte +2 -1
- package/dist/image-editor/ShapeOptionsBar.svelte +10 -6
- package/dist/image-editor/ShapeOptionsBar.svelte.d.ts +2 -0
- package/dist/image-editor/canvasTypes.d.ts +1 -1
- package/dist/playback/PresenceLayer.svelte +21 -12
- package/dist/playback/PresenceLayer.svelte.d.ts +7 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { highlightMotionSvg, motionNodeAtElement, type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin, type MotionRecordGestureMode } from '@pixerate/editor/animation-editor';
|
|
2
|
+
import { highlightMotionSvg, motionCameraTransform, motionCameraViewAt, motionNodeAtElement, type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin, type MotionRecordGestureMode } from '@pixerate/editor/animation-editor';
|
|
3
3
|
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
highlight ?? []
|
|
60
60
|
)
|
|
61
61
|
);
|
|
62
|
+
// A camera move's tilt and turn are 3D, so the stage element turns (its roll and blur are in the SVG).
|
|
63
|
+
let stage = $state<HTMLDivElement | null>(null);
|
|
64
|
+
const transform = $derived(motionCameraTransform(motionCameraViewAt(controller.viewScene(), view.current.time, view.current.effectsProvider), stage?.clientWidth || view.current.scene.artwork.width));
|
|
62
65
|
function onclick(event: MouseEvent) {
|
|
63
66
|
onpick?.(motionNodeAtElement(view.current.scene, event.target as Element, event.currentTarget as Element), event);
|
|
64
67
|
}
|
|
@@ -68,7 +71,8 @@
|
|
|
68
71
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
69
72
|
<div
|
|
70
73
|
class="relative overflow-hidden {recording ? 'cursor-grab touch-none' : onpick ? 'cursor-crosshair' : ''} {className}"
|
|
71
|
-
|
|
74
|
+
bind:this={stage}
|
|
75
|
+
style="aspect-ratio:{view.current.scene.artwork.width} / {view.current.scene.artwork.height}{transform ? `; transform:${transform}` : ''}"
|
|
72
76
|
data-animation-stage=""
|
|
73
77
|
data-recording={recording ? '' : undefined}
|
|
74
78
|
role="img"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { motionShotCameraMove, type AnimationEditorController, type MotionCameraMoveResult } from '@pixerate/editor/animation-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 { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
controller: AnimationEditorController;
|
|
10
|
+
/** The shot whose moves to edit. */
|
|
11
|
+
shot: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { controller, shot, class: className = '' }: Props = $props();
|
|
16
|
+
const view = animationEditorStateOf(() => controller);
|
|
17
|
+
const editor = $derived(view.current);
|
|
18
|
+
const current = $derived(editor.scene.shots.find((s) => s.id === shot));
|
|
19
|
+
const roles: { role: CameraMoveRole; label: string; none: string }[] = [
|
|
20
|
+
{ role: 'transition', label: 'Move in', none: 'ease in' },
|
|
21
|
+
{ role: 'hold', label: 'While holding', none: 'stay still' }
|
|
22
|
+
];
|
|
23
|
+
const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs';
|
|
24
|
+
|
|
25
|
+
// The provider's camera moves of each role, loaded when the provider changes.
|
|
26
|
+
let moves = $state<Record<CameraMoveRole, EffectSummary[]>>({ transition: [], hold: [] });
|
|
27
|
+
$effect(() => {
|
|
28
|
+
void editor.effectsProvider;
|
|
29
|
+
let live = true;
|
|
30
|
+
void Promise.all([controller.cameraMoves('transition'), controller.cameraMoves('hold')]).then(
|
|
31
|
+
([transition, hold]) => live && (moves = { transition, hold }),
|
|
32
|
+
() => live && (moves = { transition: [], hold: [] })
|
|
33
|
+
);
|
|
34
|
+
return () => (live = false);
|
|
35
|
+
});
|
|
36
|
+
const has = $derived(moves.transition.length + moves.hold.length > 0);
|
|
37
|
+
const named = $derived(current?.camera?.in !== undefined || current?.camera?.during !== undefined);
|
|
38
|
+
const inMove = $derived(motionShotCameraMove(current, 'transition'));
|
|
39
|
+
const holdMove = $derived(motionShotCameraMove(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<MotionCameraMoveResult> | MotionCameraMoveResult) {
|
|
45
|
+
void Promise.resolve(r).then((x) => (message = x.ok ? null : x.error));
|
|
46
|
+
}
|
|
47
|
+
function pick(role: CameraMoveRole, id: string) {
|
|
48
|
+
report(id ? controller.setShotCameraMove(shot, role, id) : controller.clearShotCameraMove(shot, 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 = motionShotCameraMove(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.setShotCameraMove(shot, 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 } = motionShotCameraMove(current, role)?.values ?? {};
|
|
66
|
+
set(role, { values: rest, preset: null });
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<!--
|
|
71
|
+
A shot's camera moves from the effects provider: the move it gets in with and the move it makes
|
|
72
|
+
while it holds, each with its length (a move in), presets and controls. Hidden with the reason
|
|
73
|
+
when the provider has no camera moves. Every change is one undo step (a slider drag is one).
|
|
74
|
+
-->
|
|
75
|
+
{#if current}
|
|
76
|
+
{#if !has}
|
|
77
|
+
{#if named}
|
|
78
|
+
<p class="text-[10px] text-muted-foreground {className}" data-camera-moves="unavailable">
|
|
79
|
+
This shot names camera moves, but {editor.effectsProvider ? `${editor.effectsProvider.name} has none` : 'no effects provider is set'}, so the camera eases instead.
|
|
80
|
+
</p>
|
|
81
|
+
{/if}
|
|
82
|
+
{:else}
|
|
83
|
+
<div class="grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1 {className}" data-camera-moves="">
|
|
84
|
+
{#each roles as { role, label, none } (role)}
|
|
85
|
+
{@const move = role === 'transition' ? inMove : holdMove}
|
|
86
|
+
{@const loaded = role === 'transition' ? inDescription : holdDescription}
|
|
87
|
+
{@const known = moves[role].some((m) => m.id === move?.move)}
|
|
88
|
+
<span class="text-muted-foreground">{label}</span>
|
|
89
|
+
<select class={input} aria-label={label} disabled={editor.readOnly} value={move?.move ?? ''} onchange={(e) => pick(role, e.currentTarget.value)}>
|
|
90
|
+
<option value="">{none}</option>
|
|
91
|
+
{#if move && !known}<option value={move.move}>{move.move} (not in the provider)</option>{/if}
|
|
92
|
+
{#each moves[role] as m (m.id)}
|
|
93
|
+
<option value={m.id}>{m.name}</option>
|
|
94
|
+
{/each}
|
|
95
|
+
</select>
|
|
96
|
+
{#if move && known && !editor.readOnly}
|
|
97
|
+
{#if loaded.error}
|
|
98
|
+
<p class="col-span-2 text-[11px] text-amber-500">{loaded.error}</p>
|
|
99
|
+
{:else if loaded.description}
|
|
100
|
+
<div class="col-span-2 flex flex-col gap-1 border-l border-border pl-2" data-camera-move-controls={role}>
|
|
101
|
+
{#if role === 'transition'}
|
|
102
|
+
<div class="flex items-center gap-1">
|
|
103
|
+
<span class="w-16 text-muted-foreground">Length</span>
|
|
104
|
+
<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)} />
|
|
105
|
+
<span class="text-[10px] text-muted-foreground">ms</span>
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
<EffectParameters
|
|
109
|
+
description={loaded.description}
|
|
110
|
+
values={move.values ?? {}}
|
|
111
|
+
onchange={(k, v, g) => set(role, { values: { ...(move.values ?? {}), [k]: v }, preset: null }, g)}
|
|
112
|
+
onreset={(k) => reset(role, k)}
|
|
113
|
+
onpresetname={(name) => set(role, { preset: name, values: {} })}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
{/if}
|
|
118
|
+
{/each}
|
|
119
|
+
{#if message}<p class="col-span-2 text-[11px] text-destructive" role="alert">{message}</p>{/if}
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AnimationEditorController } from '@pixerate/editor/animation-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: AnimationEditorController;
|
|
4
|
+
/** The shot whose moves to edit. */
|
|
5
|
+
shot: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ShotCameraMoves: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ShotCameraMoves = ReturnType<typeof ShotCameraMoves>;
|
|
10
|
+
export default ShotCameraMoves;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { describeMotionElements, freshShotId, motionCueChoices, motionCueFromKey, motionCueKey, type AnimationEditorController, type MotionEdit } from '@pixerate/editor/animation-editor';
|
|
2
|
+
import { describeMotionElements, freshShotId, motionCameraFramed, motionCueChoices, motionCueFromKey, motionCueKey, type AnimationEditorController, type MotionEdit } from '@pixerate/editor/animation-editor';
|
|
3
3
|
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
4
|
+
import ShotCameraMoves from './ShotCameraMoves.svelte';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
controller: AnimationEditorController;
|
|
@@ -53,8 +54,10 @@
|
|
|
53
54
|
apply(raw.trim() ? { op: 'update_shot', shot: current.id, set: { label: raw.trim() } } : { op: 'update_shot', shot: current.id, remove: ['label'] }, 'Rename shot');
|
|
54
55
|
}
|
|
55
56
|
function setCamera(v: string) {
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
// Its camera moves stay when the framing changes.
|
|
58
|
+
const moves = { ...(current.camera?.in !== undefined ? { in: current.camera.in } : {}), ...(current.camera?.during !== undefined ? { during: current.camera.during } : {}) };
|
|
59
|
+
if (!v) apply(Object.keys(moves).length ? { op: 'update_shot', shot: current.id, set: { camera: moves } } : { op: 'update_shot', shot: current.id, remove: ['camera'] }, 'Clear camera');
|
|
60
|
+
else if (v.startsWith('frame:')) apply({ op: 'update_shot', shot: current.id, set: { camera: { ...moves, frame: v.slice(6), padding: 24 } } }, 'Frame camera');
|
|
58
61
|
}
|
|
59
62
|
function setCue(v: string) {
|
|
60
63
|
const cue = motionCueFromKey(v);
|
|
@@ -63,8 +66,8 @@
|
|
|
63
66
|
</script>
|
|
64
67
|
|
|
65
68
|
<!--
|
|
66
|
-
The selected shot: label, hold, speed, the node the camera frames
|
|
67
|
-
beat or a narration word). Also adds, moves and deletes shots. Every change is one undo step.
|
|
69
|
+
The selected shot: label, hold, speed, the node the camera frames, the cue it starts on (a
|
|
70
|
+
beat or a narration word) and its camera moves (ShotCameraMoves). Also adds, moves and deletes shots. Every change is one undo step.
|
|
68
71
|
-->
|
|
69
72
|
<div class="flex flex-col gap-1.5 text-xs {className}" data-shot-inspector="">
|
|
70
73
|
<div class="flex items-center gap-1">
|
|
@@ -94,9 +97,9 @@
|
|
|
94
97
|
<span class="text-[10px] text-muted-foreground">×</span>
|
|
95
98
|
</div>
|
|
96
99
|
<span class="text-muted-foreground">Camera</span>
|
|
97
|
-
<select class={input} aria-label="Camera frames" disabled={editor.readOnly} value={current.camera ? (frame ? `frame:${frame}` : 'rect') : ''} onchange={(e) => setCamera(e.currentTarget.value)}>
|
|
100
|
+
<select class={input} aria-label="Camera frames" disabled={editor.readOnly} value={current.camera && motionCameraFramed(current.camera) ? (frame ? `frame:${frame}` : 'rect') : ''} onchange={(e) => setCamera(e.currentTarget.value)}>
|
|
98
101
|
<option value="">keep framing</option>
|
|
99
|
-
{#if current.camera && !frame}<option value="rect">a rectangle</option>{/if}
|
|
102
|
+
{#if current.camera && motionCameraFramed(current.camera) && !frame}<option value="rect">a rectangle</option>{/if}
|
|
100
103
|
{#each elements as e (e.id)}
|
|
101
104
|
<option value="frame:{e.id}">frame {e.name ?? e.id}</option>
|
|
102
105
|
{/each}
|
|
@@ -114,6 +117,7 @@
|
|
|
114
117
|
{/if}
|
|
115
118
|
</div>
|
|
116
119
|
{/if}
|
|
120
|
+
{#if current}<ShotCameraMoves {controller} shot={current.id} />{/if}
|
|
117
121
|
{#if message}
|
|
118
122
|
<p class="text-red-500" role="alert" data-edit-error="">{message}</p>
|
|
119
123
|
{/if}
|
|
@@ -7,6 +7,7 @@ import type CommandListComponent from "./CommandList.svelte";
|
|
|
7
7
|
import type CommandInspectorComponent from "./CommandInspector.svelte";
|
|
8
8
|
import type EasePickerComponent from "./EasePicker.svelte";
|
|
9
9
|
import type ShotInspectorComponent from "./ShotInspector.svelte";
|
|
10
|
+
import type ShotCameraMovesComponent from "./ShotCameraMoves.svelte";
|
|
10
11
|
import type SceneSettingsComponent from "./SceneSettings.svelte";
|
|
11
12
|
import type AnimationRecordPanelComponent from "./AnimationRecordPanel.svelte";
|
|
12
13
|
export { default as AnimationEditor } from "./AnimationEditor.svelte";
|
|
@@ -17,6 +18,7 @@ export { default as CommandList } from "./CommandList.svelte";
|
|
|
17
18
|
export { default as CommandInspector } from "./CommandInspector.svelte";
|
|
18
19
|
export { default as EasePicker } from "./EasePicker.svelte";
|
|
19
20
|
export { default as ShotInspector } from "./ShotInspector.svelte";
|
|
21
|
+
export { default as ShotCameraMoves } from "./ShotCameraMoves.svelte";
|
|
20
22
|
export { default as SceneSettings } from "./SceneSettings.svelte";
|
|
21
23
|
export { default as AnimationEffectsPanel } from "./AnimationEffectsPanel.svelte";
|
|
22
24
|
export { default as AnimationRecordPanel } from "./AnimationRecordPanel.svelte";
|
|
@@ -29,5 +31,6 @@ export type CommandListProps = ComponentProps<typeof CommandListComponent>;
|
|
|
29
31
|
export type CommandInspectorProps = ComponentProps<typeof CommandInspectorComponent>;
|
|
30
32
|
export type EasePickerProps = ComponentProps<typeof EasePickerComponent>;
|
|
31
33
|
export type ShotInspectorProps = ComponentProps<typeof ShotInspectorComponent>;
|
|
34
|
+
export type ShotCameraMovesProps = ComponentProps<typeof ShotCameraMovesComponent>;
|
|
32
35
|
export type SceneSettingsProps = ComponentProps<typeof SceneSettingsComponent>;
|
|
33
36
|
export type AnimationRecordPanelProps = ComponentProps<typeof AnimationRecordPanelComponent>;
|
|
@@ -6,6 +6,7 @@ export { default as CommandList } from "./CommandList.svelte";
|
|
|
6
6
|
export { default as CommandInspector } from "./CommandInspector.svelte";
|
|
7
7
|
export { default as EasePicker } from "./EasePicker.svelte";
|
|
8
8
|
export { default as ShotInspector } from "./ShotInspector.svelte";
|
|
9
|
+
export { default as ShotCameraMoves } from "./ShotCameraMoves.svelte";
|
|
9
10
|
export { default as SceneSettings } from "./SceneSettings.svelte";
|
|
10
11
|
export { default as AnimationEffectsPanel } from "./AnimationEffectsPanel.svelte";
|
|
11
12
|
export { default as AnimationRecordPanel } from "./AnimationRecordPanel.svelte";
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
|
|
28
28
|
interface Props {
|
|
29
29
|
controller: ImageEditorController;
|
|
30
|
+
/** The row of paint tool buttons (default true); hide it when the host has its own toolbar. */
|
|
31
|
+
showTools?: boolean;
|
|
30
32
|
class?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
let { controller, class: className = '' }: Props = $props();
|
|
35
|
+
let { controller, showTools = true, class: className = '' }: Props = $props();
|
|
34
36
|
|
|
35
37
|
let tick = $state(0);
|
|
36
38
|
$effect(() => {
|
|
@@ -58,16 +60,18 @@
|
|
|
58
60
|
</script>
|
|
59
61
|
|
|
60
62
|
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="brush-options">
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
{#if showTools}
|
|
64
|
+
<div class="flex flex-wrap gap-1" role="toolbar" aria-label="Paint tools">
|
|
65
|
+
{#each PAINT_TOOLS as t (t.id)}
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
aria-pressed={view.tool === t.id}
|
|
69
|
+
title={t.shortcut ? `${t.label} (${t.shortcut})` : t.label}
|
|
70
|
+
class={imageChipClass(view.tool === t.id)}
|
|
71
|
+
onclick={() => controller.setTool(t.id)}>{t.label}</button>
|
|
72
|
+
{/each}
|
|
73
|
+
</div>
|
|
74
|
+
{/if}
|
|
71
75
|
|
|
72
76
|
<div class="flex items-center gap-2" role="group" aria-label="Colours">
|
|
73
77
|
<label class="flex items-center gap-1">
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
2
|
interface Props {
|
|
3
3
|
controller: ImageEditorController;
|
|
4
|
+
/** The row of paint tool buttons (default true); hide it when the host has its own toolbar. */
|
|
5
|
+
showTools?: boolean;
|
|
4
6
|
class?: string;
|
|
5
7
|
}
|
|
6
8
|
declare const BrushOptionsBar: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
2
3
|
import type { ImageEditorController, ImageLayer } from '@pixerate/editor/image-editor';
|
|
3
4
|
import { untrack } from 'svelte';
|
|
4
5
|
|
|
@@ -71,15 +72,13 @@
|
|
|
71
72
|
</label>
|
|
72
73
|
{/each}
|
|
73
74
|
<label class="flex items-center gap-1.5 text-foreground">
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
aria-label="Fixed cast colour"
|
|
75
|
+
<ImageCheckbox
|
|
76
|
+
label="Fixed cast colour"
|
|
77
77
|
checked={fixed}
|
|
78
|
-
onchange={(
|
|
78
|
+
onchange={(on) => {
|
|
79
79
|
commitNext = true;
|
|
80
|
-
set({ ambientColor:
|
|
80
|
+
set({ ambientColor: on ? '#808080' : 'auto' });
|
|
81
81
|
}}
|
|
82
|
-
class="accent-primary"
|
|
83
82
|
/>
|
|
84
83
|
Fixed cast colour
|
|
85
84
|
{#if fixed}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Annotation, CropBox, CropHandle, ImageEditorFit, ImagePoint } from '@pixerate/editor/image-editor';
|
|
3
|
-
import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
|
|
3
|
+
import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef, PresenceActor } from '@pixerate/editor/tools';
|
|
4
4
|
import {
|
|
5
5
|
canvasToImagePoint,
|
|
6
6
|
imageToCanvasPoint,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
type TransformOverlay,
|
|
39
39
|
type TypeToolOptions,
|
|
40
40
|
} from '@pixerate/editor/image-editor';
|
|
41
|
-
import { untrack } from 'svelte';
|
|
41
|
+
import { untrack, type Snippet } from 'svelte';
|
|
42
42
|
import { trackPresenceDrag } from '@pixerate/editor/tools';
|
|
43
43
|
import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
|
|
44
44
|
import PresenceLayer from '../playback/PresenceLayer.svelte';
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
/**
|
|
52
52
|
* What pointer gestures do: `crop` (crop box and its overlay, in the crop margin), `select`
|
|
53
53
|
* (the selection tools), `paint` (brush, eraser, move and their shortcuts), `annotate`
|
|
54
|
-
* (annotations, shape and type layers, by the active tool)
|
|
54
|
+
* (annotations, shape and type layers, by the active tool), `sample` (the Eyedropper: a click
|
|
55
|
+
* picks the composite's colour as the paint foreground) or `none` (view only). Free
|
|
55
56
|
* Transform works in every mode. Default `none`.
|
|
56
57
|
*/
|
|
57
58
|
mode?: ImageEditorCanvasMode;
|
|
@@ -105,6 +106,10 @@
|
|
|
105
106
|
* does, and the canvas's own gestures pause. Needs `viewportGestures`. Default false.
|
|
106
107
|
*/
|
|
107
108
|
panMode?: boolean;
|
|
109
|
+
/** The Eyedropper picked a colour (`sample` mode); it is already the paint foreground. */
|
|
110
|
+
onsample?: (color: string) => void;
|
|
111
|
+
/** Draws each agent cursor during playback in place of the default arrow and name tag. */
|
|
112
|
+
presenceCursor?: Snippet<[PresenceActor]>;
|
|
108
113
|
class?: string;
|
|
109
114
|
style?: string;
|
|
110
115
|
}
|
|
@@ -131,6 +136,8 @@
|
|
|
131
136
|
open,
|
|
132
137
|
viewportGestures = true,
|
|
133
138
|
panMode = false,
|
|
139
|
+
onsample,
|
|
140
|
+
presenceCursor,
|
|
134
141
|
class: className = '',
|
|
135
142
|
style = '',
|
|
136
143
|
}: Props = $props();
|
|
@@ -715,6 +722,13 @@
|
|
|
715
722
|
if (transformTool.active) return;
|
|
716
723
|
const canvasPt = getCanvasEventPoint(e);
|
|
717
724
|
|
|
725
|
+
if (mode === 'sample') {
|
|
726
|
+
const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
|
|
727
|
+
const color = editor.controller.sampleColor(imgPt, { setForeground: true });
|
|
728
|
+
if (color) onsample?.(color);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
718
732
|
if (mode === 'select') {
|
|
719
733
|
const gesture = selectionGesture;
|
|
720
734
|
const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
|
|
@@ -1145,6 +1159,6 @@
|
|
|
1145
1159
|
{/if}
|
|
1146
1160
|
</div>
|
|
1147
1161
|
{#if playing}
|
|
1148
|
-
<PresenceLayer presence={agentPresence} viewport={docToRoot} clip={false} />
|
|
1162
|
+
<PresenceLayer presence={agentPresence} viewport={docToRoot} clip={false} cursor={presenceCursor} />
|
|
1149
1163
|
{/if}
|
|
1150
1164
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ImageEditorFit } from '@pixerate/editor/image-editor';
|
|
2
|
-
import type { AgentPlayback, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
|
|
2
|
+
import type { AgentPlayback, PlaybackRect, PlaybackRef, PresenceActor } from '@pixerate/editor/tools';
|
|
3
3
|
import { FreeTransformTool, type SelectionToolOptions, type ShapeToolOptions, type TextEditSession, type TypeToolOptions } from '@pixerate/editor/image-editor';
|
|
4
|
+
import { type Snippet } from 'svelte';
|
|
4
5
|
import type { ReactiveImageEditor } from './imageEditorState.svelte.js';
|
|
5
6
|
import type { ImageEditorCanvasErrorSource, ImageEditorCanvasMode } from './canvasTypes.js';
|
|
6
7
|
interface Props {
|
|
@@ -9,7 +10,8 @@ interface Props {
|
|
|
9
10
|
/**
|
|
10
11
|
* What pointer gestures do: `crop` (crop box and its overlay, in the crop margin), `select`
|
|
11
12
|
* (the selection tools), `paint` (brush, eraser, move and their shortcuts), `annotate`
|
|
12
|
-
* (annotations, shape and type layers, by the active tool)
|
|
13
|
+
* (annotations, shape and type layers, by the active tool), `sample` (the Eyedropper: a click
|
|
14
|
+
* picks the composite's colour as the paint foreground) or `none` (view only). Free
|
|
13
15
|
* Transform works in every mode. Default `none`.
|
|
14
16
|
*/
|
|
15
17
|
mode?: ImageEditorCanvasMode;
|
|
@@ -63,6 +65,10 @@ interface Props {
|
|
|
63
65
|
* does, and the canvas's own gestures pause. Needs `viewportGestures`. Default false.
|
|
64
66
|
*/
|
|
65
67
|
panMode?: boolean;
|
|
68
|
+
/** The Eyedropper picked a colour (`sample` mode); it is already the paint foreground. */
|
|
69
|
+
onsample?: (color: string) => void;
|
|
70
|
+
/** Draws each agent cursor during playback in place of the default arrow and name tag. */
|
|
71
|
+
presenceCursor?: Snippet<[PresenceActor]>;
|
|
66
72
|
class?: string;
|
|
67
73
|
style?: string;
|
|
68
74
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { ImageEditorController, ImageLayer } from '@pixerate/editor/image-editor';
|
|
2
|
+
import type { ImageEditorController, ImageHistogram, ImageLayer } from '@pixerate/editor/image-editor';
|
|
3
3
|
import {
|
|
4
4
|
ADJUSTMENT_LAYER_OPACITY_CONTROL,
|
|
5
5
|
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
6
6
|
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
7
7
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
8
|
+
IMAGE_ADJUSTMENT_CONTROLS,
|
|
9
|
+
readAdjustmentControls,
|
|
10
|
+
type SelectionToolOptions,
|
|
11
|
+
type ShapeToolOptions,
|
|
12
|
+
type TypeToolOptions,
|
|
8
13
|
} from '@pixerate/editor/image-editor';
|
|
9
14
|
import { untrack } from 'svelte';
|
|
10
15
|
import CanvasSizePanel from './CanvasSizePanel.svelte';
|
|
@@ -21,14 +26,38 @@
|
|
|
21
26
|
import PathsPanel from './PathsPanel.svelte';
|
|
22
27
|
import EdgeColorPanel from './EdgeColorPanel.svelte';
|
|
23
28
|
import VectorMaskEdgesPanel from './VectorMaskEdgesPanel.svelte';
|
|
29
|
+
import Histogram from './Histogram.svelte';
|
|
24
30
|
|
|
25
31
|
interface Props {
|
|
26
32
|
controller: ImageEditorController;
|
|
27
33
|
panelId: string;
|
|
34
|
+
/**
|
|
35
|
+
* The Selection panel's tool options. Bind them (with the canvas's `selectionOptions`) so the
|
|
36
|
+
* tool and settings picked here are the ones the canvas selects with; unbound, the panel keeps
|
|
37
|
+
* its own.
|
|
38
|
+
*/
|
|
39
|
+
selectionOptions?: SelectionToolOptions;
|
|
40
|
+
/** The Shape Options panel's options; bind them with the canvas's `shapeOptions`. */
|
|
41
|
+
shapeOptions?: ShapeToolOptions;
|
|
42
|
+
/** The Typography panel's options; bind them with the canvas's `typeOptions`. */
|
|
43
|
+
typeOptions?: TypeToolOptions;
|
|
44
|
+
/**
|
|
45
|
+
* The tool pickers in Brush Options (paint tools), Selection (selection tools) and Shape Options
|
|
46
|
+
* (shape kinds); default true. Hide them when the host's toolbar offers the same choices.
|
|
47
|
+
*/
|
|
48
|
+
showTools?: boolean;
|
|
28
49
|
class?: string;
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
let {
|
|
52
|
+
let {
|
|
53
|
+
controller,
|
|
54
|
+
panelId,
|
|
55
|
+
selectionOptions = $bindable({ ...DEFAULT_SELECTION_TOOL_OPTIONS }),
|
|
56
|
+
shapeOptions = $bindable({ ...DEFAULT_SHAPE_TOOL_OPTIONS }),
|
|
57
|
+
typeOptions = $bindable({ ...DEFAULT_TYPE_TOOL_OPTIONS }),
|
|
58
|
+
showTools = true,
|
|
59
|
+
class: className = '',
|
|
60
|
+
}: Props = $props();
|
|
32
61
|
|
|
33
62
|
let tick = $state(0);
|
|
34
63
|
$effect(() => {
|
|
@@ -41,9 +70,19 @@
|
|
|
41
70
|
void tick;
|
|
42
71
|
return controller.getActiveLayer();
|
|
43
72
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
const adjustments = $derived.by(() => {
|
|
74
|
+
void tick;
|
|
75
|
+
return readAdjustmentControls(controller.getState().adjustments);
|
|
76
|
+
});
|
|
77
|
+
let histogram = $state.raw<ImageHistogram | null>(null);
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (panelId !== 'color') return;
|
|
80
|
+
const c = controller;
|
|
81
|
+
return untrack(() => {
|
|
82
|
+
histogram = c.getHistogram();
|
|
83
|
+
return c.subscribeHistogram((h) => (histogram = h));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
47
86
|
/** Layers whose soft edge takes Edge Colour (decontaminate, despill, cast). */
|
|
48
87
|
const EDGE_COLOR_TYPES = new Set(['pixel', 'shape', 'type', 'smart']);
|
|
49
88
|
</script>
|
|
@@ -86,11 +125,23 @@
|
|
|
86
125
|
{:else if panelId === 'paint'}
|
|
87
126
|
<div class="flex flex-col gap-3">
|
|
88
127
|
<div class="font-medium text-sm text-foreground">Brush & Paint</div>
|
|
89
|
-
<BrushOptionsBar {controller} />
|
|
128
|
+
<BrushOptionsBar {controller} {showTools} />
|
|
90
129
|
</div>
|
|
91
130
|
{:else if panelId === 'effects'}
|
|
92
131
|
<!-- Kept for hosts that still ask for it by id: layerPanels() folds it into 'properties'. -->
|
|
93
132
|
<LayerEffectsPanel {controller} />
|
|
133
|
+
{:else if panelId === 'color'}
|
|
134
|
+
<!-- The colour adjustments (white balance, light, colour) without Add Adjustment Layer, which a host's layers panel offers. -->
|
|
135
|
+
<div class="flex flex-col gap-3 text-xs text-foreground">
|
|
136
|
+
<div class="flex items-center justify-between">
|
|
137
|
+
<span class="font-medium text-sm">Color Adjustments</span>
|
|
138
|
+
<button type="button" class="text-[10px] text-destructive hover:underline" onclick={() => controller.resetAdjustments()}>Reset</button>
|
|
139
|
+
</div>
|
|
140
|
+
{#if histogram}<Histogram {histogram} />{/if}
|
|
141
|
+
{#each IMAGE_ADJUSTMENT_CONTROLS as spec (spec.id)}
|
|
142
|
+
<ControlSlider {spec} value={adjustments[spec.id]} onchange={(v) => controller.setAdjustments({ [spec.id]: v })} />
|
|
143
|
+
{/each}
|
|
144
|
+
</div>
|
|
94
145
|
{:else if panelId === 'adjustment'}
|
|
95
146
|
<AdjustmentLayersPanel {controller} />
|
|
96
147
|
{:else if panelId === 'type'}
|
|
@@ -101,7 +152,7 @@
|
|
|
101
152
|
{:else if panelId === 'shape'}
|
|
102
153
|
<div class="flex flex-col gap-3">
|
|
103
154
|
<div class="font-medium text-sm text-foreground">Shape Options</div>
|
|
104
|
-
<ShapeOptionsBar {controller} options={shapeOptions} onchange={(o) => (shapeOptions = o)} />
|
|
155
|
+
<ShapeOptionsBar {controller} options={shapeOptions} onchange={(o) => (shapeOptions = o)} {showTools} />
|
|
105
156
|
</div>
|
|
106
157
|
{:else if panelId === 'smart-filters'}
|
|
107
158
|
<!-- Kept for hosts that still ask for it by id: smart filters are under 'provider-effects'. -->
|
|
@@ -111,7 +162,7 @@
|
|
|
111
162
|
{:else if panelId === 'selection'}
|
|
112
163
|
<div class="flex flex-col gap-3">
|
|
113
164
|
<div class="font-medium text-sm text-foreground">Selection</div>
|
|
114
|
-
<SelectionOptionsBar {controller} options={selectionOptions} onOptionsChange={(o) => (selectionOptions = o)} />
|
|
165
|
+
<SelectionOptionsBar {controller} options={selectionOptions} onOptionsChange={(o) => (selectionOptions = o)} {showTools} />
|
|
115
166
|
<RefineEdgePanel {controller} />
|
|
116
167
|
</div>
|
|
117
168
|
{:else if panelId === 'channels'}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
|
+
import { type SelectionToolOptions, type ShapeToolOptions, type TypeToolOptions } from '@pixerate/editor/image-editor';
|
|
2
3
|
interface Props {
|
|
3
4
|
controller: ImageEditorController;
|
|
4
5
|
panelId: string;
|
|
6
|
+
/**
|
|
7
|
+
* The Selection panel's tool options. Bind them (with the canvas's `selectionOptions`) so the
|
|
8
|
+
* tool and settings picked here are the ones the canvas selects with; unbound, the panel keeps
|
|
9
|
+
* its own.
|
|
10
|
+
*/
|
|
11
|
+
selectionOptions?: SelectionToolOptions;
|
|
12
|
+
/** The Shape Options panel's options; bind them with the canvas's `shapeOptions`. */
|
|
13
|
+
shapeOptions?: ShapeToolOptions;
|
|
14
|
+
/** The Typography panel's options; bind them with the canvas's `typeOptions`. */
|
|
15
|
+
typeOptions?: TypeToolOptions;
|
|
16
|
+
/**
|
|
17
|
+
* The tool pickers in Brush Options (paint tools), Selection (selection tools) and Shape Options
|
|
18
|
+
* (shape kinds); default true. Hide them when the host's toolbar offers the same choices.
|
|
19
|
+
*/
|
|
20
|
+
showTools?: boolean;
|
|
5
21
|
class?: string;
|
|
6
22
|
}
|
|
7
|
-
declare const LayerPanel: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
declare const LayerPanel: import("svelte").Component<Props, {}, "selectionOptions" | "shapeOptions" | "typeOptions">;
|
|
8
24
|
type LayerPanel = ReturnType<typeof LayerPanel>;
|
|
9
25
|
export default LayerPanel;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
2
3
|
import { imageRangeClass, imageSectionTitleClass } from './styles.js';
|
|
3
4
|
import MaskEdgeCheck from './MaskEdgeCheck.svelte';
|
|
4
5
|
import SelectionChangeCheck from './SelectionChangeCheck.svelte';
|
|
@@ -115,20 +116,16 @@
|
|
|
115
116
|
</label>
|
|
116
117
|
{/each}
|
|
117
118
|
<label class="flex items-center gap-2 text-foreground">
|
|
118
|
-
<
|
|
119
|
-
type="checkbox"
|
|
120
|
-
checked={params.smartRadius}
|
|
121
|
-
onchange={(e) => (params = { ...params, smartRadius: (e.currentTarget as HTMLInputElement).checked })}
|
|
122
|
-
/>
|
|
119
|
+
<ImageCheckbox label="Smart Radius" checked={params.smartRadius} onchange={(on) => (params = { ...params, smartRadius: on })} />
|
|
123
120
|
Smart Radius
|
|
124
121
|
</label>
|
|
125
122
|
<label class="flex items-center gap-2 text-foreground" title="Refine only where the edge has no image edge under it">
|
|
126
|
-
<
|
|
123
|
+
<ImageCheckbox label="Only uncertain areas" checked={onlyUncertain} onchange={(on) => (onlyUncertain = on)} />
|
|
127
124
|
Only uncertain areas
|
|
128
125
|
</label>
|
|
129
126
|
{#if canObject}
|
|
130
127
|
<label class="flex items-center gap-2 text-foreground">
|
|
131
|
-
<
|
|
128
|
+
<ImageCheckbox label="Object aware" checked={objectAware} onchange={(on) => (objectAware = on)} />
|
|
132
129
|
Object aware (segmentation model)
|
|
133
130
|
</label>
|
|
134
131
|
{/if}
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
quickMask?: boolean;
|
|
34
34
|
/** How the canvas shows the selection (`state.selectionDisplay`); wins over `quickMask`. */
|
|
35
35
|
selectionDisplay?: SelectionDisplay;
|
|
36
|
+
/** The tool picker row (default true); hide it when the host's toolbar offers the same choice. */
|
|
37
|
+
showTools?: boolean;
|
|
36
38
|
class?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
let { controller, options, onOptionsChange, selection, quickMask, selectionDisplay, class: className = '' }: Props = $props();
|
|
41
|
+
let { controller, options, onOptionsChange, selection, quickMask, selectionDisplay, showTools = true, class: className = '' }: Props = $props();
|
|
40
42
|
|
|
41
43
|
let tick = $state(0);
|
|
42
44
|
$effect(() => {
|
|
@@ -202,13 +204,15 @@
|
|
|
202
204
|
</script>
|
|
203
205
|
|
|
204
206
|
<div data-testid="selection-options-bar" class="flex flex-col gap-3 text-xs {className}">
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{t.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
{#if showTools}
|
|
208
|
+
<div role="group" aria-label="Selection tool" class="flex flex-wrap gap-1">
|
|
209
|
+
{#each SELECTION_TOOLS as t (t.id)}
|
|
210
|
+
<button type="button" aria-pressed={options.tool === t.id} class={btn(options.tool === t.id)} onclick={() => set({ tool: t.id })}>
|
|
211
|
+
{t.label}
|
|
212
|
+
</button>
|
|
213
|
+
{/each}
|
|
214
|
+
</div>
|
|
215
|
+
{/if}
|
|
212
216
|
<div role="group" aria-label="Selection mode" class="flex flex-wrap gap-1">
|
|
213
217
|
{#each SELECTION_MODES as m (m)}
|
|
214
218
|
<button type="button" aria-pressed={options.mode === m} class={btn(options.mode === m)} onclick={() => set({ mode: m })}>
|
|
@@ -9,6 +9,8 @@ interface Props {
|
|
|
9
9
|
quickMask?: boolean;
|
|
10
10
|
/** How the canvas shows the selection (`state.selectionDisplay`); wins over `quickMask`. */
|
|
11
11
|
selectionDisplay?: SelectionDisplay;
|
|
12
|
+
/** The tool picker row (default true); hide it when the host's toolbar offers the same choice. */
|
|
13
|
+
showTools?: boolean;
|
|
12
14
|
class?: string;
|
|
13
15
|
}
|
|
14
16
|
declare const SelectionOptionsBar: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
2
3
|
import type { ImageEditorController, SelectionMask } from '@pixerate/editor/image-editor';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
px
|
|
64
65
|
</label>
|
|
65
66
|
<label class="flex items-center gap-1 text-foreground">
|
|
66
|
-
<
|
|
67
|
+
<ImageCheckbox label="Fill the background" checked={fill} onchange={(on) => (fill = on)} />
|
|
67
68
|
Fill the background
|
|
68
69
|
</label>
|
|
69
70
|
</div>
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
controller: ImageEditorController;
|
|
17
17
|
options: ShapeToolOptions;
|
|
18
18
|
onchange: (options: ShapeToolOptions) => void;
|
|
19
|
+
/** The tool picker row (default true); hide it when the host's toolbar offers the same choice. */
|
|
20
|
+
showTools?: boolean;
|
|
19
21
|
class?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
let { controller, options, onchange, class: className = '' }: Props = $props();
|
|
24
|
+
let { controller, options, onchange, showTools = true, class: className = '' }: Props = $props();
|
|
23
25
|
|
|
24
26
|
let tick = $state(0);
|
|
25
27
|
$effect(() => {
|
|
@@ -50,11 +52,13 @@
|
|
|
50
52
|
</script>
|
|
51
53
|
|
|
52
54
|
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="shape-options">
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
{#if showTools}
|
|
56
|
+
<div class="flex flex-wrap gap-1" role="toolbar" aria-label="Shape kind">
|
|
57
|
+
{#each LIVE_SHAPE_KINDS as k (k)}
|
|
58
|
+
<button type="button" aria-pressed={options.kind === k} class={imageChipClass(options.kind === k)} onclick={() => onchange({ ...options, kind: k })}>{SHAPE_KIND_LABELS[k]}</button>
|
|
59
|
+
{/each}
|
|
60
|
+
</div>
|
|
61
|
+
{/if}
|
|
58
62
|
<div class="flex flex-wrap items-center gap-3">
|
|
59
63
|
<label class="flex items-center gap-1">
|
|
60
64
|
<ImageCheckbox label="Fill" checked={options.fill !== null} onchange={(on) => set({ fill: on ? '#4f46e5' : null })} />
|
|
@@ -8,6 +8,8 @@ interface Props {
|
|
|
8
8
|
controller: ImageEditorController;
|
|
9
9
|
options: ShapeToolOptions;
|
|
10
10
|
onchange: (options: ShapeToolOptions) => void;
|
|
11
|
+
/** The tool picker row (default true); hide it when the host's toolbar offers the same choice. */
|
|
12
|
+
showTools?: boolean;
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
13
15
|
declare const ShapeOptionsBar: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** What pointer gestures on `ImageEditorCanvas` do (and whether the crop overlay shows). */
|
|
2
|
-
export type ImageEditorCanvasMode = 'crop' | 'select' | 'paint' | 'annotate' | 'transform' | 'none';
|
|
2
|
+
export type ImageEditorCanvasMode = 'crop' | 'select' | 'paint' | 'annotate' | 'transform' | 'sample' | 'none';
|
|
3
3
|
/** Which gesture an `ImageEditorCanvas` `onerror` report comes from. */
|
|
4
4
|
export type ImageEditorCanvasErrorSource = 'paint' | 'select' | 'shape' | 'type' | 'transform' | 'path';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
3
|
-
import type { PresenceStore, PresenceState } from '@pixerate/editor/tools';
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import type { PresenceActor, PresenceStore, PresenceState } from '@pixerate/editor/tools';
|
|
4
4
|
|
|
5
5
|
type Point = { x: number; y: number };
|
|
6
6
|
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
viewport?: { x: number; y: number; zoom: number } | ((point: Point) => Point);
|
|
15
15
|
/** Clip the cursors to the layer's box (default true). `false` lets a cursor visit chrome outside it. */
|
|
16
16
|
clip?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Draws an actor's cursor in place of the default arrow, name tag and note (a host's own
|
|
19
|
+
* presence cursor). It is placed at the actor's point; the focus ring is still drawn.
|
|
20
|
+
*/
|
|
21
|
+
cursor?: Snippet<[PresenceActor]>;
|
|
17
22
|
class?: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
let { presence, viewport, clip = true, class: className = '' }: Props = $props();
|
|
25
|
+
let { presence, viewport, clip = true, cursor, class: className = '' }: Props = $props();
|
|
21
26
|
let view: PresenceState = $state.raw(untrack(() => presence.getState()));
|
|
22
27
|
$effect(() => {
|
|
23
28
|
view = presence.getState();
|
|
@@ -52,15 +57,19 @@
|
|
|
52
57
|
<div data-actor-focus={a.id} style="position: absolute; {ring(a.focus)} border-radius: 8px; border: 2px solid {a.color}; background: {a.color}14; transition: all 160ms ease-out;"></div>
|
|
53
58
|
{/if}
|
|
54
59
|
<div data-actor={a.id} style="position: absolute; left: 0; top: 0; transform: translate({map(a).x}px, {map(a).y}px);">
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
{#if cursor}
|
|
61
|
+
{@render cursor(a)}
|
|
62
|
+
{:else}
|
|
63
|
+
<svg width="18" height="20" viewBox="0 0 12 18" style="display: block; transform-origin: 0 0; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); {a.pressed ? 'transform: scale(0.85);' : ''}">
|
|
64
|
+
<path d="M0,0 L0,14 L4,10.5 L7,17 L9.5,16 L6.5,9.5 L11.5,9.5 Z" style="fill: {a.color}; stroke: #ffffff; stroke-width: 1;" />
|
|
65
|
+
</svg>
|
|
66
|
+
<span style="position: absolute; left: 14px; top: 16px; display: flex; flex-direction: column; align-items: flex-start; gap: 4px;">
|
|
67
|
+
<span style="white-space: nowrap; border-radius: 999px; padding: 1px 7px; font-size: 11px; font-weight: 600; color: #ffffff; background: {a.color};">{a.name}</span>
|
|
68
|
+
{#if a.message}
|
|
69
|
+
<span data-actor-message={a.id} style="width: max-content; max-width: 240px; border-radius: 8px; border-top-left-radius: 0; padding: 4px 8px; font-size: 12px; color: #1e293b; background: #ffffff; box-shadow: 0 4px 12px rgba(15,23,42,.18);">{a.message}</span>
|
|
70
|
+
{/if}
|
|
71
|
+
</span>
|
|
72
|
+
{/if}
|
|
64
73
|
</div>
|
|
65
74
|
{/each}
|
|
66
75
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { PresenceActor, PresenceStore } from '@pixerate/editor/tools';
|
|
2
3
|
type Point = {
|
|
3
4
|
x: number;
|
|
4
5
|
y: number;
|
|
@@ -17,6 +18,11 @@ interface Props {
|
|
|
17
18
|
} | ((point: Point) => Point);
|
|
18
19
|
/** Clip the cursors to the layer's box (default true). `false` lets a cursor visit chrome outside it. */
|
|
19
20
|
clip?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Draws an actor's cursor in place of the default arrow, name tag and note (a host's own
|
|
23
|
+
* presence cursor). It is placed at the actor's point; the focus ring is still drawn.
|
|
24
|
+
*/
|
|
25
|
+
cursor?: Snippet<[PresenceActor]>;
|
|
20
26
|
class?: string;
|
|
21
27
|
}
|
|
22
28
|
declare const PresenceLayer: import("svelte").Component<Props, {}, "">;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.170.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.230.0"
|
|
169
169
|
},
|
|
170
170
|
"peerDependencies": {
|
|
171
171
|
"@dagrejs/dagre": "^1.1.4",
|