@pixerate/editor-svelte 0.154.0 → 0.156.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/image-editor/ContentAwareFillWorkspace.svelte +10 -2
- package/dist/image-editor/ContentAwareFillWorkspace.svelte.d.ts +1 -1
- package/dist/image-editor/ImageEditor.svelte +2 -0
- package/dist/image-editor/PathsPanel.svelte +149 -0
- package/dist/image-editor/PathsPanel.svelte.d.ts +17 -0
- package/dist/image-editor/index.d.ts +3 -0
- package/dist/image-editor/index.js +1 -0
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
interface Props {
|
|
17
17
|
controller: ImageEditorController;
|
|
18
18
|
/** Called with the result after OK, or `null` after Cancel. */
|
|
19
|
-
onclose: (result: { layerId: string; bounds: CropBox; output: 'current' | 'new-layer' } | null) => void;
|
|
19
|
+
onclose: (result: { layerId: string; bounds: CropBox; output: 'current' | 'new-layer' | 'duplicate-layer' } | null) => void;
|
|
20
20
|
class?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
void tick;
|
|
54
54
|
return session.computing;
|
|
55
55
|
});
|
|
56
|
+
const wholeImage = $derived.by(() => {
|
|
57
|
+
void tick;
|
|
58
|
+
return session.showsWholeImage;
|
|
59
|
+
});
|
|
56
60
|
const sessionError = $derived.by(() => {
|
|
57
61
|
void tick;
|
|
58
62
|
return session.error;
|
|
@@ -164,6 +168,9 @@
|
|
|
164
168
|
</div>
|
|
165
169
|
{/if}
|
|
166
170
|
<span class="font-mono text-muted-foreground">{Math.round(zoom * 100)}%</span>
|
|
171
|
+
{#if !wholeImage}
|
|
172
|
+
<button type="button" class={button} title="Show (and sample from) more of the image around the selection" onclick={() => session.expandView()}>Expand View</button>
|
|
173
|
+
{/if}
|
|
167
174
|
{#if computing}
|
|
168
175
|
<span role="status" class="text-muted-foreground">Updating preview…</span>
|
|
169
176
|
{/if}
|
|
@@ -250,8 +257,9 @@
|
|
|
250
257
|
options={[
|
|
251
258
|
{ value: 'new-layer', label: 'New Layer' },
|
|
252
259
|
{ value: 'current', label: 'Current Layer' },
|
|
260
|
+
{ value: 'duplicate-layer', label: 'Duplicate Layer' },
|
|
253
261
|
]}
|
|
254
|
-
onchange={(v) => set({ output: v as 'current' | 'new-layer' })}
|
|
262
|
+
onchange={(v) => set({ output: v as 'current' | 'new-layer' | 'duplicate-layer' })}
|
|
255
263
|
/>
|
|
256
264
|
</label>
|
|
257
265
|
{#if error ?? sessionError}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import SelectionOptionsBar from './SelectionOptionsBar.svelte';
|
|
36
36
|
import RefineEdgePanel from './RefineEdgePanel.svelte';
|
|
37
37
|
import ChannelsPanel from './ChannelsPanel.svelte';
|
|
38
|
+
import PathsPanel from './PathsPanel.svelte';
|
|
38
39
|
import FreeTransformOptionsBar from './FreeTransformOptionsBar.svelte';
|
|
39
40
|
import ShapeOptionsBar from './ShapeOptionsBar.svelte';
|
|
40
41
|
import TypeOptionsBar from './TypeOptionsBar.svelte';
|
|
@@ -980,6 +981,7 @@
|
|
|
980
981
|
layers={editor.state.adjustments.layers}
|
|
981
982
|
/>
|
|
982
983
|
<ChannelsPanel controller={editor.controller} />
|
|
984
|
+
<PathsPanel controller={editor.controller} />
|
|
983
985
|
</div>
|
|
984
986
|
{/if}
|
|
985
987
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { imageSectionTitleClass } from './styles.js';
|
|
3
|
+
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
4
|
+
import { pathSvgData, selectionModeFromModifiers } from '@pixerate/editor/image-editor';
|
|
5
|
+
import { untrack } from 'svelte';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Paths panel: the document's saved paths and its Work Path (in italics) with outline
|
|
9
|
+
* thumbnails. Click selects a path, double-click renames it (renaming the Work Path saves it).
|
|
10
|
+
* Load as Selection (Shift adds, Alt subtracts), Make Work Path from the selection, Fill and
|
|
11
|
+
* Stroke on the active layer with a colour and width, Add Vector Mask to the active layer,
|
|
12
|
+
* Duplicate and Delete. Every change is one undo step on the controller.
|
|
13
|
+
*/
|
|
14
|
+
interface Props {
|
|
15
|
+
controller: ImageEditorController;
|
|
16
|
+
/** Thumbnail size in CSS pixels (default 32). */
|
|
17
|
+
thumbnailSize?: number;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { controller, thumbnailSize = 32, class: className = '' }: Props = $props();
|
|
22
|
+
|
|
23
|
+
let tick = $state(0);
|
|
24
|
+
$effect(() => {
|
|
25
|
+
const c = controller;
|
|
26
|
+
return untrack(() => c.subscribe(() => tick++));
|
|
27
|
+
});
|
|
28
|
+
let renaming = $state<string | null>(null);
|
|
29
|
+
let color = $state('#000000');
|
|
30
|
+
let width = $state(2);
|
|
31
|
+
let error = $state<string | null>(null);
|
|
32
|
+
const paths = $derived.by(() => {
|
|
33
|
+
void tick;
|
|
34
|
+
return controller.getPaths();
|
|
35
|
+
});
|
|
36
|
+
const activeId = $derived.by(() => {
|
|
37
|
+
void tick;
|
|
38
|
+
return controller.getState().activePathId ?? null;
|
|
39
|
+
});
|
|
40
|
+
const active = $derived(paths.find((p) => p.id === activeId) ?? null);
|
|
41
|
+
const hasSelection = $derived.by(() => {
|
|
42
|
+
void tick;
|
|
43
|
+
return Boolean(controller.getSelection());
|
|
44
|
+
});
|
|
45
|
+
const size = $derived.by(() => {
|
|
46
|
+
void tick;
|
|
47
|
+
return controller.getState().imageDimensions;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function attempt(fn: () => unknown) {
|
|
51
|
+
error = null;
|
|
52
|
+
try {
|
|
53
|
+
fn();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error = err instanceof Error ? err.message : String(err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const btn = (on = false) =>
|
|
60
|
+
`px-2 py-1 rounded-md border text-[11px] transition-colors disabled:opacity-40 ${
|
|
61
|
+
on ? 'bg-primary border-primary text-primary-foreground' : 'bg-background border-border text-foreground hover:bg-accent hover:text-accent-foreground'
|
|
62
|
+
}`;
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<div data-testid="paths-panel" class="flex flex-col gap-2 text-xs {className}">
|
|
66
|
+
<span class={imageSectionTitleClass}>Paths</span>
|
|
67
|
+
{#if paths.length === 0}
|
|
68
|
+
<p class="text-muted-foreground">No paths. Make a work path from a selection.</p>
|
|
69
|
+
{/if}
|
|
70
|
+
<ul role="list" class="flex flex-col gap-0.5">
|
|
71
|
+
{#each paths as p (p.id)}
|
|
72
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
73
|
+
<li
|
|
74
|
+
data-testid="path-row"
|
|
75
|
+
aria-current={p.id === activeId ? 'true' : undefined}
|
|
76
|
+
class="flex items-center gap-1.5 px-1.5 py-1 rounded cursor-pointer select-none {p.id === activeId ? 'bg-primary/30 ring-1 ring-primary' : 'hover:bg-muted'}"
|
|
77
|
+
onclick={() => attempt(() => controller.setActivePath(p.id))}
|
|
78
|
+
ondblclick={() => (renaming = p.id)}
|
|
79
|
+
>
|
|
80
|
+
<svg aria-hidden="true" viewBox="0 0 {size.width || 1} {size.height || 1}" style:width="{thumbnailSize}px" style:height="{thumbnailSize}px" class="shrink-0 rounded ring-1 ring-border bg-background">
|
|
81
|
+
<path d={pathSvgData(p)} fill="currentColor" fill-opacity="0.25" stroke="currentColor" stroke-width={Math.max(size.width, size.height) / 64} fill-rule={p.fillRule ?? 'nonzero'} />
|
|
82
|
+
</svg>
|
|
83
|
+
{#if renaming === p.id}
|
|
84
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
85
|
+
<input
|
|
86
|
+
autofocus
|
|
87
|
+
aria-label="Path name"
|
|
88
|
+
value={p.work ? '' : p.name}
|
|
89
|
+
placeholder="Path name"
|
|
90
|
+
class="min-w-0 flex-1 bg-background border border-border rounded px-1 py-0.5 text-foreground"
|
|
91
|
+
onclick={(e) => e.stopPropagation()}
|
|
92
|
+
onblur={(e) => {
|
|
93
|
+
renaming = null;
|
|
94
|
+
const name = (e.currentTarget as HTMLInputElement).value.trim();
|
|
95
|
+
if (name && name !== p.name) attempt(() => controller.renamePath(p.id, name));
|
|
96
|
+
}}
|
|
97
|
+
onkeydown={(e) => {
|
|
98
|
+
if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
|
|
99
|
+
if (e.key === 'Escape') renaming = null;
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
{:else}
|
|
103
|
+
<span class="min-w-0 flex-1 truncate {p.work ? 'italic' : ''}">{p.name}</span>
|
|
104
|
+
{/if}
|
|
105
|
+
</li>
|
|
106
|
+
{/each}
|
|
107
|
+
</ul>
|
|
108
|
+
<div class="flex flex-wrap gap-1">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
class={btn()}
|
|
112
|
+
disabled={!active}
|
|
113
|
+
title="Load the path as the selection (Shift adds, Alt subtracts)"
|
|
114
|
+
onclick={(e) => attempt(() => controller.loadPathAsSelection(active!.id, { mode: selectionModeFromModifiers(e, 'replace') }))}
|
|
115
|
+
>
|
|
116
|
+
Load as Selection
|
|
117
|
+
</button>
|
|
118
|
+
<button type="button" class={btn()} disabled={!hasSelection} onclick={() => attempt(() => controller.makeWorkPath())}>Make Work Path</button>
|
|
119
|
+
{#if active?.work}
|
|
120
|
+
<button type="button" class={btn()} onclick={() => (renaming = active.id)}>Save Path</button>
|
|
121
|
+
{/if}
|
|
122
|
+
<button type="button" class={btn()} disabled={!active} onclick={() => attempt(() => controller.addVectorMaskFromPath(active!.id))}>Add Vector Mask</button>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
125
|
+
<input type="color" aria-label="Path paint colour" bind:value={color} class="h-6 w-8 bg-background border border-border rounded" />
|
|
126
|
+
<button type="button" class={btn()} disabled={!active} onclick={() => attempt(() => controller.fillPath(active!.id, { color }))}>Fill Path</button>
|
|
127
|
+
<label class="flex items-center gap-1 text-muted-foreground">
|
|
128
|
+
<input
|
|
129
|
+
type="number"
|
|
130
|
+
min="0.5"
|
|
131
|
+
max="250"
|
|
132
|
+
step="0.5"
|
|
133
|
+
aria-label="Stroke width"
|
|
134
|
+
value={width}
|
|
135
|
+
oninput={(e) => (width = Math.min(250, Math.max(0.5, Number((e.currentTarget as HTMLInputElement).value) || 1)))}
|
|
136
|
+
class="w-12 bg-background border border-border rounded px-1 py-0.5 text-foreground"
|
|
137
|
+
/>
|
|
138
|
+
px
|
|
139
|
+
</label>
|
|
140
|
+
<button type="button" class={btn()} disabled={!active} onclick={() => attempt(() => controller.strokePath(active!.id, { width, color }))}>Stroke Path</button>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="flex flex-wrap gap-1">
|
|
143
|
+
<button type="button" class={btn()} disabled={!active} onclick={() => attempt(() => controller.duplicatePath(active!.id))}>Duplicate</button>
|
|
144
|
+
<button type="button" class={btn()} disabled={!active} onclick={() => attempt(() => controller.deletePath(active!.id))}>Delete</button>
|
|
145
|
+
</div>
|
|
146
|
+
{#if error}
|
|
147
|
+
<p role="alert" class="text-destructive">{error}</p>
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
|
+
/**
|
|
3
|
+
* The Paths panel: the document's saved paths and its Work Path (in italics) with outline
|
|
4
|
+
* thumbnails. Click selects a path, double-click renames it (renaming the Work Path saves it).
|
|
5
|
+
* Load as Selection (Shift adds, Alt subtracts), Make Work Path from the selection, Fill and
|
|
6
|
+
* Stroke on the active layer with a colour and width, Add Vector Mask to the active layer,
|
|
7
|
+
* Duplicate and Delete. Every change is one undo step on the controller.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
controller: ImageEditorController;
|
|
11
|
+
/** Thumbnail size in CSS pixels (default 32). */
|
|
12
|
+
thumbnailSize?: number;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const PathsPanel: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type PathsPanel = ReturnType<typeof PathsPanel>;
|
|
17
|
+
export default PathsPanel;
|
|
@@ -8,6 +8,7 @@ import type CurvesEditorComponent from "./CurvesEditor.svelte";
|
|
|
8
8
|
import type ControlSliderComponent from "./ControlSlider.svelte";
|
|
9
9
|
import type SelectionOptionsBarComponent from "./SelectionOptionsBar.svelte";
|
|
10
10
|
import type RefineEdgePanelComponent from "./RefineEdgePanel.svelte";
|
|
11
|
+
import type PathsPanelComponent from "./PathsPanel.svelte";
|
|
11
12
|
import type SelectAndMaskWorkspaceComponent from "./SelectAndMaskWorkspace.svelte";
|
|
12
13
|
import type ContentAwareFillWorkspaceComponent from "./ContentAwareFillWorkspace.svelte";
|
|
13
14
|
import type ChannelsPanelComponent from "./ChannelsPanel.svelte";
|
|
@@ -34,6 +35,7 @@ export { default as CurvesEditor } from './CurvesEditor.svelte';
|
|
|
34
35
|
export { default as ControlSlider } from './ControlSlider.svelte';
|
|
35
36
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
36
37
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
38
|
+
export { default as PathsPanel } from './PathsPanel.svelte';
|
|
37
39
|
export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
|
|
38
40
|
export { default as ContentAwareFillWorkspace } from './ContentAwareFillWorkspace.svelte';
|
|
39
41
|
export { default as ChannelsPanel } from './ChannelsPanel.svelte';
|
|
@@ -62,6 +64,7 @@ export type CurvesEditorProps = ComponentProps<typeof CurvesEditorComponent>;
|
|
|
62
64
|
export type ControlSliderProps = ComponentProps<typeof ControlSliderComponent>;
|
|
63
65
|
export type SelectionOptionsBarProps = ComponentProps<typeof SelectionOptionsBarComponent>;
|
|
64
66
|
export type RefineEdgePanelProps = ComponentProps<typeof RefineEdgePanelComponent>;
|
|
67
|
+
export type PathsPanelProps = ComponentProps<typeof PathsPanelComponent>;
|
|
65
68
|
export type SelectAndMaskWorkspaceProps = ComponentProps<typeof SelectAndMaskWorkspaceComponent>;
|
|
66
69
|
export type ContentAwareFillWorkspaceProps = ComponentProps<typeof ContentAwareFillWorkspaceComponent>;
|
|
67
70
|
export type ChannelsPanelProps = ComponentProps<typeof ChannelsPanelComponent>;
|
|
@@ -10,6 +10,7 @@ export { default as CurvesEditor } from './CurvesEditor.svelte';
|
|
|
10
10
|
export { default as ControlSlider } from './ControlSlider.svelte';
|
|
11
11
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
12
12
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
13
|
+
export { default as PathsPanel } from './PathsPanel.svelte';
|
|
13
14
|
export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
|
|
14
15
|
export { default as ContentAwareFillWorkspace } from './ContentAwareFillWorkspace.svelte';
|
|
15
16
|
export { default as ChannelsPanel } from './ChannelsPanel.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.156.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.212.0"
|
|
169
169
|
},
|
|
170
170
|
"peerDependencies": {
|
|
171
171
|
"@dagrejs/dagre": "^1.1.4",
|