@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.
@@ -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 on the untransformed source image (natural pixels); crop, rotation,
98
- // adjustments and annotations stay non-destructive and are re-applied on top.
99
- const imageDataUrl = await editor.toSourceDataURL();
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.toSourceDataURL();
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
- class="absolute inset-0 w-full h-full block"
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,EACnB,MAAM,+BAA+B,CAAC;AAqBzC,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;CAC9B;AA8iCH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
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;AAqGH,QAAA,MAAM,mBAAmB,2CAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
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
- applyInpaintedImage(newDataUrl: string): Promise<void>;
58
- applyBackgroundRemovedImage(newDataUrl: string): Promise<void>;
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;mBAGhB,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"}
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
- applyInpaintedImage(newDataUrl) {
157
- return controller.applyInpaintedImage(newDataUrl);
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
- applyBackgroundRemovedImage(newDataUrl) {
160
- return controller.applyBackgroundRemovedImage(newDataUrl);
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.67.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.88.0"
134
+ "@pixerate/editor": "0.90.0"
135
135
  },
136
136
  "peerDependencies": {
137
137
  "@dagrejs/dagre": "^1.1.4",