@pixerate/editor-svelte 0.115.0 → 0.116.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-editor/CanvasSizePanel.svelte +171 -0
- package/dist/image-editor/CanvasSizePanel.svelte.d.ts +8 -0
- package/dist/image-editor/ImageEditorCanvas.svelte +125 -5
- package/dist/image-editor/ImageEditorCanvas.svelte.d.ts +6 -2
- package/dist/image-editor/LayerPanel.svelte +98 -0
- package/dist/image-editor/LayerPanel.svelte.d.ts +9 -0
- package/dist/image-editor/canvasTypes.d.ts +2 -2
- package/dist/image-editor/index.d.ts +6 -0
- package/dist/image-editor/index.js +2 -0
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -86,8 +86,10 @@
|
|
|
86
86
|
locate?: (ref: PlaybackRef) => PlaybackRect | null;
|
|
87
87
|
/** Your own element for a playback ref, anywhere on the page; its box is mapped to document pixels. */
|
|
88
88
|
locateElement?: (ref: PlaybackRef) => Element | null;
|
|
89
|
-
/** Show a panel for an `open` step (default: click its `[data-image-tab]` under `chromeRoot`). */
|
|
89
|
+
/** Show a panel for an `open` step (default: click its `[data-image-tab]` or `[data-image-panel]` under `chromeRoot`). */
|
|
90
90
|
open?: (ref: PlaybackRef) => void | Promise<void>;
|
|
91
|
+
/** Whether wheel zoom and middle/space drag panning are enabled (default true). */
|
|
92
|
+
viewportGestures?: boolean;
|
|
91
93
|
class?: string;
|
|
92
94
|
style?: string;
|
|
93
95
|
}
|
|
@@ -112,10 +114,16 @@
|
|
|
112
114
|
locate,
|
|
113
115
|
locateElement,
|
|
114
116
|
open,
|
|
117
|
+
viewportGestures = true,
|
|
115
118
|
class: className = '',
|
|
116
119
|
style = '',
|
|
117
120
|
}: Props = $props();
|
|
118
121
|
|
|
122
|
+
/** Fits the image to view, resetting zoom to 1 and pan to (0, 0). */
|
|
123
|
+
export function fitToView(): void {
|
|
124
|
+
editor.controller.resetZoom();
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
120
128
|
let containerEl = $state<HTMLDivElement | null>(null);
|
|
121
129
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
@@ -187,7 +195,64 @@
|
|
|
187
195
|
reveal: () => onplaybackreveal?.(),
|
|
188
196
|
}));
|
|
189
197
|
|
|
190
|
-
|
|
198
|
+
let isSpacePressed = $state(false);
|
|
199
|
+
let isPanning = $state(false);
|
|
200
|
+
let panStartPoint: { clientX: number; clientY: number } | null = null;
|
|
201
|
+
let panStartOffset: { x: number; y: number } = { x: 0, y: 0 };
|
|
202
|
+
|
|
203
|
+
function handleWheel(e: WheelEvent) {
|
|
204
|
+
if (!viewportGestures || !canvasEl) return;
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
|
|
207
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
208
|
+
const sx = rect.width > 0 ? view.width / rect.width : 1;
|
|
209
|
+
const sy = rect.height > 0 ? view.height / rect.height : 1;
|
|
210
|
+
const canvasPt = {
|
|
211
|
+
x: (e.clientX - rect.left) * sx,
|
|
212
|
+
y: (e.clientY - rect.top) * sy,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const s = editor.controller.getState();
|
|
216
|
+
const currentZoom = s.zoom || 1;
|
|
217
|
+
const factor = e.deltaY < 0 ? 1.1 : 0.9;
|
|
218
|
+
const nextZoom = Math.min(10, Math.max(0.1, currentZoom * factor));
|
|
219
|
+
|
|
220
|
+
// Zoom centered at cursor:
|
|
221
|
+
// in canvas coordinates: img = (canvasPt - pan - center) / zoom
|
|
222
|
+
// We want canvasPt to remain invariant after zoom:
|
|
223
|
+
// (canvasPt - center - newPan) / newZoom = (canvasPt - center - oldPan) / oldZoom
|
|
224
|
+
// newPan = canvasPt - center - (canvasPt - center - oldPan) * (newZoom / oldZoom)
|
|
225
|
+
const centerX = view.width / 2;
|
|
226
|
+
const centerY = view.height / 2;
|
|
227
|
+
const curPan = s.pan || { x: 0, y: 0 };
|
|
228
|
+
const ratio = nextZoom / currentZoom;
|
|
229
|
+
|
|
230
|
+
const newPanX = canvasPt.x - centerX - (canvasPt.x - centerX - curPan.x) * ratio;
|
|
231
|
+
const newPanY = canvasPt.y - centerY - (canvasPt.y - centerY - curPan.y) * ratio;
|
|
232
|
+
|
|
233
|
+
editor.controller.setZoom(nextZoom);
|
|
234
|
+
editor.controller.setPan({ x: Math.round(newPanX), y: Math.round(newPanY) });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
$effect(() => {
|
|
238
|
+
if (typeof window === 'undefined') return;
|
|
239
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
240
|
+
if (e.code === 'Space' && !e.repeat && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
|
|
241
|
+
isSpacePressed = true;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
245
|
+
if (e.code === 'Space') {
|
|
246
|
+
isSpacePressed = false;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
window.addEventListener('keydown', onKeyDown);
|
|
250
|
+
window.addEventListener('keyup', onKeyUp);
|
|
251
|
+
return () => {
|
|
252
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
253
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
254
|
+
};
|
|
255
|
+
});
|
|
191
256
|
const selectionGesture = $derived(new SelectionGesture(editor.controller, () => selectionOptions));
|
|
192
257
|
let selectionDraft = $state.raw<ImagePoint[] | null>(null);
|
|
193
258
|
// Paint tools (Move, Brush, Eraser): the pointer gesture and the brush cursor.
|
|
@@ -476,7 +541,36 @@
|
|
|
476
541
|
}
|
|
477
542
|
|
|
478
543
|
function handleMouseDown(e: MouseEvent) {
|
|
479
|
-
if (!canvasEl
|
|
544
|
+
if (!canvasEl) return;
|
|
545
|
+
|
|
546
|
+
if (viewportGestures && (e.button === 1 || (e.button === 0 && isSpacePressed))) {
|
|
547
|
+
e.preventDefault();
|
|
548
|
+
isPanning = true;
|
|
549
|
+
panStartPoint = { clientX: e.clientX, clientY: e.clientY };
|
|
550
|
+
const curPan = editor.controller.getState().pan || { x: 0, y: 0 };
|
|
551
|
+
panStartOffset = { ...curPan };
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (mode === 'transform') {
|
|
556
|
+
const canvasPt = getCanvasEventPoint(e);
|
|
557
|
+
const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
|
|
558
|
+
const hitLayerId = editor.controller.layerAt(imgPt);
|
|
559
|
+
if (hitLayerId) {
|
|
560
|
+
editor.controller.setActiveLayer(hitLayerId);
|
|
561
|
+
if (!transformTool.active) {
|
|
562
|
+
transformTool.begin();
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
editor.controller.setActiveLayer(null);
|
|
566
|
+
if (transformTool.active) {
|
|
567
|
+
transformTool.cancel();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (transformTool.active) return;
|
|
480
574
|
const canvasPt = getCanvasEventPoint(e);
|
|
481
575
|
|
|
482
576
|
if (mode === 'select') {
|
|
@@ -567,7 +661,23 @@
|
|
|
567
661
|
}
|
|
568
662
|
|
|
569
663
|
function handleMouseMove(e: MouseEvent) {
|
|
570
|
-
if (!canvasEl
|
|
664
|
+
if (!canvasEl) return;
|
|
665
|
+
|
|
666
|
+
if (isPanning && panStartPoint) {
|
|
667
|
+
const dx = e.clientX - panStartPoint.clientX;
|
|
668
|
+
const dy = e.clientY - panStartPoint.clientY;
|
|
669
|
+
editor.controller.setPan({
|
|
670
|
+
x: Math.round(panStartOffset.x + dx),
|
|
671
|
+
y: Math.round(panStartOffset.y + dy),
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (viewportGestures && isSpacePressed) {
|
|
677
|
+
canvasEl.style.cursor = 'grab';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (transformTool.active) return;
|
|
571
681
|
const canvasPt = getCanvasEventPoint(e);
|
|
572
682
|
|
|
573
683
|
if (mode === 'select') {
|
|
@@ -713,6 +823,11 @@
|
|
|
713
823
|
}
|
|
714
824
|
|
|
715
825
|
function handleMouseUp(e: MouseEvent) {
|
|
826
|
+
if (isPanning) {
|
|
827
|
+
isPanning = false;
|
|
828
|
+
panStartPoint = null;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
716
831
|
if (!canvasEl || transformTool.active) return;
|
|
717
832
|
const canvasPt = getCanvasEventPoint(e);
|
|
718
833
|
|
|
@@ -822,7 +937,12 @@
|
|
|
822
937
|
React's `<ImageEditorCanvas>`.
|
|
823
938
|
-->
|
|
824
939
|
<div bind:this={rootEl} class="relative {className}" {style} data-testid="image-editor-canvas-root">
|
|
825
|
-
|
|
940
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
941
|
+
<div
|
|
942
|
+
bind:this={containerEl}
|
|
943
|
+
onwheel={handleWheel}
|
|
944
|
+
class="absolute inset-0 overflow-hidden select-none"
|
|
945
|
+
>
|
|
826
946
|
<canvas
|
|
827
947
|
bind:this={canvasEl}
|
|
828
948
|
onmousedown={handleMouseDown}
|
|
@@ -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, {
|
|
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.
|
|
3
|
+
"version": "0.116.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.
|
|
163
|
+
"@pixerate/editor": "0.158.0"
|
|
164
164
|
},
|
|
165
165
|
"peerDependencies": {
|
|
166
166
|
"@dagrejs/dagre": "^1.1.4",
|