@pixerate/editor-svelte 0.115.0 → 0.117.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,171 @@
1
+ <script lang="ts">
2
+ import type { ImageEditorController, TransformAnchor } from '@pixerate/editor/image-editor';
3
+ import { CANVAS_SIZE_PRESETS, isTransformAnchor } from '@pixerate/editor/image-editor';
4
+ import { untrack } from 'svelte';
5
+ import { imagePrimaryButtonClass, imageSelectClass } from './styles.js';
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
+ return untrack(() => c.subscribe(() => tick++));
18
+ });
19
+
20
+ const natural = $derived.by(() => {
21
+ void tick;
22
+ return controller.getCanvasSize();
23
+ });
24
+
25
+ let width = $state(800);
26
+ let height = $state(600);
27
+ let lockAspect = $state(false);
28
+ let anchor = $state<TransformAnchor>('center');
29
+ let error = $state<string | null>(null);
30
+
31
+ // Sync initial dimensions with current canvas size
32
+ $effect(() => {
33
+ width = natural.width;
34
+ height = natural.height;
35
+ });
36
+
37
+ const ANCHOR_GRID: TransformAnchor[][] = [
38
+ ['top-left', 'top', 'top-right'],
39
+ ['left', 'center', 'right'],
40
+ ['bottom-left', 'bottom', 'bottom-right'],
41
+ ];
42
+
43
+ function onWidthChange(val: number) {
44
+ if (lockAspect && natural.width > 0) {
45
+ const ratio = natural.height / natural.width;
46
+ height = Math.max(1, Math.round(val * ratio));
47
+ }
48
+ width = val;
49
+ }
50
+
51
+ function onHeightChange(val: number) {
52
+ if (lockAspect && natural.height > 0) {
53
+ const ratio = natural.width / natural.height;
54
+ width = Math.max(1, Math.round(val * ratio));
55
+ }
56
+ height = val;
57
+ }
58
+
59
+ function onPresetSelect(e: Event) {
60
+ const target = e.currentTarget as HTMLSelectElement;
61
+ const presetId = target.value;
62
+ target.value = '';
63
+ const preset = CANVAS_SIZE_PRESETS.find((p) => p.id === presetId);
64
+ if (!preset) return;
65
+ width = preset.width;
66
+ height = preset.height;
67
+ }
68
+
69
+
70
+ function apply() {
71
+ error = null;
72
+ try {
73
+ controller.resizeCanvas({ width, height, anchor });
74
+ } catch (err) {
75
+ error = err instanceof Error ? err.message : String(err);
76
+ }
77
+ }
78
+ </script>
79
+
80
+ <div class="flex flex-col gap-3 text-xs text-foreground {className}" data-image-panel="canvas-size" data-testid="canvas-size-panel">
81
+ <div class="font-medium text-sm text-foreground">Canvas Size</div>
82
+
83
+ {#if error}
84
+ <div class="text-xs text-destructive bg-destructive/10 p-1.5 rounded">{error}</div>
85
+ {/if}
86
+
87
+ <div class="flex flex-col gap-1.5">
88
+ <label for="canvas-size-preset" class="text-muted-foreground text-[11px]">Preset</label>
89
+ <select
90
+ id="canvas-size-preset"
91
+ class="{imageSelectClass} w-full"
92
+ onchange={onPresetSelect}
93
+ aria-label="Preset canvas sizes"
94
+ >
95
+ <option value="">Choose preset…</option>
96
+ {#each CANVAS_SIZE_PRESETS as preset (preset.id)}
97
+ <option value={preset.id}>{preset.label} ({preset.width} × {preset.height})</option>
98
+ {/each}
99
+ </select>
100
+ </div>
101
+
102
+ <div class="grid grid-cols-2 gap-2 items-center">
103
+ <div class="flex flex-col gap-1">
104
+ <label for="canvas-size-width" class="text-muted-foreground text-[11px]">Width (px)</label>
105
+ <input
106
+ id="canvas-size-width"
107
+ type="number"
108
+ min="1"
109
+ max="16384"
110
+ class="w-full bg-background border border-border rounded px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
111
+ value={width}
112
+ oninput={(e) => onWidthChange(Number((e.currentTarget as HTMLInputElement).value))}
113
+ />
114
+ </div>
115
+
116
+ <div class="flex flex-col gap-1">
117
+ <label for="canvas-size-height" class="text-muted-foreground text-[11px]">Height (px)</label>
118
+ <input
119
+ id="canvas-size-height"
120
+ type="number"
121
+ min="1"
122
+ max="16384"
123
+ class="w-full bg-background border border-border rounded px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
124
+ value={height}
125
+ oninput={(e) => onHeightChange(Number((e.currentTarget as HTMLInputElement).value))}
126
+ />
127
+ </div>
128
+ </div>
129
+
130
+ <div class="flex items-center gap-2">
131
+ <label class="flex items-center gap-1.5 cursor-pointer text-muted-foreground select-none">
132
+ <input
133
+ type="checkbox"
134
+ class="rounded border-border text-primary focus:ring-primary"
135
+ checked={lockAspect}
136
+ onchange={(e) => (lockAspect = (e.currentTarget as HTMLInputElement).checked)}
137
+ />
138
+ <span>Lock aspect ratio</span>
139
+ </label>
140
+ </div>
141
+
142
+ <div class="flex flex-col gap-1.5">
143
+ <span class="text-muted-foreground text-[11px]">Anchor</span>
144
+ <div class="grid grid-cols-3 gap-1 w-24 h-24 p-1 bg-muted/40 border border-border rounded" role="radiogroup" aria-label="Resize anchor">
145
+ {#each ANCHOR_GRID as row}
146
+ {#each row as a (a)}
147
+ <button
148
+ type="button"
149
+ role="radio"
150
+ aria-checked={anchor === a}
151
+ aria-label={a}
152
+ class="w-full h-full rounded flex items-center justify-center transition-colors {anchor === a ? 'bg-primary text-primary-foreground' : 'bg-background hover:bg-muted text-muted-foreground'}"
153
+ onclick={() => (anchor = a)}
154
+ >
155
+ <div class="w-1.5 h-1.5 rounded-full {anchor === a ? 'bg-primary-foreground' : 'bg-muted-foreground/60'}"></div>
156
+ </button>
157
+ {/each}
158
+ {/each}
159
+ </div>
160
+ </div>
161
+
162
+ <div class="pt-2">
163
+ <button
164
+ type="button"
165
+ class="{imagePrimaryButtonClass} w-full"
166
+ onclick={apply}
167
+ >
168
+ Apply Canvas Size
169
+ </button>
170
+ </div>
171
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { ImageEditorController } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ controller: ImageEditorController;
4
+ class?: string;
5
+ }
6
+ declare const CanvasSizePanel: import("svelte").Component<Props, {}, "">;
7
+ type CanvasSizePanel = ReturnType<typeof CanvasSizePanel>;
8
+ export default CanvasSizePanel;
@@ -16,6 +16,8 @@
16
16
  handlePaintShortcut,
17
17
  pointerPressure,
18
18
  FreeTransformTool,
19
+ boxFrameHit,
20
+ boxFrameOverlay,
19
21
  getViewportProjection,
20
22
  measureCanvasViewport,
21
23
  watchCanvasViewport,
@@ -26,6 +28,7 @@
26
28
  type SelectionToolOptions,
27
29
  type ShapeToolOptions,
28
30
  type TextEditSession,
31
+ type TransformOverlay,
29
32
  type TypeToolOptions,
30
33
  } from '@pixerate/editor/image-editor';
31
34
  import { untrack } from 'svelte';
@@ -86,8 +89,10 @@
86
89
  locate?: (ref: PlaybackRef) => PlaybackRect | null;
87
90
  /** Your own element for a playback ref, anywhere on the page; its box is mapped to document pixels. */
88
91
  locateElement?: (ref: PlaybackRef) => Element | null;
89
- /** Show a panel for an `open` step (default: click its `[data-image-tab]` under `chromeRoot`). */
92
+ /** Show a panel for an `open` step (default: click its `[data-image-tab]` or `[data-image-panel]` under `chromeRoot`). */
90
93
  open?: (ref: PlaybackRef) => void | Promise<void>;
94
+ /** Whether wheel zoom and middle/space drag panning are enabled (default true). */
95
+ viewportGestures?: boolean;
91
96
  class?: string;
92
97
  style?: string;
93
98
  }
@@ -112,10 +117,16 @@
112
117
  locate,
113
118
  locateElement,
114
119
  open,
120
+ viewportGestures = true,
115
121
  class: className = '',
116
122
  style = '',
117
123
  }: Props = $props();
