@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.
@@ -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
- class="absolute inset-0 w-full h-full block"
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,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;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;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,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.67.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.88.0"
134
+ "@pixerate/editor": "0.89.0"
135
135
  },
136
136
  "peerDependencies": {
137
137
  "@dagrejs/dagre": "^1.1.4",