@pixerate/editor-svelte 0.67.0 → 0.68.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 +114 -2
- 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 +340 -0
- package/dist/image-editor/LayersPanel.svelte.d.ts +11 -0
- package/dist/image-editor/LayersPanel.svelte.d.ts.map +1 -0
- package/dist/image-editor/imageEditorState.svelte.d.ts +2 -1
- package/dist/image-editor/imageEditorState.svelte.d.ts.map +1 -1
- package/dist/image-editor/imageEditorState.svelte.js +3 -0
- 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);
|
|
@@ -193,15 +205,66 @@
|
|
|
193
205
|
await editor.controller.render(canvasEl, {
|
|
194
206
|
viewportWidth: vpWidth,
|
|
195
207
|
viewportHeight: vpHeight,
|
|
208
|
+
fit,
|
|
196
209
|
showCropOverlay: activeTab === 'crop',
|
|
197
210
|
showSelectionOverlay: true,
|
|
198
211
|
interactive: true,
|
|
199
212
|
draftAnnotation: overrideAnnotation !== undefined ? overrideAnnotation : draftAnnotation,
|
|
200
213
|
draftCrop: overrideCrop !== undefined ? overrideCrop : draftCrop,
|
|
201
214
|
draftSelectionPath: selectionDraft,
|
|
215
|
+
brushCursor:
|
|
216
|
+
activeTab === 'paint' && brushCursor && editor.controller.getState().activeTool !== 'move'
|
|
217
|
+
? { ...brushCursor, diameter: editor.controller.getState().paint.brush.size }
|
|
218
|
+
: null,
|
|
202
219
|
});
|
|
203
220
|
}
|
|
204
221
|
|
|
222
|
+
// Paint tools: pointer events (coalesced samples, pointer capture) into strokes and moves.
|
|
223
|
+
function paintSamples(e: PointerEvent) {
|
|
224
|
+
const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];
|
|
225
|
+
const list = events.length ? events : [e];
|
|
226
|
+
const rect = canvasEl!.getBoundingClientRect();
|
|
227
|
+
const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
|
|
228
|
+
const sy = rect.height > 0 ? canvasEl!.height / rect.height : 1;
|
|
229
|
+
const s = editor.controller.getState();
|
|
230
|
+
return list.map((ev) => ({
|
|
231
|
+
...canvasToImagePoint({ x: (ev.clientX - rect.left) * sx, y: (ev.clientY - rect.top) * sy }, s, canvasEl!.width, canvasEl!.height, false),
|
|
232
|
+
pressure: pointerPressure(ev),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
function handlePaintPointerDown(e: PointerEvent) {
|
|
236
|
+
if (activeTab !== 'paint' || e.button !== 0 || !canvasEl) return;
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
try {
|
|
239
|
+
canvasEl.setPointerCapture?.(e.pointerId);
|
|
240
|
+
} catch {
|
|
241
|
+
// Synthetic events have no active pointer to capture.
|
|
242
|
+
}
|
|
243
|
+
paintError = paintGesture.down(paintSamples(e)[0]);
|
|
244
|
+
}
|
|
245
|
+
function handlePaintPointerMove(e: PointerEvent) {
|
|
246
|
+
if (activeTab !== 'paint' || !canvasEl) return;
|
|
247
|
+
const samples = paintSamples(e);
|
|
248
|
+
brushCursor = samples[samples.length - 1] ?? null;
|
|
249
|
+
if (paintGesture.active) paintGesture.moveTo(samples);
|
|
250
|
+
else void renderCanvas();
|
|
251
|
+
}
|
|
252
|
+
function handlePaintPointerUp() {
|
|
253
|
+
if (activeTab !== 'paint' || !paintGesture.active) return;
|
|
254
|
+
paintGesture.up();
|
|
255
|
+
if (paintGesture.error) paintError = paintGesture.error;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
$effect(() => {
|
|
259
|
+
if (activeTab !== 'paint' || typeof window === 'undefined') return;
|
|
260
|
+
const c = editor.controller;
|
|
261
|
+
const onKey = (e: KeyboardEvent) => {
|
|
262
|
+
if (handlePaintShortcut(c, e)) e.preventDefault();
|
|
263
|
+
};
|
|
264
|
+
window.addEventListener('keydown', onKey);
|
|
265
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
266
|
+
});
|
|
267
|
+
|
|
205
268
|
$effect(() => {
|
|
206
269
|
// Re-render when editor state or activeTab changes
|
|
207
270
|
void editor.state;
|
|
@@ -659,6 +722,30 @@
|
|
|
659
722
|
>
|
|
660
723
|
Adjustments
|
|
661
724
|
</button>
|
|
725
|
+
<button
|
|
726
|
+
type="button"
|
|
727
|
+
onclick={() => {
|
|
728
|
+
activeTab = 'layers';
|
|
729
|
+
editor.setTool('select');
|
|
730
|
+
}}
|
|
731
|
+
class="px-3 py-1.5 rounded-md font-medium transition-colors {activeTab === 'layers'
|
|
732
|
+
? 'bg-indigo-600 text-white shadow'
|
|
733
|
+
: 'text-slate-400 hover:text-slate-200'}"
|
|
734
|
+
>
|
|
735
|
+
Layers
|
|
736
|
+
</button>
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
onclick={() => {
|
|
740
|
+
activeTab = 'paint';
|
|
741
|
+
editor.setTool('brush');
|
|
742
|
+
}}
|
|
743
|
+
class="px-3 py-1.5 rounded-md font-medium transition-colors {activeTab === 'paint'
|
|
744
|
+
? 'bg-indigo-600 text-white shadow'
|
|
745
|
+
: 'text-slate-400 hover:text-slate-200'}"
|
|
746
|
+
>
|
|
747
|
+
Paint
|
|
748
|
+
</button>
|
|
662
749
|
<button
|
|
663
750
|
type="button"
|
|
664
751
|
onclick={() => {
|
|
@@ -798,7 +885,12 @@
|
|
|
798
885
|
onmousemove={handleMouseMove}
|
|
799
886
|
onmouseup={handleMouseUp}
|
|
800
887
|
onmouseleave={handleMouseUp}
|
|
801
|
-
|
|
888
|
+
onpointerdown={handlePaintPointerDown}
|
|
889
|
+
onpointermove={handlePaintPointerMove}
|
|
890
|
+
onpointerup={handlePaintPointerUp}
|
|
891
|
+
onpointercancel={() => paintGesture.cancel()}
|
|
892
|
+
data-testid="image-editor-canvas"
|
|
893
|
+
class="absolute inset-0 w-full h-full block {activeTab === 'paint' ? 'touch-none cursor-crosshair' : ''}"
|
|
802
894
|
></canvas>
|
|
803
895
|
</div>
|
|
804
896
|
|
|
@@ -978,6 +1070,26 @@
|
|
|
978
1070
|
{/if}
|
|
979
1071
|
|
|
980
1072
|
<!-- TAB 3: ANNOTATIONS -->
|
|
1073
|
+
{#if activeTab === 'layers'}
|
|
1074
|
+
<div class="flex flex-col gap-3 text-xs">
|
|
1075
|
+
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Layers</span>
|
|
1076
|
+
<LayersPanel controller={editor.controller} />
|
|
1077
|
+
</div>
|
|
1078
|
+
{/if}
|
|
1079
|
+
|
|
1080
|
+
{#if activeTab === 'paint'}
|
|
1081
|
+
<div class="flex flex-col gap-3 text-xs">
|
|
1082
|
+
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Paint</span>
|
|
1083
|
+
<BrushOptionsBar controller={editor.controller} />
|
|
1084
|
+
{#if paintError}
|
|
1085
|
+
<p role="alert" class="text-rose-400" data-testid="paint-error">{paintError}</p>
|
|
1086
|
+
{/if}
|
|
1087
|
+
<div class="border-t border-slate-700/60 pt-3">
|
|
1088
|
+
<LayersPanel controller={editor.controller} />
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
{/if}
|
|
1092
|
+
|
|
981
1093
|
{#if activeTab === 'annotate'}
|
|
982
1094
|
<div class="flex flex-col gap-3 text-xs">
|
|
983
1095
|
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">
|
|
@@ -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;AA4oCH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ImageEditorController, ImageLayer, LayerMenuAction, LayerPanelRow, PixelBuffer } from '@pixerate/editor/image-editor';
|
|
3
|
+
import { BLEND_MODES, dropPlacement, dropZone, layerBadge, layerMenu, layerPanelRows, runLayerMenuAction } from '@pixerate/editor/image-editor';
|
|
4
|
+
import { untrack } from 'svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
controller: ImageEditorController;
|
|
8
|
+
/** Thumbnail size in px (default 32). */
|
|
9
|
+
thumbnailSize?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { controller, thumbnailSize = 32, class: className = '' }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let tick = $state(0);
|
|
16
|
+
$effect(() => {
|
|
17
|
+
const c = controller;
|
|
18
|
+
// `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
|
|
19
|
+
return untrack(() => c.subscribe(() => tick++));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const snapshot = $derived.by(() => {
|
|
23
|
+
void tick;
|
|
24
|
+
const s = controller.getState();
|
|
25
|
+
return {
|
|
26
|
+
layers: s.layers,
|
|
27
|
+
activeLayerId: s.activeLayerId,
|
|
28
|
+
activeTarget: s.activeTarget,
|
|
29
|
+
rows: layerPanelRows(s.layers, s.activeLayerId),
|
|
30
|
+
active: controller.getActiveLayer() ?? null,
|
|
31
|
+
menu: layerMenu(controller),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let error = $state<string | null>(null);
|
|
36
|
+
let renaming = $state<string | null>(null);
|
|
37
|
+
let menu = $state<{ x: number; y: number } | null>(null);
|
|
38
|
+
let dragId = $state<string | null>(null);
|
|
39
|
+
/** Whether the next slider change records an undo step (the first of a drag does, the rest replace it). */
|
|
40
|
+
let commitNext = true;
|
|
41
|
+
|
|
42
|
+
const LOCKS = ['pixels', 'transparency', 'position', 'all'] as const;
|
|
43
|
+
const iconButton = 'px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 hover:text-white disabled:opacity-40';
|
|
44
|
+
const rowClass = (active: boolean) =>
|
|
45
|
+
`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'}`;
|
|
46
|
+
|
|
47
|
+
function attempt(fn: () => unknown) {
|
|
48
|
+
try {
|
|
49
|
+
error = null;
|
|
50
|
+
fn();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
error = err instanceof Error ? err.message : String(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function run(action: LayerMenuAction) {
|
|
57
|
+
menu = null;
|
|
58
|
+
error = await runLayerMenuAction(controller, action);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function prop(key: 'opacity' | 'fillOpacity', value: number) {
|
|
62
|
+
const id = snapshot.active?.id;
|
|
63
|
+
if (!id) return;
|
|
64
|
+
attempt(() => {
|
|
65
|
+
controller.setLayerProperties(id, { [key]: value }, commitNext);
|
|
66
|
+
commitNext = false;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onDrop(e: DragEvent, row: LayerPanelRow) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
const id = e.dataTransfer?.getData('text/x-pixerate-layer') || dragId;
|
|
73
|
+
dragId = null;
|
|
74
|
+
if (!id) return;
|
|
75
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
76
|
+
const fraction = rect.height ? (e.clientY - rect.top) / rect.height : 0.5;
|
|
77
|
+
const placement = dropPlacement(snapshot.layers, id, row.layer.id, dropZone(fraction, row.layer.type === 'group'));
|
|
78
|
+
if (placement) attempt(() => controller.moveLayer(id, placement));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const locked = (layer: ImageLayer) => Boolean(layer.locks?.all || layer.locks?.pixels || layer.locks?.position || layer.locks?.transparency);
|
|
82
|
+
const fillOf = (layer: ImageLayer) => (layer.type === 'pixel' ? layer.fillOpacity ?? 1 : 1);
|
|
83
|
+
|
|
84
|
+
/** Draws RGBA pixels into a canvas (nothing without a 2D context). */
|
|
85
|
+
function paint(canvas: HTMLCanvasElement, pixels: PixelBuffer | null) {
|
|
86
|
+
const draw = (px: PixelBuffer | null) => {
|
|
87
|
+
const ctx = canvas.getContext('2d');
|
|
88
|
+
if (!ctx || !px || typeof ctx.createImageData !== 'function') return;
|
|
89
|
+
canvas.width = px.width;
|
|
90
|
+
canvas.height = px.height;
|
|
91
|
+
const img = ctx.createImageData(px.width, px.height);
|
|
92
|
+
img.data.set(px.data as ArrayLike<number>);
|
|
93
|
+
ctx.putImageData(img, 0, 0);
|
|
94
|
+
};
|
|
95
|
+
draw(pixels);
|
|
96
|
+
return { update: draw };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function thumb(layer: ImageLayer): PixelBuffer | null {
|
|
100
|
+
void tick;
|
|
101
|
+
return layer.type === 'pixel' ? controller.getLayerThumbnail(layer.id, { maxSide: 64 }) : null;
|
|
102
|
+
}
|
|
103
|
+
function maskThumb(layer: ImageLayer): PixelBuffer | null {
|
|
104
|
+
void tick;
|
|
105
|
+
return layer.mask && !layer.mask.source ? controller.getLayerThumbnail(layer.id, { maxSide: 64, mask: true }) : null;
|
|
106
|
+
}
|
|
107
|
+
const thumbClass = (selected: boolean) =>
|
|
108
|
+
`shrink-0 rounded bg-slate-800 ${selected ? 'ring-2 ring-white' : 'ring-1 ring-slate-700'}`;
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<svelte:window onclick={() => menu && (menu = null)} />
|
|
112
|
+
|
|
113
|
+
<div class="flex flex-col gap-2 text-xs text-slate-200 {className}" data-testid="layers-panel">
|
|
114
|
+
<div class="flex flex-wrap items-center gap-1" role="toolbar" aria-label="Layer actions">
|
|
115
|
+
<button type="button" class={iconButton} onclick={() => run('new-layer')}>New Layer</button>
|
|
116
|
+
<button type="button" class={iconButton} onclick={() => run('new-group')}>New Group</button>
|
|
117
|
+
<button type="button" class={iconButton} disabled={!snapshot.active} onclick={() => run('duplicate')}>Duplicate</button>
|
|
118
|
+
<button type="button" class={iconButton} disabled={!snapshot.active} onclick={() => run('add-mask')}>Add Mask</button>
|
|
119
|
+
<button type="button" class={iconButton} disabled={!snapshot.active} onclick={() => run('delete')}>Delete</button>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
class={iconButton}
|
|
123
|
+
aria-label="Layer menu"
|
|
124
|
+
aria-haspopup="menu"
|
|
125
|
+
onclick={(e) => {
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
128
|
+
menu = menu ? null : { x: r.left, y: r.bottom };
|
|
129
|
+
}}>⋯</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{#if snapshot.active}
|
|
133
|
+
{@const active = snapshot.active}
|
|
134
|
+
<div class="grid grid-cols-2 gap-x-2 gap-y-1 items-center" data-testid="layer-properties">
|
|
135
|
+
<label class="flex flex-col gap-0.5">
|
|
136
|
+
<span class="text-slate-400">Opacity {Math.round(active.opacity * 100)}%</span>
|
|
137
|
+
<input
|
|
138
|
+
type="range"
|
|
139
|
+
min="0"
|
|
140
|
+
max="100"
|
|
141
|
+
aria-label="Layer opacity"
|
|
142
|
+
value={Math.round(active.opacity * 100)}
|
|
143
|
+
onpointerdown={() => (commitNext = true)}
|
|
144
|
+
onpointerup={() => (commitNext = true)}
|
|
145
|
+
onkeydown={() => (commitNext = true)}
|
|
146
|
+
oninput={(e) => prop('opacity', Number((e.target as HTMLInputElement).value) / 100)}
|
|
147
|
+
class="accent-indigo-500"
|
|
148
|
+
/>
|
|
149
|
+
</label>
|
|
150
|
+
<label class="flex flex-col gap-0.5">
|
|
151
|
+
<span class="text-slate-400">Fill {Math.round(fillOf(active) * 100)}%</span>
|
|
152
|
+
<input
|
|
153
|
+
type="range"
|
|
154
|
+
min="0"
|
|
155
|
+
max="100"
|
|
156
|
+
aria-label="Layer fill"
|
|
157
|
+
disabled={active.type !== 'pixel'}
|
|
158
|
+
value={Math.round(fillOf(active) * 100)}
|
|
159
|
+
onpointerdown={() => (commitNext = true)}
|
|
160
|
+
onpointerup={() => (commitNext = true)}
|
|
161
|
+
onkeydown={() => (commitNext = true)}
|
|
162
|
+
oninput={(e) => prop('fillOpacity', Number((e.target as HTMLInputElement).value) / 100)}
|
|
163
|
+
class="accent-indigo-500 disabled:opacity-40"
|
|
164
|
+
/>
|
|
165
|
+
</label>
|
|
166
|
+
<select
|
|
167
|
+
aria-label="Layer blend mode"
|
|
168
|
+
class="bg-slate-800 border border-slate-700 rounded px-1.5 py-1"
|
|
169
|
+
value={active.blend}
|
|
170
|
+
onchange={(e) => attempt(() => controller.setLayerProperties(active.id, { blend: (e.target as HTMLSelectElement).value as never }))}
|
|
171
|
+
>
|
|
172
|
+
{#if active.type === 'group'}<option value="pass-through">Pass Through</option>{/if}
|
|
173
|
+
{#each BLEND_MODES as m (m.id)}
|
|
174
|
+
<option value={m.id}>{m.label}</option>
|
|
175
|
+
{/each}
|
|
176
|
+
</select>
|
|
177
|
+
<label class="flex items-center gap-1.5 text-slate-300">
|
|
178
|
+
<input
|
|
179
|
+
type="checkbox"
|
|
180
|
+
aria-label="Clip to layer below"
|
|
181
|
+
checked={Boolean(active.clipped)}
|
|
182
|
+
onchange={(e) => attempt(() => controller.setLayerProperties(active.id, { clipped: (e.target as HTMLInputElement).checked }))}
|
|
183
|
+
class="accent-indigo-500"
|
|
184
|
+
/>
|
|
185
|
+
Clip to below
|
|
186
|
+
</label>
|
|
187
|
+
<div class="col-span-2 flex flex-wrap gap-2 text-slate-400" role="group" aria-label="Layer locks">
|
|
188
|
+
{#each LOCKS as k (k)}
|
|
189
|
+
<label class="flex items-center gap-1">
|
|
190
|
+
<input
|
|
191
|
+
type="checkbox"
|
|
192
|
+
aria-label={`Lock ${k}`}
|
|
193
|
+
checked={Boolean(active.locks?.[k])}
|
|
194
|
+
onchange={(e) => attempt(() => controller.setLayerProperties(active.id, { locks: { ...active.locks, [k]: (e.target as HTMLInputElement).checked } }))}
|
|
195
|
+
class="accent-indigo-500"
|
|
196
|
+
/>
|
|
197
|
+
{k}
|
|
198
|
+
</label>
|
|
199
|
+
{/each}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
{/if}
|
|
203
|
+
|
|
204
|
+
<ul class="flex flex-col gap-0.5" role="tree" aria-label="Layers">
|
|
205
|
+
{#each snapshot.rows as row (row.layer.id)}
|
|
206
|
+
{@const layer = row.layer}
|
|
207
|
+
<li
|
|
208
|
+
role="treeitem"
|
|
209
|
+
aria-selected={row.active}
|
|
210
|
+
aria-level={row.depth + 1}
|
|
211
|
+
data-layer-id={layer.id}
|
|
212
|
+
draggable={!layer.locks?.all}
|
|
213
|
+
ondragstart={(e) => {
|
|
214
|
+
e.dataTransfer?.setData('text/x-pixerate-layer', layer.id);
|
|
215
|
+
dragId = layer.id;
|
|
216
|
+
}}
|
|
217
|
+
ondragover={(e) => e.preventDefault()}
|
|
218
|
+
ondrop={(e) => onDrop(e, row)}
|
|
219
|
+
tabindex="0"
|
|
220
|
+
onclick={() => attempt(() => controller.setActiveLayer(layer.id, 'pixels'))}
|
|
221
|
+
onkeydown={(e) => {
|
|
222
|
+
if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
attempt(() => controller.setActiveLayer(layer.id, 'pixels'));
|
|
225
|
+
}
|
|
226
|
+
}}
|
|
227
|
+
oncontextmenu={(e) => {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
controller.setActiveLayer(layer.id, snapshot.activeLayerId === layer.id ? snapshot.activeTarget : 'pixels');
|
|
230
|
+
menu = { x: e.clientX, y: e.clientY };
|
|
231
|
+
}}
|
|
232
|
+
class={rowClass(row.active)}
|
|
233
|
+
style="padding-left: {6 + row.depth * 14}px"
|
|
234
|
+
>
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
aria-label={layer.visible ? `Hide ${row.label}` : `Show ${row.label}`}
|
|
238
|
+
aria-pressed={layer.visible}
|
|
239
|
+
class="w-4 text-center text-slate-300"
|
|
240
|
+
onclick={(e) => {
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
attempt(() => controller.setLayerProperties(layer.id, { visible: !layer.visible }));
|
|
243
|
+
}}>{layer.visible ? '●' : '○'}</button>
|
|
244
|
+
{#if layer.type === 'group'}
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
aria-label={layer.collapsed ? `Expand ${row.label}` : `Collapse ${row.label}`}
|
|
248
|
+
class="w-3 text-slate-400"
|
|
249
|
+
onclick={(e) => {
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
attempt(() => controller.setLayerProperties(layer.id, { collapsed: !(layer.type === 'group' && layer.collapsed) }));
|
|
252
|
+
}}>{layer.collapsed ? '▸' : '▾'}</button>
|
|
253
|
+
{/if}
|
|
254
|
+
{#if row.clipped}<span class="text-slate-500" aria-label="Clipped">↳</span>{/if}
|
|
255
|
+
{#if layer.type === 'pixel'}
|
|
256
|
+
<canvas
|
|
257
|
+
use:paint={thumb(layer)}
|
|
258
|
+
aria-label={`${row.label} pixels`}
|
|
259
|
+
style="width: {thumbnailSize}px; height: {thumbnailSize}px"
|
|
260
|
+
class={thumbClass(row.active && snapshot.activeTarget === 'pixels' && Boolean(layer.mask))}
|
|
261
|
+
></canvas>
|
|
262
|
+
{:else}
|
|
263
|
+
<span
|
|
264
|
+
class="shrink-0 grid place-items-center rounded bg-slate-800 text-[9px] text-slate-400 ring-1 ring-slate-700"
|
|
265
|
+
style="width: {thumbnailSize}px; height: {thumbnailSize}px">{layerBadge(layer)}</span>
|
|
266
|
+
{/if}
|
|
267
|
+
{#if layer.mask}
|
|
268
|
+
<canvas
|
|
269
|
+
use:paint={maskThumb(layer)}
|
|
270
|
+
role="button"
|
|
271
|
+
tabindex="0"
|
|
272
|
+
aria-label={`${row.label} mask${layer.mask.enabled === false ? ' (disabled)' : ''}`}
|
|
273
|
+
data-testid="layer-mask-thumbnail"
|
|
274
|
+
style="width: {thumbnailSize}px; height: {thumbnailSize}px"
|
|
275
|
+
class={thumbClass(row.active && snapshot.activeTarget === 'mask')}
|
|
276
|
+
onclick={(e) => {
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
if (e.shiftKey) attempt(() => controller.setLayerMaskProps(layer.id, { enabled: layer.mask!.enabled === false }));
|
|
279
|
+
else attempt(() => controller.setActiveLayer(layer.id, 'mask'));
|
|
280
|
+
}}
|
|
281
|
+
></canvas>
|
|
282
|
+
{/if}
|
|
283
|
+
{#if renaming === layer.id}
|
|
284
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
285
|
+
<input
|
|
286
|
+
autofocus
|
|
287
|
+
aria-label="Layer name"
|
|
288
|
+
value={layer.name || row.label}
|
|
289
|
+
class="flex-1 min-w-0 bg-slate-900 border border-slate-600 rounded px-1"
|
|
290
|
+
onclick={(e) => e.stopPropagation()}
|
|
291
|
+
onblur={(e) => {
|
|
292
|
+
const v = (e.target as HTMLInputElement).value;
|
|
293
|
+
renaming = null;
|
|
294
|
+
if (v.trim() && v !== layer.name) attempt(() => controller.setLayerProperties(layer.id, { name: v }));
|
|
295
|
+
}}
|
|
296
|
+
onkeydown={(e) => {
|
|
297
|
+
if (e.key === 'Enter') (e.target as HTMLInputElement).blur();
|
|
298
|
+
if (e.key === 'Escape') renaming = null;
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
{:else}
|
|
302
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
303
|
+
<span
|
|
304
|
+
class="flex-1 min-w-0 truncate {layer.visible ? '' : 'text-slate-500'}"
|
|
305
|
+
ondblclick={(e) => {
|
|
306
|
+
e.stopPropagation();
|
|
307
|
+
renaming = layer.id;
|
|
308
|
+
}}>{row.label}</span>
|
|
309
|
+
{/if}
|
|
310
|
+
{#if locked(layer)}<span aria-label="Locked" class="text-slate-400">🔒</span>{/if}
|
|
311
|
+
{#if layer.blend !== 'normal' && layer.blend !== 'pass-through'}<span class="text-[9px] text-slate-500">{layer.blend}</span>{/if}
|
|
312
|
+
</li>
|
|
313
|
+
{/each}
|
|
314
|
+
</ul>
|
|
315
|
+
|
|
316
|
+
{#if error}
|
|
317
|
+
<p role="alert" class="text-rose-400">{error}</p>
|
|
318
|
+
{/if}
|
|
319
|
+
|
|
320
|
+
{#if menu}
|
|
321
|
+
<div
|
|
322
|
+
role="menu"
|
|
323
|
+
tabindex="-1"
|
|
324
|
+
aria-label="Layer menu"
|
|
325
|
+
class="fixed z-50 min-w-44 rounded border border-slate-700 bg-slate-900 py-1 shadow-xl"
|
|
326
|
+
style="left: {menu.x}px; top: {menu.y}px"
|
|
327
|
+
onclick={(e) => e.stopPropagation()}
|
|
328
|
+
onkeydown={(e) => e.key === 'Escape' && (menu = null)}
|
|
329
|
+
>
|
|
330
|
+
{#each snapshot.menu as item (item.action)}
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
role="menuitem"
|
|
334
|
+
disabled={!item.enabled}
|
|
335
|
+
class="block w-full px-3 py-1 text-left hover:bg-slate-800 disabled:opacity-40"
|
|
336
|
+
onclick={() => run(item.action)}>{item.label}</button>
|
|
337
|
+
{/each}
|
|
338
|
+
</div>
|
|
339
|
+
{/if}
|
|
340
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const LayersPanel: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type LayersPanel = ReturnType<typeof LayersPanel>;
|
|
10
|
+
export default LayersPanel;
|
|
11
|
+
//# 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;AAKlI,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,yCAAyC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsOH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,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 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>;
|
|
@@ -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,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;oCAGhC,MAAM;4CAGE,MAAM;IAG9C,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
|
},
|
|
@@ -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.68.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.89.0"
|
|
135
135
|
},
|
|
136
136
|
"peerDependencies": {
|
|
137
137
|
"@dagrejs/dagre": "^1.1.4",
|