118
124
 
125
+ /** Fits the image to view, resetting zoom to 1 and pan to (0, 0). */
126
+ export function fitToView(): void {
127
+ editor.controller.resetZoom();
128
+ }
129
+
119
130
  let canvasEl = $state<HTMLCanvasElement | null>(null);
120
131
  let containerEl = $state<HTMLDivElement | null>(null);
121
132
  let rootEl = $state<HTMLDivElement | null>(null);
@@ -187,7 +198,64 @@
187
198
  reveal: () => onplaybackreveal?.(),
188
199
  }));
189
200
 
190
- // Selection tools (magic wand, marquees, lasso): the pointer gesture.
201
+ let isSpacePressed = $state(false);
202
+ let isPanning = $state(false);
203
+ let panStartPoint: { clientX: number; clientY: number } | null = null;
204
+ let panStartOffset: { x: number; y: number } = { x: 0, y: 0 };
205
+
206
+ function handleWheel(e: WheelEvent) {
207
+ if (!viewportGestures || !canvasEl) return;
208
+ e.preventDefault();
209
+
210
+ const rect = canvasEl.getBoundingClientRect();
211
+ const sx = rect.width > 0 ? view.width / rect.width : 1;
212
+ const sy = rect.height > 0 ? view.height / rect.height : 1;
213
+ const canvasPt = {
214
+ x: (e.clientX - rect.left) * sx,
215
+ y: (e.clientY - rect.top) * sy,
216
+ };
217
+
218
+ const s = editor.controller.getState();
219
+ const currentZoom = s.zoom || 1;
220
+ const factor = e.deltaY < 0 ? 1.1 : 0.9;
221
+ const nextZoom = Math.min(10, Math.max(0.1, currentZoom * factor));
222
+
223
+ // Zoom centered at cursor:
224
+ // in canvas coordinates: img = (canvasPt - pan - center) / zoom
225
+ // We want canvasPt to remain invariant after zoom:
226
+ // (canvasPt - center - newPan) / newZoom = (canvasPt - center - oldPan) / oldZoom
227
+ // newPan = canvasPt - center - (canvasPt - center - oldPan) * (newZoom / oldZoom)
228
+ const centerX = view.width / 2;
229
+ const centerY = view.height / 2;
230
+ const curPan = s.pan || { x: 0, y: 0 };
231
+ const ratio = nextZoom / currentZoom;
232
+
233
+ const newPanX = canvasPt.x - centerX - (canvasPt.x - centerX - curPan.x) * ratio;
234
+ const newPanY = canvasPt.y - centerY - (canvasPt.y - centerY - curPan.y) * ratio;
235
+
236
+ editor.controller.setZoom(nextZoom);
237
+ editor.controller.setPan({ x: Math.round(newPanX), y: Math.round(newPanY) });
238
+ }
239
+
240
+ $effect(() => {
241
+ if (typeof window === 'undefined') return;
242
+ const onKeyDown = (e: KeyboardEvent) => {
243
+ if (e.code === 'Space' && !e.repeat && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
244
+ isSpacePressed = true;
245
+ }
246
+ };
247
+ const onKeyUp = (e: KeyboardEvent) => {
248
+ if (e.code === 'Space') {
249
+ isSpacePressed = false;
250
+ }
251
+ };
252
+ window.addEventListener('keydown', onKeyDown);
253
+ window.addEventListener('keyup', onKeyUp);
254
+ return () => {
255
+ window.removeEventListener('keydown', onKeyDown);
256
+ window.removeEventListener('keyup', onKeyUp);
257
+ };
258
+ });
191
259
  const selectionGesture = $derived(new SelectionGesture(editor.controller, () => selectionOptions));
