@pixerate/editor-svelte 0.67.0 → 0.69.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/BrushOptionsBar.svelte +110 -0
- package/dist/image-editor/BrushOptionsBar.svelte.d.ts +9 -0
- package/dist/image-editor/BrushOptionsBar.svelte.d.ts.map +1 -0
- package/dist/image-editor/ImageEditor.svelte +135 -8
- package/dist/image-editor/ImageEditor.svelte.d.ts +2 -1
- package/dist/image-editor/ImageEditor.svelte.d.ts.map +1 -1
- package/dist/image-editor/LayersPanel.svelte +394 -0
- package/dist/image-editor/LayersPanel.svelte.d.ts +15 -0
- package/dist/image-editor/LayersPanel.svelte.d.ts.map +1 -0
- package/dist/image-editor/SelectionOptionsBar.svelte +12 -0
- package/dist/image-editor/SelectionOptionsBar.svelte.d.ts.map +1 -1
- package/dist/image-editor/imageEditorState.svelte.d.ts +6 -3
- package/dist/image-editor/imageEditorState.svelte.d.ts.map +1 -1
- package/dist/image-editor/imageEditorState.svelte.js +9 -4
- package/dist/image-editor/index.d.ts +6 -0
- package/dist/image-editor/index.d.ts.map +1 -1
- package/dist/image-editor/index.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
3
|
+
import { BRUSH_OPTION_CONTROLS, PAINT_TOOLS } from '@pixerate/editor/image-editor';
|
|
4
|
+
import { untrack } from 'svelte';
|
|
5
|
+
import ControlSlider from './ControlSlider.svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
controller: ImageEditorController;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { controller, class: className = '' }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let tick = $state(0);
|
|
15
|
+
$effect(() => {
|
|
16
|
+
const c = controller;
|
|
17
|
+
// `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
|
|
18
|
+
return untrack(() => c.subscribe(() => tick++));
|
|
19
|
+
});
|
|
20
|
+
const view = $derived.by(() => {
|
|
21
|
+
void tick;
|
|
22
|
+
const s = controller.getState();
|
|
23
|
+
return { tool: s.activeTool, ...s.paint };
|
|
24
|
+
});
|
|
25
|
+
let error = $state<string | null>(null);
|
|
26
|
+
|
|
27
|
+
const chip = (on: boolean) => `px-2 py-1 rounded text-[11px] ${on ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-300 hover:text-white'}`;
|
|
28
|
+
|
|
29
|
+
function attempt(fn: () => unknown) {
|
|
30
|
+
try {
|
|
31
|
+
error = null;
|
|
32
|
+
fn();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
error = err instanceof Error ? err.message : String(err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="flex flex-col gap-2 text-xs text-slate-200 {className}" data-testid="brush-options">
|
|
40
|
+
<div class="flex gap-1" role="toolbar" aria-label="Paint tools">
|
|
41
|
+
{#each PAINT_TOOLS as t (t.id)}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
aria-pressed={view.tool === t.id}
|
|
45
|
+
title={`${t.label} (${t.shortcut})`}
|
|
46
|
+
class={chip(view.tool === t.id)}
|
|
47
|
+
onclick={() => controller.setTool(t.id)}>{t.label}</button>
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="flex items-center gap-2" role="group" aria-label="Colours">
|
|
52
|
+
<label class="flex items-center gap-1">
|
|
53
|
+
<input
|
|
54
|
+
type="color"
|
|
55
|
+
aria-label="Foreground colour"
|
|
56
|
+
value={view.foreground}
|
|
57
|
+
oninput={(e) => controller.setPaintOptions({ foreground: (e.target as HTMLInputElement).value })}
|
|
58
|
+
class="h-6 w-8 rounded border border-slate-700 bg-transparent"
|
|
59
|
+
/>
|
|
60
|
+
<span class="text-slate-400">FG</span>
|
|
61
|
+
</label>
|
|
62
|
+
<label class="flex items-center gap-1">
|
|
63
|
+
<input
|
|
64
|
+
type="color"
|
|
65
|
+
aria-label="Background colour"
|
|
66
|
+
value={view.background}
|
|
67
|
+
oninput={(e) => controller.setPaintOptions({ background: (e.target as HTMLInputElement).value })}
|
|
68
|
+
class="h-6 w-8 rounded border border-slate-700 bg-transparent"
|
|
69
|
+
/>
|
|
70
|
+
<span class="text-slate-400">BG</span>
|
|
71
|
+
</label>
|
|
72
|
+
<button type="button" title="Swap colours (X)" class={chip(false)} onclick={() => controller.swapPaintColors()}>Swap</button>
|
|
73
|
+
<button type="button" title="Default colours (D)" class={chip(false)} onclick={() => controller.resetPaintColors()}>Default</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{#each BRUSH_OPTION_CONTROLS as spec (spec.id)}
|
|
77
|
+
<ControlSlider {spec} value={view.brush[spec.id]} onchange={(v) => controller.setPaintOptions({ brush: { [spec.id]: v } })} />
|
|
78
|
+
{/each}
|
|
79
|
+
|
|
80
|
+
<div class="flex flex-wrap gap-3 text-slate-300" role="group" aria-label="Pressure">
|
|
81
|
+
<label class="flex items-center gap-1.5">
|
|
82
|
+
<input
|
|
83
|
+
type="checkbox"
|
|
84
|
+
aria-label="Pressure controls size"
|
|
85
|
+
checked={view.brush.pressureSize}
|
|
86
|
+
onchange={(e) => controller.setPaintOptions({ brush: { pressureSize: (e.target as HTMLInputElement).checked } })}
|
|
87
|
+
class="accent-indigo-500"
|
|
88
|
+
/>
|
|
89
|
+
Pressure size
|
|
90
|
+
</label>
|
|
91
|
+
<label class="flex items-center gap-1.5">
|
|
92
|
+
<input
|
|
93
|
+
type="checkbox"
|
|
94
|
+
aria-label="Pressure controls opacity"
|
|
95
|
+
checked={view.brush.pressureOpacity}
|
|
96
|
+
onchange={(e) => controller.setPaintOptions({ brush: { pressureOpacity: (e.target as HTMLInputElement).checked } })}
|
|
97
|
+
class="accent-indigo-500"
|
|
98
|
+
/>
|
|
99
|
+
Pressure opacity
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="flex gap-1">
|
|
104
|
+
<button type="button" class={chip(false)} onclick={() => attempt(() => controller.fillPixels())}>Fill</button>
|
|
105
|
+
<button type="button" class={chip(false)} onclick={() => attempt(() => controller.clearPixels())}>Clear</button>
|
|
106
|
+
</div>
|
|
107
|
+
{#if error}
|
|
108
|
+
<p role="alert" class="text-rose-400">{error}</p>
|
|
109
|
+
{/if}
|
|
110
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: ImageEditorController;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const BrushOptionsBar: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type BrushOptionsBar = ReturnType<typeof BrushOptionsBar>;
|
|
8
|
+
export default BrushOptionsBar;
|
|
9
|
+
//# sourceMappingURL=BrushOptionsBar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrushOptionsBar.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/BrushOptionsBar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAMzE,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiFH,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
InpaintHook,
|
|
10
10
|
RemoveBackgroundHook,
|
|
11
11
|
ImageEditorAIHooks,
|
|
12
|
+
ImageEditorFit,
|
|
12
13
|
} from '@pixerate/editor/image-editor';
|
|
13
14
|
import {
|
|
14
15
|
canvasToImagePoint,
|
|
@@ -22,11 +23,16 @@
|
|
|
22
23
|
readAdjustmentControls,
|
|
23
24
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
24
25
|
SelectionGesture,
|
|
26
|
+
PaintGesture,
|
|
27
|
+
handlePaintShortcut,
|
|
28
|
+
pointerPressure,
|
|
25
29
|
type SelectionToolOptions,
|
|
26
30
|
} from '@pixerate/editor/image-editor';
|
|
27
31
|
import ControlSlider from './ControlSlider.svelte';
|
|
28
32
|
import Histogram from './Histogram.svelte';
|
|
29
33
|
import AdjustmentLayersPanel from './AdjustmentLayersPanel.svelte';
|
|
34
|
+
import LayersPanel from './LayersPanel.svelte';
|
|
35
|
+
import BrushOptionsBar from './BrushOptionsBar.svelte';
|
|
30
36
|
import SelectionOptionsBar from './SelectionOptionsBar.svelte';
|
|
31
37
|
import RefineEdgePanel from './RefineEdgePanel.svelte';
|
|
32
38
|
import { onDestroy, untrack } from 'svelte';
|
|
@@ -42,6 +48,7 @@
|
|
|
42
48
|
onInpaint?: InpaintHook;
|
|
43
49
|
onRemoveBackground?: RemoveBackgroundHook;
|
|
44
50
|
aiHooks?: ImageEditorAIHooks;
|
|
51
|
+
fit?: ImageEditorFit;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
let {
|
|
@@ -54,6 +61,7 @@
|
|
|
54
61
|
onInpaint,
|
|
55
62
|
onRemoveBackground,
|
|
56
63
|
aiHooks,
|
|
64
|
+
fit,
|
|
57
65
|
}: Props = $props();
|
|
58
66
|
|
|
59
67
|
const internalEditor = createReactiveImageEditor();
|
|
@@ -65,11 +73,15 @@
|
|
|
65
73
|
|
|
66
74
|
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
67
75
|
let containerEl = $state<HTMLDivElement | null>(null);
|
|
68
|
-
let activeTab = $state<'crop' | 'adjust' | 'select' | 'annotate' | 'depth' | 'ai'>('crop');
|
|
76
|
+
let activeTab = $state<'crop' | 'adjust' | 'layers' | 'paint' | 'select' | 'annotate' | 'depth' | 'ai'>('crop');
|
|
69
77
|
|
|
70
78
|
// Selection tools (magic wand, marquees, lasso): options bar state and the pointer gesture.
|
|
71
79
|
let selectionOptions = $state<SelectionToolOptions>({ ...DEFAULT_SELECTION_TOOL_OPTIONS });
|
|
72
80
|
const selectionGesture = $derived(new SelectionGesture(editor.controller, () => selectionOptions));
|
|
81
|
+
// Paint tools (Move, Brush, Eraser): the pointer gesture and the brush cursor.
|
|
82
|
+
const paintGesture = $derived(new PaintGesture(editor.controller));
|
|
83
|
+
let brushCursor: ImagePoint | null = null;
|
|
84
|
+
let paintError = $state<string | null>(null);
|
|
73
85
|
let selectionDraft = $state.raw<ImagePoint[] | null>(null);
|
|
74
86
|
let selectionError = $state<string | null>(null);
|
|
75
87
|
let activeCropRatio = $state<number | null>(null);
|
|
@@ -81,6 +93,9 @@
|
|
|
81
93
|
let isInpainting = $state(false);
|
|
82
94
|
let aiError = $state<string | null>(null);
|
|
83
95
|
let aiSuccess = $state<string | null>(null);
|
|
96
|
+
/** Where AI results go: by layer count (auto), a new layer / mask, or replacing the Background. */
|
|
97
|
+
let aiTarget = $state<'auto' | 'new-layer' | 'replace'>('auto');
|
|
98
|
+
const aiOptions = $derived(aiTarget === 'auto' ? {} : { target: aiTarget });
|
|
84
99
|
|
|
85
100
|
const effectiveOnInpaint = $derived(onInpaint || aiHooks?.onInpaint);
|
|
86
101
|
const effectiveOnRemoveBg = $derived(onRemoveBackground || aiHooks?.onRemoveBackground);
|
|
@@ -94,12 +109,12 @@
|
|
|
94
109
|
aiSuccess = null;
|
|
95
110
|
isRemovingBg = true;
|
|
96
111
|
try {
|
|
97
|
-
// AI hooks work
|
|
98
|
-
//
|
|
99
|
-
const imageDataUrl = await editor.
|
|
112
|
+
// AI hooks work in natural pixels (no crop or rotation): the source when the result replaces
|
|
113
|
+
// it, the composite of the layers when the result becomes a layer or mask.
|
|
114
|
+
const imageDataUrl = await editor.controller.toAIInputDataURL(aiOptions);
|
|
100
115
|
const result = await effectiveOnRemoveBg({ imageDataUrl });
|
|
101
116
|
if (typeof result === 'string') {
|
|
102
|
-
await editor.applyBackgroundRemovedImage(result);
|
|
117
|
+
await editor.applyBackgroundRemovedImage(result, aiOptions);
|
|
103
118
|
aiSuccess = 'Background removed successfully!';
|
|
104
119
|
} else {
|
|
105
120
|
aiSuccess = 'Background removal complete.';
|
|
@@ -125,7 +140,7 @@
|
|
|
125
140
|
isInpainting = true;
|
|
126
141
|
try {
|
|
127
142
|
// Source image and mask share natural image space so they line up exactly.
|
|
128
|
-
const imageDataUrl = await editor.
|
|
143
|
+
const imageDataUrl = await editor.controller.toAIInputDataURL(aiOptions);
|
|
129
144
|
const maskDataUrl = await editor.toMaskDataURL({
|
|
130
145
|
useAnnotations: inpaintMaskMode === 'annotations',
|
|
131
146
|
useCrop: inpaintMaskMode === 'crop',
|
|
@@ -137,7 +152,7 @@
|
|
|
137
152
|
cropBox: inpaintMaskMode === 'crop' ? editor.state.crop : null,
|
|
138
153
|
});
|
|
139
154
|
if (typeof result === 'string') {
|
|
140
|
-
await editor.applyInpaintedImage(result);
|
|
155
|
+
await editor.applyInpaintedImage(result, aiOptions);
|
|
141
156
|
aiSuccess = 'Inpainting applied successfully!';
|
|
142
157
|
} else {
|
|
143
158
|
aiSuccess = 'Inpainting complete.';
|
|
@@ -193,15 +208,66 @@
|
|
|
193
208
|
await editor.controller.render(canvasEl, {
|
|
194
209
|
viewportWidth: vpWidth,
|
|
195
210
|
viewportHeight: vpHeight,
|
|
211
|
+
fit,
|
|
196
212
|
showCropOverlay: activeTab === 'crop',
|
|
197
213
|
showSelectionOverlay: true,
|
|
198
214
|
interactive: true,
|
|
199
215
|
draftAnnotation: overrideAnnotation !== undefined ? overrideAnnotation : draftAnnotation,
|
|
200
216
|
draftCrop: overrideCrop !== undefined ? overrideCrop : draftCrop,
|
|
201
217
|
draftSelectionPath: selectionDraft,
|
|
218
|
+
brushCursor:
|
|
219
|
+
activeTab === 'paint' && brushCursor && editor.controller.getState().activeTool !== 'move'
|
|
220
|
+
? { ...brushCursor, diameter: editor.controller.getState().paint.brush.size }
|
|
221
|
+
: null,
|
|
202
222
|
});
|
|
203
223
|
}
|
|
204
224
|
|
|
225
|
+
// Paint tools: pointer events (coalesced samples, pointer capture) into strokes and moves.
|
|
226
|
+
function paintSamples(e: PointerEvent) {
|
|
227
|
+
const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];
|
|
228
|
+
const list = events.length ? events : [e];
|
|
229
|
+
const rect = canvasEl!.getBoundingClientRect();
|
|
230
|
+
const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
|
|
231
|
+
const sy = rect.height > 0 ? canvasEl!.height / rect.height : 1;
|
|
232
|
+
const s = editor.controller.getState();
|
|
233
|
+
return list.map((ev) => ({
|
|
234
|
+
...canvasToImagePoint({ x: (ev.clientX - rect.left) * sx, y: (ev.clientY - rect.top) * sy }, s, canvasEl!.width, canvasEl!.height, false),
|
|
235
|
+
pressure: pointerPressure(ev),
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
function handlePaintPointerDown(e: PointerEvent) {
|
|
239
|
+
if (activeTab !== 'paint' || e.button !== 0 || !canvasEl) return;
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
try {
|
|
242
|
+
canvasEl.setPointerCapture?.(e.pointerId);
|
|
243
|
+
} catch {
|
|
244
|
+
// Synthetic events have no active pointer to capture.
|
|
245
|
+
}
|
|
246
|
+
paintError = paintGesture.down(paintSamples(e)[0]);
|
|
247
|
+
}
|
|
248
|
+
function handlePaintPointerMove(e: PointerEvent) {
|
|
249
|
+
if (activeTab !== 'paint' || !canvasEl) return;
|
|
250
|
+
const samples = paintSamples(e);
|
|
251
|
+
brushCursor = samples[samples.length - 1] ?? null;
|
|
252
|
+
if (paintGesture.active) paintGesture.moveTo(samples);
|
|
253
|
+
else void renderCanvas();
|
|
254
|
+
}
|
|
255
|
+
function handlePaintPointerUp() {
|
|
256
|
+
if (activeTab !== 'paint' || !paintGesture.active) return;
|
|
257
|
+
paintGesture.up();
|
|
258
|
+
if (paintGesture.error) paintError = paintGesture.error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
$effect(() => {
|
|
262
|
+
if (activeTab !== 'paint' || typeof window === 'undefined') return;
|
|
263
|
+
const c = editor.controller;
|
|
264
|
+
const onKey = (e: KeyboardEvent) => {
|
|
265
|
+
if (handlePaintShortcut(c, e)) e.preventDefault();
|
|
266
|
+
};
|
|
267
|
+
window.addEventListener('keydown', onKey);
|
|
268
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
269
|
+
});
|
|
270
|
+
|
|
205
271
|
$effect(() => {
|
|
206
272
|
// Re-render when editor state or activeTab changes
|
|
207
273
|
void editor.state;
|
|
@@ -659,6 +725,30 @@
|
|
|
659
725
|
>
|
|
660
726
|
Adjustments
|
|
661
727
|
</button>
|
|
728
|
+
<button
|
|
729
|
+
type="button"
|
|
730
|
+
onclick={() => {
|
|
731
|
+
activeTab = 'layers';
|
|
732
|
+
editor.setTool('select');
|
|
733
|
+
}}
|
|
734
|
+
class="px-3 py-1.5 rounded-md font-medium transition-colors {activeTab === 'layers'
|
|
735
|
+
? 'bg-indigo-600 text-white shadow'
|
|
736
|
+
: 'text-slate-400 hover:text-slate-200'}"
|
|
737
|
+
>
|
|
738
|
+
Layers
|
|
739
|
+
</button>
|
|
740
|
+
<button
|
|
741
|
+
type="button"
|
|
742
|
+
onclick={() => {
|
|
743
|
+
activeTab = 'paint';
|
|
744
|
+
editor.setTool('brush');
|
|
745
|
+
}}
|
|
746
|
+
class="px-3 py-1.5 rounded-md font-medium transition-colors {activeTab === 'paint'
|
|
747
|
+
? 'bg-indigo-600 text-white shadow'
|
|
748
|
+
: 'text-slate-400 hover:text-slate-200'}"
|
|
749
|
+
>
|
|
750
|
+
Paint
|
|
751
|
+
</button>
|
|
662
752
|
<button
|
|
663
753
|
type="button"
|
|
664
754
|
onclick={() => {
|
|
@@ -798,7 +888,12 @@
|
|
|
798
888
|
onmousemove={handleMouseMove}
|
|
799
889
|
onmouseup={handleMouseUp}
|
|
800
890
|
onmouseleave={handleMouseUp}
|
|
801
|
-
|
|
891
|
+
onpointerdown={handlePaintPointerDown}
|
|
892
|
+
onpointermove={handlePaintPointerMove}
|
|
893
|
+
onpointerup={handlePaintPointerUp}
|
|
894
|
+
onpointercancel={() => paintGesture.cancel()}
|
|
895
|
+
data-testid="image-editor-canvas"
|
|
896
|
+
class="absolute inset-0 w-full h-full block {activeTab === 'paint' ? 'touch-none cursor-crosshair' : ''}"
|
|
802
897
|
></canvas>
|
|
803
898
|
</div>
|
|
804
899
|
|
|
@@ -978,6 +1073,26 @@
|
|
|
978
1073
|
{/if}
|
|
979
1074
|
|
|
980
1075
|
<!-- TAB 3: ANNOTATIONS -->
|
|
1076
|
+
{#if activeTab === 'layers'}
|
|
1077
|
+
<div class="flex flex-col gap-3 text-xs">
|
|
1078
|
+
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Layers</span>
|
|
1079
|
+
<LayersPanel controller={editor.controller} />
|
|
1080
|
+
</div>
|
|
1081
|
+
{/if}
|
|
1082
|
+
|
|
1083
|
+
{#if activeTab === 'paint'}
|
|
1084
|
+
<div class="flex flex-col gap-3 text-xs">
|
|
1085
|
+
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Paint</span>
|
|
1086
|
+
<BrushOptionsBar controller={editor.controller} />
|
|
1087
|
+
{#if paintError}
|
|
1088
|
+
<p role="alert" class="text-rose-400" data-testid="paint-error">{paintError}</p>
|
|
1089
|
+
{/if}
|
|
1090
|
+
<div class="border-t border-slate-700/60 pt-3">
|
|
1091
|
+
<LayersPanel controller={editor.controller} />
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
{/if}
|
|
1095
|
+
|
|
981
1096
|
{#if activeTab === 'annotate'}
|
|
982
1097
|
<div class="flex flex-col gap-3 text-xs">
|
|
983
1098
|
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">
|
|
@@ -1227,6 +1342,18 @@
|
|
|
1227
1342
|
<!-- TAB 5: AI TOOLS / INPAINTING & BACKGROUND REMOVAL -->
|
|
1228
1343
|
{#if activeTab === 'ai'}
|
|
1229
1344
|
<div class="flex flex-col gap-4 text-xs">
|
|
1345
|
+
<label class="flex items-center justify-between gap-2 text-slate-300">
|
|
1346
|
+
<span>Results go to</span>
|
|
1347
|
+
<select
|
|
1348
|
+
aria-label="AI result target"
|
|
1349
|
+
bind:value={aiTarget}
|
|
1350
|
+
class="bg-slate-800 border border-slate-700 rounded px-1.5 py-1"
|
|
1351
|
+
>
|
|
1352
|
+
<option value="auto">{editor.controller.isMultiLayer ? 'Auto (new layer)' : 'Auto (replace image)'}</option>
|
|
1353
|
+
<option value="new-layer">New layer / mask</option>
|
|
1354
|
+
<option value="replace">Replace image</option>
|
|
1355
|
+
</select>
|
|
1356
|
+
</label>
|
|
1230
1357
|
<!-- Background Removal Section -->
|
|
1231
1358
|
<div class="p-3 rounded-lg bg-slate-950/60 border border-slate-800 flex flex-col gap-2.5">
|
|
1232
1359
|
<div class="flex items-center justify-between">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExportOptions, InpaintHook, RemoveBackgroundHook, ImageEditorAIHooks } from '@pixerate/editor/image-editor';
|
|
1
|
+
import type { ExportOptions, InpaintHook, RemoveBackgroundHook, ImageEditorAIHooks, ImageEditorFit } from '@pixerate/editor/image-editor';
|
|
2
2
|
import { type ReactiveImageEditor } from './imageEditorState.svelte.js';
|
|
3
3
|
interface Props {
|
|
4
4
|
editor?: ReactiveImageEditor;
|
|
@@ -10,6 +10,7 @@ interface Props {
|
|
|
10
10
|
onInpaint?: InpaintHook;
|
|
11
11
|
onRemoveBackground?: RemoveBackgroundHook;
|
|
12
12
|
aiHooks?: ImageEditorAIHooks;
|
|
13
|
+
fit?: ImageEditorFit;
|
|
13
14
|
}
|
|
14
15
|
declare const ImageEditor: import("svelte").Component<Props, {}, "">;
|
|
15
16
|
type ImageEditor = ReturnType<typeof ImageEditor>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/ImageEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAIR,aAAa,EAGb,WAAW,EACX,oBAAoB,EACpB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ImageEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/ImageEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAIR,aAAa,EAGb,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,cAAc,EACf,MAAM,+BAA+B,CAAC;AA0BzC,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGjG,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,GAAG,CAAC,EAAE,cAAc,CAAC;CACtB;AAupCH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ImageEditorController, ImageLayer, LayerMenuAction, LayerPanelRow, PixelBuffer } from '@pixerate/editor/image-editor';
|
|
3
|
+
import {
|
|
4
|
+
BLEND_MODES,
|
|
5
|
+
PSD_FILE_ACCEPT,
|
|
6
|
+
downloadPsd,
|
|
7
|
+
dropPlacement,
|
|
8
|
+
dropZone,
|
|
9
|
+
layerBadge,
|
|
10
|
+
layerMenu,
|
|
11
|
+
layerPanelRows,
|
|
12
|
+
openPsdFile,
|
|
13
|
+
runLayerMenuAction,
|
|
14
|
+
} from '@pixerate/editor/image-editor';
|
|
15
|
+
import { untrack } from 'svelte';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
controller: ImageEditorController;
|
|
19
|
+
/** Thumbnail size in px (default 32). */
|
|
20
|
+
thumbnailSize?: number;
|
|
21
|
+
/** Show Open PSD / Export PSD (default true). */
|
|
22
|
+
psd?: boolean;
|
|
23
|
+
/** Base name of exported PSD files (default `image`). */
|
|
24
|
+
psdFileName?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let { controller, thumbnailSize = 32, psd = true, psdFileName = 'image', class: className = '' }: Props = $props();
|
|
29
|
+
|
|
30
|
+
let tick = $state(0);
|
|
31
|
+
$effect(() => {
|
|
32
|
+
const c = controller;
|
|
33
|
+
// `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
|
|
34
|
+
return untrack(() => c.subscribe(() => tick++));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const snapshot = $derived.by(() => {
|
|
38
|
+
void tick;
|
|
39
|
+
const s = controller.getState();
|
|
40
|
+
return {
|
|
41
|
+
layers: s.layers,
|
|
42
|
+
activeLayerId: s.activeLayerId,
|
|
43
|
+
activeTarget: s.activeTarget,
|
|
44
|
+
rows: layerPanelRows(s.layers, s.activeLayerId),
|
|
45
|
+
active: controller.getActiveLayer() ?? null,
|
|
46
|
+
menu: layerMenu(controller),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let error = $state<string | null>(null);
|
|
51
|
+
let renaming = $state<string | null>(null);
|
|
52
|
+
let menu = $state<{ x: number; y: number } | null>(null);
|
|
53
|
+
let dragId = $state<string | null>(null);
|
|
54
|
+
/** Warnings of the last PSD open or export. */
|
|
55
|
+
let psdNotes = $state<string[]>([]);
|
|
56
|
+
let fileInput = $state<HTMLInputElement | null>(null);
|
|
57
|
+
|
|
58
|
+
async function psdTask(fn: () => Promise<string[]>) {
|
|
59
|
+
try {
|
|
60
|
+
error = null;
|
|
61
|
+
psdNotes = await fn();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
error = err instanceof Error ? err.message : String(err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onPsdFile(e: Event) {
|
|
68
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
69
|
+
const file = input.files?.[0];
|
|
70
|
+
input.value = '';
|
|
71
|
+
if (file) psdTask(() => openPsdFile(controller, file));
|
|
72
|
+
}
|
|
73
|
+
/** Whether the next slider change records an undo step (the first of a drag does, the rest replace it). */
|
|
74
|
+
let commitNext = true;
|
|
75
|
+
|
|
76
|
+
const LOCKS = ['pixels', 'transparency', 'position', 'all'] as const;
|
|
77
|
+
const iconButton = 'px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 hover:text-white disabled:opacity-40';
|
|
78
|
+
const rowClass = (active: boolean) =>
|
|
79
|
+
`flex items-center gap-1.5 px-1.5 py-1 rounded cursor-pointer select-none ${active ? 'bg-indigo-600/30 ring-1 ring-indigo-500' : 'hover:bg-slate-800'}`;
|
|
80
|
+
|
|
81
|
+
function attempt(fn: () => unknown) {
|
|
82
|
+
try {
|
|
83
|
+
error = null;
|
|
84
|
+
fn();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
error = err instanceof Error ? err.message : String(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function run(action: LayerMenuAction) {
|
|
91
|
+
menu = null;
|
|
92
|
+
error = await runLayerMenuAction(controller, action);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function prop(key: 'opacity' | 'fillOpacity', value: number) {
|
|
96
|
+
const id = snapshot.active?.id;
|
|
97
|
+
if (!id) return;
|
|
98
|
+
attempt(() => {
|
|
99
|
+
controller.setLayerProperties(id, { [key]: value }, commitNext);
|
|
100
|
+
commitNext = false;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onDrop(e: DragEvent, row: LayerPanelRow) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const id = e.dataTransfer?.getData('text/x-pixerate-layer') || dragId;
|
|
107
|
+
dragId = null;
|
|
108
|
+
if (!id) return;
|
|
109
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
110
|
+
const fraction = rect.height ? (e.clientY - rect.top) / rect.height : 0.5;
|
|
111
|
+
const placement = dropPlacement(snapshot.layers, id, row.layer.id, dropZone(fraction, row.layer.type === 'group'));
|
|
112
|
+
if (placement) attempt(() => controller.moveLayer(id, placement));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const locked = (layer: ImageLayer) => Boolean(layer.locks?.all || layer.locks?.pixels || layer.locks?.position || layer.locks?.transparency);
|
|
116
|
+
const fillOf = (layer: ImageLayer) => (layer.type === 'pixel' ? layer.fillOpacity ?? 1 : 1);
|
|
117
|
+
|
|
118
|
+
/** Draws RGBA pixels into a canvas (nothing without a 2D context). */
|
|
119
|
+
function paint(canvas: HTMLCanvasElement, pixels: PixelBuffer | null) {
|
|
120
|
+
const draw = (px: PixelBuffer | null) => {
|
|
121
|
+
const ctx = canvas.getContext('2d');
|
|
122
|
+
if (!ctx || !px || typeof ctx.createImageData !== 'function') return;
|
|
123
|
+
canvas.width = px.width;
|
|
124
|
+
canvas.height = px.height;
|
|
125
|
+
const img = ctx.createImageData(px.width, px.height);
|
|
126
|
+
img.data.set(px.data as ArrayLike<number>);
|
|
127
|
+
ctx.putImageData(img, 0, 0);
|
|
128
|
+
};
|
|
129
|
+
draw(pixels);
|
|
130
|
+
return { update: draw };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function thumb(layer: ImageLayer): PixelBuffer | null {
|
|
134
|
+
void tick;
|
|
135
|
+
return layer.type === 'pixel' ? controller.getLayerThumbnail(layer.id, { maxSide: 64 }) : null;
|
|
136
|
+
}
|
|
137
|
+
function maskThumb(layer: ImageLayer): PixelBuffer | null {
|
|
138
|
+
void tick;
|
|
139
|
+
return layer.mask && !layer.mask.source ? controller.getLayerThumbnail(layer.id, { maxSide: 64, mask: true }) : null;
|
|
140
|
+
}
|
|
141
|
+
const thumbClass = (selected: boolean) =>
|
|
142
|
+
`shrink-0 rounded bg-slate-800 ${selected ? 'ring-2 ring-white' : 'ring-1 ring-slate-700'}`;
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<svelte:window onclick={() => menu && (menu = null)} />
|
|
146
|
+
|
|
147
|
+
<div class="flex flex-col gap-2 text-xs text-slate-200 {className}" data-testid="layers-panel">
|
|
148
|
+
<div class="flex flex-wrap items-center gap-1" role="toolbar" aria-label="Layer actions">
|
|
149
|
+
<button type="button" class={iconButton} onclick={() => run('new-layer')}>New Layer</button>
|
|
150
|
+
<button type="button" class={iconButton} onclick={() => run('new-group')}>New Group</button>
|
|
151
|
+
<button type="button" class={iconButton} disabled={!snapshot.active} onclick={() => run('duplicate')}>Duplicate</button>
|
|
152
|
+
<button type="button" class={iconButton} disabled={!snapshot.active} onclick={() => run('add-mask')}>Add Mask</button>
|
|
153
|
+
<button type="button" class={iconButton} disabled={!snapshot.active} onclick={() => run('delete')}>Delete</button>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
class={iconButton}
|
|
157
|
+
aria-label="Layer menu"
|
|
158
|
+
aria-haspopup="menu"
|
|
159
|
+
onclick={(e) => {
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
162
|
+
menu = menu ? null : { x: r.left, y: r.bottom };
|
|
163
|
+
}}>⋯</button>
|
|
164
|
+
{#if psd}
|
|
165
|
+
<button type="button" class={iconButton} onclick={() => fileInput?.click()}>Open PSD…</button>
|
|
166
|
+
<button type="button" class={iconButton} onclick={() => psdTask(async () => (await downloadPsd(controller, psdFileName)).warnings)}>Export PSD</button>
|
|
167
|
+
<input
|
|
168
|
+
bind:this={fileInput}
|
|
169
|
+
type="file"
|
|
170
|
+
accept={PSD_FILE_ACCEPT}
|
|
171
|
+
class="hidden"
|
|
172
|
+
data-testid="psd-file-input"
|
|
173
|
+
aria-label="Open a PSD file"
|
|
174
|
+
onchange={onPsdFile} />
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
177
|
+
{#if psdNotes.length > 0}
|
|
178
|
+
<ul class="rounded bg-amber-900/30 text-amber-200 px-2 py-1 space-y-0.5" data-testid="psd-warnings" aria-label="PSD warnings">
|
|
179
|
+
{#each psdNotes as n (n)}
|
|
180
|
+
<li>{n}</li>
|
|
181
|
+
{/each}
|
|
182
|
+
<li><button type="button" class="underline" onclick={() => (psdNotes = [])}>Dismiss</button></li>
|
|
183
|
+
</ul>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if snapshot.active}
|
|
187
|
+
{@const active = snapshot.active}
|
|
188
|
+
<div class="grid grid-cols-2 gap-x-2 gap-y-1 items-center" data-testid="layer-properties">
|
|
189
|
+
<label class="flex flex-col gap-0.5">
|
|
190
|
+
<span class="text-slate-400">Opacity {Math.round(active.opacity * 100)}%</span>
|
|
191
|
+
<input
|
|
192
|
+
type="range"
|
|
193
|
+
min="0"
|
|
194
|
+
max="100"
|
|
195
|
+
aria-label="Layer opacity"
|
|
196
|
+
value={Math.round(active.opacity * 100)}
|
|
197
|
+
onpointerdown={() => (commitNext = true)}
|
|
198
|
+
onpointerup={() => (commitNext = true)}
|
|
199
|
+
onkeydown={() => (commitNext = true)}
|
|
200
|
+
oninput={(e) => prop('opacity', Number((e.target as HTMLInputElement).value) / 100)}
|
|
201
|
+
class="accent-indigo-500"
|
|
202
|
+
/>
|
|
203
|
+
</label>
|
|
204
|
+
<label class="flex flex-col gap-0.5">
|
|
205
|
+
<span class="text-slate-400">Fill {Math.round(fillOf(active) * 100)}%</span>
|
|
206
|
+
<input
|
|
207
|
+
type="range"
|
|
208
|
+
min="0"
|
|
209
|
+
max="100"
|
|
210
|
+
aria-label="Layer fill"
|
|
211
|
+
disabled={active.type !== 'pixel'}
|
|
212
|
+
value={Math.round(fillOf(active) * 100)}
|
|
213
|
+
onpointerdown={() => (commitNext = true)}
|
|
214
|
+
onpointerup={() => (commitNext = true)}
|
|
215
|
+
onkeydown={() => (commitNext = true)}
|
|
216
|
+
oninput={(e) => prop('fillOpacity', Number((e.target as HTMLInputElement).value) / 100)}
|
|
217
|
+
class="accent-indigo-500 disabled:opacity-40"
|
|
218
|
+
/>
|
|
219
|
+
</label>
|
|
220
|
+
<select
|
|
221
|
+
aria-label="Layer blend mode"
|
|
222
|
+
class="bg-slate-800 border border-slate-700 rounded px-1.5 py-1"
|
|
223
|
+
value={active.blend}
|
|
224
|
+
onchange={(e) => attempt(() => controller.setLayerProperties(active.id, { blend: (e.target as HTMLSelectElement).value as never }))}
|
|
225
|
+
>
|
|
226
|
+
{#if active.type === 'group'}<option value="pass-through">Pass Through</option>{/if}
|
|
227
|
+
{#each BLEND_MODES as m (m.id)}
|
|
228
|
+
<option value={m.id}>{m.label}</option>
|
|
229
|
+
{/each}
|
|
230
|
+
</select>
|
|
231
|
+
<label class="flex items-center gap-1.5 text-slate-300">
|
|
232
|
+
<input
|
|
233
|
+
type="checkbox"
|
|
234
|
+
aria-label="Clip to layer below"
|
|
235
|
+
checked={Boolean(active.clipped)}
|
|
236
|
+
onchange={(e) => attempt(() => controller.setLayerProperties(active.id, { clipped: (e.target as HTMLInputElement).checked }))}
|
|
237
|
+
class="accent-indigo-500"
|
|
238
|
+
/>
|
|
239
|
+
Clip to below
|
|
240
|
+
</label>
|
|
241
|
+
<div class="col-span-2 flex flex-wrap gap-2 text-slate-400" role="group" aria-label="Layer locks">
|
|
242
|
+
{#each LOCKS as k (k)}
|
|
243
|
+
<label class="flex items-center gap-1">
|
|
244
|
+
<input
|
|
245
|
+
type="checkbox"
|
|
246
|
+
aria-label={`Lock ${k}`}
|
|
247
|
+
checked={Boolean(active.locks?.[k])}
|
|
248
|
+
onchange={(e) => attempt(() => controller.setLayerProperties(active.id, { locks: { ...active.locks, [k]: (e.target as HTMLInputElement).checked } }))}
|
|
249
|
+
class="accent-indigo-500"
|
|
250
|
+
/>
|
|
251
|
+
{k}
|
|
252
|
+
</label>
|
|
253
|
+
{/each}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
{/if}
|
|
257
|
+
|
|
258
|
+
<ul class="flex flex-col gap-0.5" role="tree" aria-label="Layers">
|
|
259
|
+
{#each snapshot.rows as row (row.layer.id)}
|
|
260
|
+
{@const layer = row.layer}
|
|
261
|
+
<li
|
|
262
|
+
role="treeitem"
|
|
263
|
+
aria-selected={row.active}
|
|
264
|
+
aria-level={row.depth + 1}
|
|
265
|
+
data-layer-id={layer.id}
|
|
266
|
+
draggable={!layer.locks?.all}
|
|
267
|
+
ondragstart={(e) => {
|
|
268
|
+
e.dataTransfer?.setData('text/x-pixerate-layer', layer.id);
|
|
269
|
+
dragId = layer.id;
|
|
270
|
+
}}
|
|
271
|
+
ondragover={(e) => e.preventDefault()}
|
|
272
|
+
ondrop={(e) => onDrop(e, row)}
|
|
273
|
+
tabindex="0"
|
|
274
|
+
onclick={() => attempt(() => controller.setActiveLayer(layer.id, 'pixels'))}
|
|
275
|
+
onkeydown={(e) => {
|
|
276
|
+
if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
attempt(() => controller.setActiveLayer(layer.id, 'pixels'));
|
|
279
|
+
}
|
|
280
|
+
}}
|
|
281
|
+
oncontextmenu={(e) => {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
controller.setActiveLayer(layer.id, snapshot.activeLayerId === layer.id ? snapshot.activeTarget : 'pixels');
|
|
284
|
+
menu = { x: e.clientX, y: e.clientY };
|
|
285
|
+
}}
|
|
286
|
+
class={rowClass(row.active)}
|
|
287
|
+
style="padding-left: {6 + row.depth * 14}px"
|
|
288
|
+
>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
aria-label={layer.visible ? `Hide ${row.label}` : `Show ${row.label}`}
|
|
292
|
+
aria-pressed={layer.visible}
|
|
293
|
+
class="w-4 text-center text-slate-300"
|
|
294
|
+
onclick={(e) => {
|
|
295
|
+
e.stopPropagation();
|
|
296
|
+
attempt(() => controller.setLayerProperties(layer.id, { visible: !layer.visible }));
|
|
297
|
+
}}>{layer.visible ? '●' : '○'}</button>
|
|
298
|
+
{#if layer.type === 'group'}
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
aria-label={layer.collapsed ? `Expand ${row.label}` : `Collapse ${row.label}`}
|
|
302
|
+
class="w-3 text-slate-400"
|
|
303
|
+
onclick={(e) => {
|
|
304
|
+
e.stopPropagation();
|
|
305
|
+
attempt(() => controller.setLayerProperties(layer.id, { collapsed: !(layer.type === 'group' && layer.collapsed) }));
|
|
306
|
+
}}>{layer.collapsed ? '▸' : '▾'}</button>
|
|
307
|
+
{/if}
|
|
308
|
+
{#if row.clipped}<span class="text-slate-500" aria-label="Clipped">↳</span>{/if}
|
|
309
|
+
{#if layer.type === 'pixel'}
|
|
310
|
+
<canvas
|
|
311
|
+
use:paint={thumb(layer)}
|
|
312
|
+
aria-label={`${row.label} pixels`}
|
|
313
|
+
style="width: {thumbnailSize}px; height: {thumbnailSize}px"
|
|
314
|
+
class={thumbClass(row.active && snapshot.activeTarget === 'pixels' && Boolean(layer.mask))}
|
|
315
|
+
></canvas>
|
|
316
|
+
{:else}
|
|
317
|
+
<span
|
|
318
|
+
class="shrink-0 grid place-items-center rounded bg-slate-800 text-[9px] text-slate-400 ring-1 ring-slate-700"
|
|
319
|
+
style="width: {thumbnailSize}px; height: {thumbnailSize}px">{layerBadge(layer)}</span>
|
|
320
|
+
{/if}
|
|
321
|
+
{#if layer.mask}
|
|
322
|
+
<canvas
|
|
323
|
+
use:paint={maskThumb(layer)}
|
|
324
|
+
role="button"
|
|
325
|
+
tabindex="0"
|
|
326
|
+
aria-label={`${row.label} mask${layer.mask.enabled === false ? ' (disabled)' : ''}`}
|
|
327
|
+
data-testid="layer-mask-thumbnail"
|
|
328
|
+
style="width: {thumbnailSize}px; height: {thumbnailSize}px"
|
|
329
|
+
class={thumbClass(row.active && snapshot.activeTarget === 'mask')}
|
|
330
|
+
onclick={(e) => {
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
if (e.shiftKey) attempt(() => controller.setLayerMaskProps(layer.id, { enabled: layer.mask!.enabled === false }));
|
|
333
|
+
else attempt(() => controller.setActiveLayer(layer.id, 'mask'));
|
|
334
|
+
}}
|
|
335
|
+
></canvas>
|
|
336
|
+
{/if}
|
|
337
|
+
{#if renaming === layer.id}
|
|
338
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
339
|
+
<input
|
|
340
|
+
autofocus
|
|
341
|
+
aria-label="Layer name"
|
|
342
|
+
value={layer.name || row.label}
|
|
343
|
+
class="flex-1 min-w-0 bg-slate-900 border border-slate-600 rounded px-1"
|
|
344
|
+
onclick={(e) => e.stopPropagation()}
|
|
345
|
+
onblur={(e) => {
|
|
346
|
+
const v = (e.target as HTMLInputElement).value;
|
|
347
|
+
renaming = null;
|
|
348
|
+
if (v.trim() && v !== layer.name) attempt(() => controller.setLayerProperties(layer.id, { name: v }));
|
|
349
|
+
}}
|
|
350
|
+
onkeydown={(e) => {
|
|
351
|
+
if (e.key === 'Enter') (e.target as HTMLInputElement).blur();
|
|
352
|
+
if (e.key === 'Escape') renaming = null;
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
{:else}
|
|
356
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
357
|
+
<span
|
|
358
|
+
class="flex-1 min-w-0 truncate {layer.visible ? '' : 'text-slate-500'}"
|
|
359
|
+
ondblclick={(e) => {
|
|
360
|
+
e.stopPropagation();
|
|
361
|
+
renaming = layer.id;
|
|
362
|
+
}}>{row.label}</span>
|
|
363
|
+
{/if}
|
|
364
|
+
{#if locked(layer)}<span aria-label="Locked" class="text-slate-400">🔒</span>{/if}
|
|
365
|
+
{#if layer.blend !== 'normal' && layer.blend !== 'pass-through'}<span class="text-[9px] text-slate-500">{layer.blend}</span>{/if}
|
|
366
|
+
</li>
|
|
367
|
+
{/each}
|
|
368
|
+
</ul>
|
|
369
|
+
|
|
370
|
+
{#if error}
|
|
371
|
+
<p role="alert" class="text-rose-400">{error}</p>
|
|
372
|
+
{/if}
|
|
373
|
+
|
|
374
|
+
{#if menu}
|
|
375
|
+
<div
|
|
376
|
+
role="menu"
|
|
377
|
+
tabindex="-1"
|
|
378
|
+
aria-label="Layer menu"
|
|
379
|
+
class="fixed z-50 min-w-44 rounded border border-slate-700 bg-slate-900 py-1 shadow-xl"
|
|
380
|
+
style="left: {menu.x}px; top: {menu.y}px"
|
|
381
|
+
onclick={(e) => e.stopPropagation()}
|
|
382
|
+
onkeydown={(e) => e.key === 'Escape' && (menu = null)}
|
|
383
|
+
>
|
|
384
|
+
{#each snapshot.menu as item (item.action)}
|
|
385
|
+
<button
|
|
386
|
+
type="button"
|
|
387
|
+
role="menuitem"
|
|
388
|
+
disabled={!item.enabled}
|
|
389
|
+
class="block w-full px-3 py-1 text-left hover:bg-slate-800 disabled:opacity-40"
|
|
390
|
+
onclick={() => run(item.action)}>{item.label}</button>
|
|
391
|
+
{/each}
|
|
392
|
+
</div>
|
|
393
|
+
{/if}
|
|
394
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: ImageEditorController;
|
|
4
|
+
/** Thumbnail size in px (default 32). */
|
|
5
|
+
thumbnailSize?: number;
|
|
6
|
+
/** Show Open PSD / Export PSD (default true). */
|
|
7
|
+
psd?: boolean;
|
|
8
|
+
/** Base name of exported PSD files (default `image`). */
|
|
9
|
+
psdFileName?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const LayersPanel: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type LayersPanel = ReturnType<typeof LayersPanel>;
|
|
14
|
+
export default LayersPanel;
|
|
15
|
+
//# sourceMappingURL=LayersPanel.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LayersPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/LayersPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAA2D,MAAM,+BAA+B,CAAC;AAgBlI,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,yCAAyC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsQH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -101,6 +101,18 @@
|
|
|
101
101
|
<input type="checkbox" checked={options.sampleMerged} onchange={(e) => set({ sampleMerged: (e.currentTarget as HTMLInputElement).checked })} />
|
|
102
102
|
Sample adjusted image
|
|
103
103
|
</label>
|
|
104
|
+
<label class="flex items-center gap-2 text-slate-300">
|
|
105
|
+
Sample
|
|
106
|
+
<select
|
|
107
|
+
aria-label="Sample from"
|
|
108
|
+
value={options.sample ?? 'composite'}
|
|
109
|
+
onchange={(e) => set({ sample: (e.currentTarget as HTMLSelectElement).value as 'composite' | 'active-layer' })}
|
|
110
|
+
class="bg-slate-800 border border-slate-700 rounded px-1 py-0.5"
|
|
111
|
+
>
|
|
112
|
+
<option value="composite">All layers</option>
|
|
113
|
+
<option value="active-layer">Active layer</option>
|
|
114
|
+
</select>
|
|
115
|
+
</label>
|
|
104
116
|
</div>
|
|
105
117
|
{/if}
|
|
106
118
|
<label class="flex items-center gap-2 text-slate-300">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectionOptionsBar.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/SelectionOptionsBar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACR,qBAAqB,EACrB,aAAa,EAEb,oBAAoB,EACrB,MAAM,+BAA+B,CAAC;AAUvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,eAAe,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,gFAAgF;IAChF,SAAS,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACjC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"SelectionOptionsBar.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/SelectionOptionsBar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACR,qBAAqB,EACrB,aAAa,EAEb,oBAAoB,EACrB,MAAM,+BAA+B,CAAC;AAUvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,eAAe,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,gFAAgF;IAChF,SAAS,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACjC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4GH,QAAA,MAAM,mBAAmB,2CAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ImageEditorController, type ImageEditorOptions, type ImageEditorState, type ImageEditorTool, type CropBox, type AspectRatio, type ImageAdjustments, type DepthMaskOptions, type Annotation, type CreateAnnotationPayload, type ExportOptions, type SerializedImageEditorState, type ImagePoint, type AdjustmentKind, type AdjustmentLayerInit, type AdjustmentLayerUpdate, type ImageHistogram } from '@pixerate/editor/image-editor';
|
|
1
|
+
import { ImageEditorController, type ImageEditorFit, type ImageEditorOptions, type ImageEditorState, type ImageEditorTool, type CropBox, type AspectRatio, type ImageAdjustments, type DepthMaskOptions, type Annotation, type CreateAnnotationPayload, type ExportOptions, type SerializedImageEditorState, type AIResultOptions, type ImagePoint, type AdjustmentKind, type AdjustmentLayerInit, type AdjustmentLayerUpdate, type ImageHistogram } from '@pixerate/editor/image-editor';
|
|
2
2
|
export declare function createReactiveImageEditor(options?: ImageEditorOptions): {
|
|
3
3
|
readonly controller: ImageEditorController;
|
|
4
4
|
readonly state: ImageEditorState;
|
|
@@ -40,6 +40,7 @@ export declare function createReactiveImageEditor(options?: ImageEditorOptions):
|
|
|
40
40
|
setZoom(zoom: number): void;
|
|
41
41
|
setPan(pan: ImagePoint): void;
|
|
42
42
|
setComparing(comparing: boolean): void;
|
|
43
|
+
setFit(fit: ImageEditorFit): void;
|
|
43
44
|
loadImage(src: string | HTMLImageElement): Promise<void>;
|
|
44
45
|
toDataURL(opts?: ExportOptions): Promise<string>;
|
|
45
46
|
toBlob(opts?: ExportOptions): Promise<Blob>;
|
|
@@ -54,8 +55,10 @@ export declare function createReactiveImageEditor(options?: ImageEditorOptions):
|
|
|
54
55
|
useCrop?: boolean;
|
|
55
56
|
}): Promise<string>;
|
|
56
57
|
toSourceDataURL(opts?: Pick<ExportOptions, "format" | "quality">): Promise<string>;
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
/** In documents with several layers (or `target: 'new-layer'`) the result goes to a new layer. */
|
|
59
|
+
applyInpaintedImage(newDataUrl: string, options?: AIResultOptions): Promise<string | void>;
|
|
60
|
+
/** In documents with several layers (or `target: 'new-layer'`) the result's alpha masks the active layer. */
|
|
61
|
+
applyBackgroundRemovedImage(newDataUrl: string, options?: AIResultOptions): Promise<string | void>;
|
|
59
62
|
/** Stops syncing controller state; call when the editor is no longer used. */
|
|
60
63
|
destroy(): void;
|
|
61
64
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imageEditorState.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/imageEditorState.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,aAAa,EAClB,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACpB,MAAM,+BAA+B,CAAC;AAEvC,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,kBAAuB;;;;IA2BtE,mGAAmG;;;kBAQrF,eAAe;kBAGf,OAAO,GAAG,IAAI;2BAGL,WAAW;;oBAMlB,MAAM;yBAGD,MAAM;;;4BASH,OAAO,CAAC,gBAAgB,CAAC;;IAMjD,oGAAoG;;IAIpG,mEAAmE;6BAC1C,cAAc,GAAG,mBAAmB,UAAU,MAAM;8BAGnD,MAAM,UAAU,qBAAqB;8BAGrC,MAAM;4BAGR,MAAM,WAAW,MAAM;8BAGrB,MAAM,YAAY,OAAO;0BAG7B,OAAO,CAAC,gBAAgB,CAAC;;uBAM5B,uBAAuB;yBAGrB,MAAM,WAAW,OAAO,CAAC,UAAU,CAAC;yBAGpC,MAAM;;yBAMN,MAAM,GAAG,IAAI;;;;oBAYlB,MAAM;qBAGL,MAAM;;kBAMT,MAAM;gBAGR,UAAU;4BAGE,OAAO;
|
|
1
|
+
{"version":3,"file":"imageEditorState.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/imageEditorState.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,aAAa,EAClB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACpB,MAAM,+BAA+B,CAAC;AAEvC,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,kBAAuB;;;;IA2BtE,mGAAmG;;;kBAQrF,eAAe;kBAGf,OAAO,GAAG,IAAI;2BAGL,WAAW;;oBAMlB,MAAM;yBAGD,MAAM;;;4BASH,OAAO,CAAC,gBAAgB,CAAC;;IAMjD,oGAAoG;;IAIpG,mEAAmE;6BAC1C,cAAc,GAAG,mBAAmB,UAAU,MAAM;8BAGnD,MAAM,UAAU,qBAAqB;8BAGrC,MAAM;4BAGR,MAAM,WAAW,MAAM;8BAGrB,MAAM,YAAY,OAAO;0BAG7B,OAAO,CAAC,gBAAgB,CAAC;;uBAM5B,uBAAuB;yBAGrB,MAAM,WAAW,OAAO,CAAC,UAAU,CAAC;yBAGpC,MAAM;;yBAMN,MAAM,GAAG,IAAI;;;;oBAYlB,MAAM;qBAGL,MAAM;;kBAMT,MAAM;gBAGR,UAAU;4BAGE,OAAO;gBAGnB,cAAc;mBAGX,MAAM,GAAG,gBAAgB;qBAGvB,aAAa;kBAGhB,aAAa;;mBAMZ,0BAA0B;sBAGvB;QAAE,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE;yBAG5C;QAAE,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE;2BAG7C,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC;IAGhE,kGAAkG;oCAClE,MAAM,YAAY,eAAe;IAGjE,6GAA6G;4CACrE,MAAM,YAAY,eAAe;IAGzE,8EAA8E;;EAMjF;AAED,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE/E,6GAA6G;AAC7G,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,qBAAqB;;;EAarE"}
|
|
@@ -129,6 +129,9 @@ export function createReactiveImageEditor(options = {}) {
|
|
|
129
129
|
setComparing(comparing) {
|
|
130
130
|
controller.setComparing(comparing);
|
|
131
131
|
},
|
|
132
|
+
setFit(fit) {
|
|
133
|
+
controller.setFit(fit);
|
|
134
|
+
},
|
|
132
135
|
loadImage(src) {
|
|
133
136
|
return controller.loadImage(src);
|
|
134
137
|
},
|
|
@@ -153,11 +156,13 @@ export function createReactiveImageEditor(options = {}) {
|
|
|
153
156
|
toSourceDataURL(opts) {
|
|
154
157
|
return controller.toSourceDataURL(opts);
|
|
155
158
|
},
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
/** In documents with several layers (or `target: 'new-layer'`) the result goes to a new layer. */
|
|
160
|
+
applyInpaintedImage(newDataUrl, options) {
|
|
161
|
+
return controller.applyInpaintedImage(newDataUrl, options);
|
|
158
162
|
},
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
/** In documents with several layers (or `target: 'new-layer'`) the result's alpha masks the active layer. */
|
|
164
|
+
applyBackgroundRemovedImage(newDataUrl, options) {
|
|
165
|
+
return controller.applyBackgroundRemovedImage(newDataUrl, options);
|
|
161
166
|
},
|
|
162
167
|
/** Stops syncing controller state; call when the editor is no longer used. */
|
|
163
168
|
destroy() {
|
|
@@ -6,6 +6,8 @@ import type CurvesEditorComponent from "./CurvesEditor.svelte";
|
|
|
6
6
|
import type ControlSliderComponent from "./ControlSlider.svelte";
|
|
7
7
|
import type SelectionOptionsBarComponent from "./SelectionOptionsBar.svelte";
|
|
8
8
|
import type RefineEdgePanelComponent from "./RefineEdgePanel.svelte";
|
|
9
|
+
import type LayersPanelComponent from "./LayersPanel.svelte";
|
|
10
|
+
import type BrushOptionsBarComponent from "./BrushOptionsBar.svelte";
|
|
9
11
|
export { default as ImageEditor } from './ImageEditor.svelte';
|
|
10
12
|
export { default as Histogram } from './Histogram.svelte';
|
|
11
13
|
export { default as AdjustmentLayersPanel } from './AdjustmentLayersPanel.svelte';
|
|
@@ -13,6 +15,8 @@ export { default as CurvesEditor } from './CurvesEditor.svelte';
|
|
|
13
15
|
export { default as ControlSlider } from './ControlSlider.svelte';
|
|
14
16
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
15
17
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
18
|
+
export { default as LayersPanel } from './LayersPanel.svelte';
|
|
19
|
+
export { default as BrushOptionsBar } from './BrushOptionsBar.svelte';
|
|
16
20
|
export * from './imageEditorState.svelte.js';
|
|
17
21
|
export type ImageEditorProps = ComponentProps<typeof ImageEditorComponent>;
|
|
18
22
|
export type HistogramProps = ComponentProps<typeof HistogramComponent>;
|
|
@@ -21,5 +25,7 @@ export type CurvesEditorProps = ComponentProps<typeof CurvesEditorComponent>;
|
|
|
21
25
|
export type ControlSliderProps = ComponentProps<typeof ControlSliderComponent>;
|
|
22
26
|
export type SelectionOptionsBarProps = ComponentProps<typeof SelectionOptionsBarComponent>;
|
|
23
27
|
export type RefineEdgePanelProps = ComponentProps<typeof RefineEdgePanelComponent>;
|
|
28
|
+
export type LayersPanelProps = ComponentProps<typeof LayersPanelComponent>;
|
|
29
|
+
export type BrushOptionsBarProps = ComponentProps<typeof BrushOptionsBarComponent>;
|
|
24
30
|
export { applyRegionMosaic, applyRegionBlur, resolveMosaicRegionBounds, type MosaicRegion, type MosaicImageDataLike } from '@pixerate/editor/image-editor';
|
|
25
31
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/image-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,kBAAkB,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,8BAA8B,MAAM,gCAAgC,CAAC;AACjF,OAAO,KAAK,qBAAqB,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,4BAA4B,MAAM,8BAA8B,CAAC;AAC7E,OAAO,KAAK,wBAAwB,MAAM,0BAA0B,CAAC;AAErE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,cAAc,8BAA8B,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACvE,MAAM,MAAM,0BAA0B,GAAG,cAAc,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAC/F,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC7E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,wBAAwB,GAAG,cAAc,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAC3F,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEnF,OAAO,EACN,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EACzB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,MAAM,+BAA+B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/image-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,kBAAkB,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,8BAA8B,MAAM,gCAAgC,CAAC;AACjF,OAAO,KAAK,qBAAqB,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,4BAA4B,MAAM,8BAA8B,CAAC;AAC7E,OAAO,KAAK,wBAAwB,MAAM,0BAA0B,CAAC;AACrE,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,wBAAwB,MAAM,0BAA0B,CAAC;AAErE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,cAAc,8BAA8B,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACvE,MAAM,MAAM,0BAA0B,GAAG,cAAc,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAC/F,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC7E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,wBAAwB,GAAG,cAAc,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAC3F,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,wBAAwB,CAAC,CAAC;AACnF,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEnF,OAAO,EACN,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EACzB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,MAAM,+BAA+B,CAAC"}
|
|
@@ -5,5 +5,7 @@ export { default as CurvesEditor } from './CurvesEditor.svelte';
|
|
|
5
5
|
export { default as ControlSlider } from './ControlSlider.svelte';
|
|
6
6
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
7
7
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
8
|
+
export { default as LayersPanel } from './LayersPanel.svelte';
|
|
9
|
+
export { default as BrushOptionsBar } from './BrushOptionsBar.svelte';
|
|
8
10
|
export * from './imageEditorState.svelte.js';
|
|
9
11
|
export { applyRegionMosaic, applyRegionBlur, resolveMosaicRegionBounds } from '@pixerate/editor/image-editor';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.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.
|
|
134
|
+
"@pixerate/editor": "0.90.0"
|
|
135
135
|
},
|
|
136
136
|
"peerDependencies": {
|
|
137
137
|
"@dagrejs/dagre": "^1.1.4",
|