192
260
  let selectionDraft = $state.raw<ImagePoint[] | null>(null);
193
261
  // Paint tools (Move, Brush, Eraser): the pointer gesture and the brush cursor.
@@ -278,6 +346,63 @@
278
346
  return () => window.removeEventListener('keydown', onKey);
279
347
  });
280
348
 
349
+ /** How far outside a frame (in hit tolerances) a drag rotates instead of picking a layer. */
350
+ const ROTATE_BAND = 4;
351
+
352
+ /**
353
+ * Transform mode, with no transform open: the active layer's frame, drawn as a transform's.
354
+ * A layer locked in place gets the outline without handles.
355
+ */
356
+ function layerFrame(): TransformOverlay | null {
357
+ if (mode !== 'transform' || transformTool.active) return null;
358
+ const c = editor.controller;
359
+ const id = c.getState().activeLayerId;
360
+ const layer = id ? c.getLayer(id) : undefined;
361
+ if (!layer || layer.visible === false) return null;
362
+ const bounds = c.getLayerBounds(layer.id);
363
+ if (!bounds) return null;
364
+ return boxFrameOverlay(bounds, !(layer.locks?.all || layer.locks?.position));
365
+ }
366
+
367
+ /**
368
+ * Transform mode: a drag on the active layer's frame transforms it, and a press elsewhere picks
369
+ * the layer there (or none) and drags that. Each drag opens a Free Transform and closes it on
370
+ * release, one undo step when it changed something, so other edits never meet an open one.
371
+ */
372
+ let frameDrag = false;
373
+ function handleFramePointerDown(e: PointerEvent): boolean {
374
+ if (mode !== 'transform' || transformTool.active || !canvasEl) return false;
375
+ // Middle-button and Space drags pan (see handleMouseDown).
376
+ if (e.button !== 0 || (viewportGestures && isSpacePressed)) return false;
377
+ const c = editor.controller;
378
+ const { pt, tolerance } = transformPoint(e);
379
+ const frame = layerFrame();
380
+ let target = frame && boxFrameHit(frame, pt, tolerance, tolerance * ROTATE_BAND) ? c.getState().activeLayerId : null;
381
+ if (!target) {
382
+ target = c.layerAt(pt);
383
+ c.setActiveLayer(target);
384
+ }
385
+ const layer = target ? c.getLayer(target) : undefined;
386
+ if (!layer || layer.locks?.all || layer.locks?.position) return true;
387
+ e.preventDefault();
388
+ if (transformTool.begin({ layerId: layer.id })) return true;
389
+ try {
390
+ canvasEl.setPointerCapture?.(e.pointerId);
391
+ } catch {
392
+ // Synthetic events have no active pointer to capture.
393
+ }
394
+ transformTool.down(pt, tolerance);
395
+ frameDrag = true;
396
+ return true;
397
+ }
398
+ function endFrameDrag(cancelled = false) {
399
+ if (!frameDrag) return;
400
+ frameDrag = false;
401
+ if (!transformTool.active) return;
402
+ if (!cancelled && transformTool.modified) transformTool.commit();
403
+ else transformTool.cancel();
404
+ }
405
+
281
406
  /** The document point of a pointer event and the hit tolerance (6 screen px). */
282
407
  function transformPoint(e: PointerEvent) {
283
408
  const rect = canvasEl!.getBoundingClientRect();
@@ -383,7 +508,7 @@
383
508
  mode === 'paint' && brushCursor && editor.controller.getState().activeTool !== 'move' && !transformTool.active
384
509
  ? { ...brushCursor, diameter: editor.controller.getState().paint.brush.size }
385
510
  : null,
386
- freeTransform: transformTool.overlay(),
511
+ freeTransform: transformTool.overlay() ?? layerFrame(),
387
512
  textEdit: typeTool.session?.alive ? typeTool.session.overlay() : null,
388
513
  });
389
514
  }
@@ -403,6 +528,7 @@
403
528
  }
404
529
  function handlePaintPointerDown(e: PointerEvent) {
405
530
  if (handleTransformPointerDown(e)) return;
531
+ if (handleFramePointerDown(e)) return;
406
532
  if (mode !== 'paint' || e.button !== 0 || !canvasEl) return;
407
533
  e.preventDefault();
408
534
  try {
@@ -414,6 +540,12 @@
414
540
  }
415
541
  function handlePaintPointerMove(e: PointerEvent) {
416
542
  if (handleTransformPointerMove(e)) return;
543
+ if (mode === 'transform' && canvasEl) {
544
+ const { pt, tolerance } = transformPoint(e);
545
+ const frame = layerFrame();
546
+ transformCursor = frame ? transformTool.cursor(boxFrameHit(frame, pt, tolerance, tolerance * ROTATE_BAND)) : 'default';
547
+ return;
548
+ }
417
549
  if (mode !== 'paint' || !canvasEl) return;
418
550
  const samples = paintSamples(e);
419
551
  brushCursor = samples[samples.length - 1] ?? null;
@@ -421,7 +553,10 @@
421
553
  else void renderCanvas();
422
554
  }
423
555
  function handlePaintPointerUp() {
424
- if (handleTransformPointerUp()) return;
556
+ if (handleTransformPointerUp()) {
557
+ endFrameDrag();
558
+ return;
559
+ }
425
560
  if (mode !== 'paint' || !paintGesture.active) return;
426
561
  paintGesture.up();
427
562
  if (paintGesture.error) report('paint', paintGesture.error);
@@ -476,7 +611,21 @@
476
611
  }
477
612
 
478
613
  function handleMouseDown(e: MouseEvent) {
479
- if (!canvasEl || transformTool.active) return;
614
+ if (!canvasEl) return;
615
+
616
+ if (viewportGestures && (e.button === 1 || (e.button === 0 && isSpacePressed))) {
617
+ e.preventDefault();
618
+ isPanning = true;
619
+ panStartPoint = { clientX: e.clientX, clientY: e.clientY };
620
+ const curPan = editor.controller.getState().pan || { x: 0, y: 0 };
621
+ panStartOffset = { ...curPan };
622
+ return;
623
+ }
624
+
625
+ // Transform mode picks and drags layers on pointerdown (handleFramePointerDown).
626
+ if (mode === 'transform') return;
627
+
628
+ if (transformTool.active) return;
480
629
  const canvasPt = getCanvasEventPoint(e);
481
630
 
482
631
  if (mode === 'select') {
@@ -567,7 +716,23 @@
567
716
  }
568
717
 
569
718
  function handleMouseMove(e: MouseEvent) {
570
- if (!canvasEl || transformTool.active) return;
719
+ if (!canvasEl) return;
720
+
721
+ if (isPanning && panStartPoint) {
722
+ const dx = e.clientX - panStartPoint.clientX;
723
+ const dy = e.clientY - panStartPoint.clientY;
724
+ editor.controller.setPan({
725
+ x: Math.round(panStartOffset.x + dx),
726
+ y: Math.round(panStartOffset.y + dy),
727
+ });
728
+ return;
729
+ }
730
+
731
+ if (viewportGestures && isSpacePressed) {
732
+ canvasEl.style.cursor = 'grab';
733
+ }
734
+
735
+ if (transformTool.active) return;
571
736
  const canvasPt = getCanvasEventPoint(e);
572
737
 
573
738
  if (mode === 'select') {
@@ -713,6 +878,11 @@
713
878
  }
714
879
 
715
880
  function handleMouseUp(e: MouseEvent) {
881
+ if (isPanning) {
882
+ isPanning = false;
883
+ panStartPoint = null;
884
+ return;
885
+ }
716
886
  if (!canvasEl || transformTool.active) return;
717
887
  const canvasPt = getCanvasEventPoint(e);
718
888
 
@@ -822,7 +992,12 @@
822
992
  React's `<ImageEditorCanvas>`.
823
993
  -->
824
994
  <div bind:this={rootEl} class="relative {className}" {style} data-testid="image-editor-canvas-root">
825
- <div bind:this={containerEl} class="absolute inset-0 overflow-hidden select-none">
995
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
996
+ <div
997
+ bind:this={containerEl}
998
+ onwheel={handleWheel}
999
+ class="absolute inset-0 overflow-hidden select-none"
1000
+ >
826
1001
  <canvas
827
1002
  bind:this={canvasEl}
828
1003
  onmousedown={handleMouseDown}
@@ -832,10 +1007,13 @@
832
1007
  onpointerdown={handlePaintPointerDown}
833
1008
  onpointermove={handlePaintPointerMove}
834
1009
  onpointerup={handlePaintPointerUp}
835
- onpointercancel={() => paintGesture.cancel()}
1010
+ onpointercancel={() => {
1011
+ paintGesture.cancel();
1012
+ endFrameDrag(true);
1013
+ }}
836
1014
  data-testid="image-editor-canvas"
837
- style="width: 100%; height: 100%;{transformActive ? ` cursor: ${transformCursor};` : ''}"
838
- class="absolute inset-0 w-full h-full block {transformActive ? 'touch-none' : mode === 'paint' ? 'touch-none cursor-crosshair' : mode === 'annotate' && editor.state.activeTool === 'type' ? 'cursor-text' : mode === 'annotate' && editor.state.activeTool === 'shape' ? 'cursor-crosshair' : ''}"
1015
+ style="width: 100%; height: 100%;{transformActive || mode === 'transform' ? ` cursor: ${transformCursor};` : ''}"
1016
+ class="absolute inset-0 w-full h-full block {transformActive || mode === 'transform' ? 'touch-none' : mode === 'paint' ? 'touch-none cursor-crosshair' : mode === 'annotate' && editor.state.activeTool === 'type' ? 'cursor-text' : mode === 'annotate' && editor.state.activeTool === 'shape' ? 'cursor-crosshair' : ''}"
839
1017
  ></canvas>
840
1018
  {#if typeSession}
841
1019
  <textarea
@@ -54,11 +54,15 @@ interface Props {
54
54
  locate?: (ref: PlaybackRef) => PlaybackRect | null;
55
55
  /** Your own element for a playback ref, anywhere on the page; its box is mapped to document pixels. */
56
56
  locateElement?: (ref: PlaybackRef) => Element | null;
57
- /** Show a panel for an `open` step (default: click its `[data-image-tab]` under `chromeRoot`). */
57
+ /** Show a panel for an `open` step (default: click its `[data-image-tab]` or `[data-image-panel]` under `chromeRoot`). */
58
58
  open?: (ref: PlaybackRef) => void | Promise<void>;
59
+ /** Whether wheel zoom and middle/space drag panning are enabled (default true). */
60
+ viewportGestures?: boolean;
59
61
  class?: string;
60
62
  style?: string;
61
63
  }
62
- declare const ImageEditorCanvas: import("svelte").Component<Props, {}, "typeSession">;
64
+ declare const ImageEditorCanvas: import("svelte").Component<Props, {
65
+ fitToView: () => void;
66
+ }, "typeSession">;
63
67
  type ImageEditorCanvas = ReturnType<typeof ImageEditorCanvas>;
64
68
  export default ImageEditorCanvas;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import type { ImageEditorController, ImageLayer } from '@pixerate/editor/image-editor';
3
+ import {
4
+ ADJUSTMENT_LAYER_OPACITY_CONTROL,
5
+ DEFAULT_SHAPE_TOOL_OPTIONS,
6
+ DEFAULT_TYPE_TOOL_OPTIONS,
7
+ DEFAULT_SELECTION_TOOL_OPTIONS,
8
+ } from '@pixerate/editor/image-editor';
9
+ import CanvasSizePanel from './CanvasSizePanel.svelte';
10
+ import ControlSlider from './ControlSlider.svelte';
11
+ import LayerEffectsPanel from './LayerEffectsPanel.svelte';
12
+ import BrushOptionsBar from './BrushOptionsBar.svelte';
13
+ import ShapeOptionsBar from './ShapeOptionsBar.svelte';
14
+ import TypeOptionsBar from './TypeOptionsBar.svelte';
15
+ import AdjustmentLayersPanel from './AdjustmentLayersPanel.svelte';
16
+ import SelectionOptionsBar from './SelectionOptionsBar.svelte';
17
+
18
+ interface Props {
19
+ controller: ImageEditorController;
20
+ panelId: string;
21
+ class?: string;
22
+ }
23
+
24
+ let { controller, panelId, class: className = '' }: Props = $props();
25
+
26
+ const activeLayer = $derived(controller.getActiveLayer());
27
+ let shapeOptions = $state({ ...DEFAULT_SHAPE_TOOL_OPTIONS });
28
+ let typeOptions = $state({ ...DEFAULT_TYPE_TOOL_OPTIONS });
29
+ let selectionOptions = $state({ ...DEFAULT_SELECTION_TOOL_OPTIONS });
30
+ </script>
31
+
32
+ <div class="layer-panel-container {className}" data-image-panel={panelId} data-testid="layer-panel-{panelId}">
33
+ {#if panelId === 'canvas-size'}
34
+ <CanvasSizePanel {controller} />
35
+ {:else if panelId === 'properties'}
36
+ <div class="flex flex-col gap-3 text-xs text-foreground">
37
+ <div class="font-medium text-sm">Properties</div>
38
+ {#if activeLayer}
39
+ <div class="flex flex-col gap-2">
40
+ <div class="flex items-center justify-between">
41
+ <span class="text-muted-foreground">Name</span>
42
+ <span class="font-medium">{activeLayer.name || activeLayer.id}</span>
43
+ </div>
44
+ <div class="flex items-center justify-between">
45
+ <span class="text-muted-foreground">Type</span>
46
+ <span class="capitalize">{activeLayer.type}</span>
47
+ </div>
48
+ <div class="flex items-center justify-between">
49
+ <span class="text-muted-foreground">Blend Mode</span>
50
+ <span class="capitalize">{activeLayer.blend}</span>
51
+ </div>
52
+ <ControlSlider
53
+ spec={ADJUSTMENT_LAYER_OPACITY_CONTROL}
54
+ label="Opacity"
55
+ value={Math.round(activeLayer.opacity * 100)}
56
+ onchange={(val) => controller.setLayerProperties(activeLayer.id, { opacity: val / 100 })}
57
+ />
58
+ </div>
59
+ {:else}
60
+ <div class="text-muted-foreground">No active layer selected.</div>
61
+ {/if}
62
+ </div>
63
+ {:else if panelId === 'paint'}
64
+ <div class="flex flex-col gap-3">
65
+ <div class="font-medium text-sm text-foreground">Brush & Paint</div>
66
+ <BrushOptionsBar {controller} />
67
+ </div>
68
+ {:else if panelId === 'effects'}
69
+ <div class="flex flex-col gap-3">
70
+ <div class="font-medium text-sm text-foreground">Layer Effects</div>
71
+ <LayerEffectsPanel {controller} />
72
+ </div>
73
+ {:else if panelId === 'adjustment'}
74
+ <AdjustmentLayersPanel {controller} />
75
+ {:else if panelId === 'type'}
76
+ <div class="flex flex-col gap-3">
77
+ <div class="font-medium text-sm text-foreground">Typography</div>
78
+ <TypeOptionsBar {controller} options={typeOptions} onchange={(o) => (typeOptions = o)} />
79
+ </div>
80
+ {:else if panelId === 'shape'}
81
+ <div class="flex flex-col gap-3">
82
+ <div class="font-medium text-sm text-foreground">Shape Options</div>
83
+ <ShapeOptionsBar {controller} options={shapeOptions} onchange={(o) => (shapeOptions = o)} />
84
+ </div>
85
+ {:else if panelId === 'smart-filters'}
86
+ <div class="flex flex-col gap-3">
87
+ <div class="font-medium text-sm text-foreground">Smart Filters</div>
88
+ <LayerEffectsPanel {controller} />
89
+ </div>
90
+ {:else if panelId === 'selection'}
91
+ <div class="flex flex-col gap-3">
92
+ <div class="font-medium text-sm text-foreground">Selection</div>
93
+ <SelectionOptionsBar {controller} options={selectionOptions} onOptionsChange={(o) => (selectionOptions = o)} />
94
+ </div>
95
+ {:else}
96
+ <div class="text-xs text-muted-foreground">Panel {panelId}</div>
97
+ {/if}
98
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { ImageEditorController } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ controller: ImageEditorController;
4
+ panelId: string;
5
+ class?: string;
6
+ }
7
+ declare const LayerPanel: import("svelte").Component<Props, {}, "">;
8
+ type LayerPanel = ReturnType<typeof LayerPanel>;
9
+ export default LayerPanel;
@@ -1,4 +1,4 @@
1
1
  /** What pointer gestures on `ImageEditorCanvas` do (and whether the crop overlay shows). */
2
- export type ImageEditorCanvasMode = 'crop' | 'select' | 'paint' | 'annotate' | 'none';
2
+ export type ImageEditorCanvasMode = 'crop' | 'select' | 'paint' | 'annotate' | 'transform' | 'none';
3
3
  /** Which gesture an `ImageEditorCanvas` `onerror` report comes from. */
4
- export type ImageEditorCanvasErrorSource = 'paint' | 'select' | 'shape' | 'type';
4
+ export type ImageEditorCanvasErrorSource = 'paint' | 'select' | 'shape' | 'type' | 'transform';
@@ -16,8 +16,12 @@ import type FreeTransformOptionsBarComponent from "./FreeTransformOptionsBar.sve
16
16
  import type ShapeOptionsBarComponent from "./ShapeOptionsBar.svelte";
17
17
  import type TypeOptionsBarComponent from "./TypeOptionsBar.svelte";
18
18
  import type ImageProviderEffectsPanelComponent from "./ImageProviderEffectsPanel.svelte";
19
+ import type CanvasSizePanelComponent from "./CanvasSizePanel.svelte";
20
+ import type LayerPanelComponent from "./LayerPanel.svelte";
19
21
  export { default as ImageEditor } from './ImageEditor.svelte';
20
22
  export { default as ImageEditorCanvas } from './ImageEditorCanvas.svelte';
23
+ export { default as CanvasSizePanel } from './CanvasSizePanel.svelte';
24
+ export { default as LayerPanel } from './LayerPanel.svelte';
21
25
  export { default as ImageEffectControls } from './ImageEffectControls.svelte';
22
26
  export type { ImageEditorCanvasMode, ImageEditorCanvasErrorSource } from './canvasTypes.js';
23
27
  export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
@@ -38,6 +42,8 @@ export { default as ImageProviderEffectsPanel } from './ImageProviderEffectsPane
38
42
  export * from './imageEditorState.svelte.js';
39
43
  export type ImageEditorProps = ComponentProps<typeof ImageEditorComponent>;
40
44
  export type ImageEditorCanvasProps = ComponentProps<typeof ImageEditorCanvasComponent>;
45
+ export type CanvasSizePanelProps = ComponentProps<typeof CanvasSizePanelComponent>;
46
+ export type LayerPanelPropsComponent = ComponentProps<typeof LayerPanelComponent>;
41
47
  export type ImageEffectControlsProps = ComponentProps<typeof ImageEffectControlsComponent>;
42
48
  export type HistogramProps = ComponentProps<typeof HistogramComponent>;
43
49
  export type AdjustmentLayersPanelProps = ComponentProps<typeof AdjustmentLayersPanelComponent>;
@@ -1,5 +1,7 @@
1
1
  export { default as ImageEditor } from './ImageEditor.svelte';
2
2
  export { default as ImageEditorCanvas } from './ImageEditorCanvas.svelte';
3
+ export { default as CanvasSizePanel } from './CanvasSizePanel.svelte';
4
+ export { default as LayerPanel } from './LayerPanel.svelte';
3
5
  export { default as ImageEffectControls } from './ImageEffectControls.svelte';
4
6
  export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
5
7
  export { default as Histogram } from './Histogram.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.115.0",
3
+ "version": "0.117.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -160,7 +160,7 @@
160
160
  "@tiptap/extension-bubble-menu": "^2.27.3",
161
161
  "@tiptap/extension-placeholder": "^2.27.3",
162
162
  "@tiptap/starter-kit": "^2.27.3",
163
- "@pixerate/editor": "0.157.0"
163
+ "@pixerate/editor": "0.159.0"
164
164
  },
165
165
  "peerDependencies": {
166
166
  "@dagrejs/dagre": "^1.1.4",