@pixerate/editor-svelte 0.107.0 → 0.109.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/README.md +2 -2
- package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +8 -2
- package/dist/canvas/createCanvasPlaybackSurface.svelte.js +13 -8
- package/dist/image-editor/AdjustmentLayersPanel.svelte +27 -26
- package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +2 -0
- package/dist/image-editor/BrushOptionsBar.svelte +35 -34
- package/dist/image-editor/ColorProfilePanel.svelte +12 -12
- package/dist/image-editor/ControlSlider.svelte +6 -4
- package/dist/image-editor/ControlSlider.svelte.d.ts +2 -0
- package/dist/image-editor/CurvesEditor.svelte +6 -4
- package/dist/image-editor/CurvesEditor.svelte.d.ts +2 -0
- package/dist/image-editor/FreeTransformOptionsBar.svelte +22 -21
- package/dist/image-editor/Histogram.svelte +1 -1
- package/dist/image-editor/ImageEditor.svelte +171 -883
- package/dist/image-editor/ImageEditorCanvas.svelte +825 -0
- package/dist/image-editor/ImageEditorCanvas.svelte.d.ts +64 -0
- package/dist/image-editor/ImageEffectControls.svelte +15 -11
- package/dist/image-editor/ImageEffectControls.svelte.d.ts +2 -0
- package/dist/image-editor/ImageProviderEffectsPanel.svelte +15 -15
- package/dist/image-editor/LayerEffectsPanel.svelte +22 -23
- package/dist/image-editor/LayersPanel.svelte +39 -39
- package/dist/image-editor/RefineEdgePanel.svelte +14 -13
- package/dist/image-editor/SelectionOptionsBar.svelte +12 -11
- package/dist/image-editor/ShapeOptionsBar.svelte +14 -13
- package/dist/image-editor/TypeOptionsBar.svelte +12 -11
- package/dist/image-editor/canvasTypes.d.ts +4 -0
- package/dist/image-editor/canvasTypes.js +1 -0
- package/dist/image-editor/index.d.ts +7 -0
- package/dist/image-editor/index.js +2 -0
- package/dist/image-editor/styles.d.ts +51 -0
- package/dist/image-editor/styles.js +51 -0
- package/dist/playback/PresenceLayer.svelte +4 -2
- package/dist/playback/PresenceLayer.svelte.d.ts +2 -0
- package/dist/playback/playback.svelte.d.ts +7 -4
- package/dist/playback/playback.svelte.js +17 -9
- package/package.json +2 -2
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageDividerClass, imageEditorFrameClass, imageEditorHeaderClass, imageErrorBannerClass, imageHeaderButtonClass, imagePrimaryButtonClass, imageRangeClass, imageSectionClass, imageSectionTitleClass, imageSelectClass, imageSidebarClass, imageSmallButtonClass, imageSpinnerClass, imageTabClass, imageTabListClass } from './styles.js';
|
|
2
3
|
import type {
|
|
3
|
-
Annotation,
|
|
4
|
-
CropBox,
|
|
5
|
-
CropHandle,
|
|
6
4
|
ExportOptions,
|
|
7
5
|
ImageEditorTool,
|
|
8
|
-
ImagePoint,
|
|
9
6
|
InpaintHook,
|
|
10
7
|
RemoveBackgroundHook,
|
|
11
8
|
ImageEditorAIHooks,
|
|
12
9
|
ImageEditorFit,
|
|
13
10
|
} from '@pixerate/editor/image-editor';
|
|
14
|
-
import type { AgentPlayback,
|
|
15
|
-
import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
|
|
16
|
-
import PresenceLayer from '../playback/PresenceLayer.svelte';
|
|
11
|
+
import type { AgentPlayback, PlaybackRef } from '@pixerate/editor/tools';
|
|
17
12
|
import {
|
|
18
|
-
canvasToImagePoint,
|
|
19
|
-
imageToCanvasPoint,
|
|
20
|
-
createImagePlaybackLocator,
|
|
21
|
-
hitTestCrop,
|
|
22
|
-
getCropCursor,
|
|
23
|
-
calculateCropDrag,
|
|
24
|
-
hitTestAnnotation,
|
|
25
13
|
normalizeProcessVersion,
|
|
26
14
|
LATEST_PROCESS_VERSION,
|
|
27
15
|
IMAGE_ADJUSTMENT_CONTROLS,
|
|
28
16
|
readAdjustmentControls,
|
|
29
17
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
30
|
-
SelectionGesture,
|
|
31
|
-
PaintGesture,
|
|
32
|
-
handlePaintShortcut,
|
|
33
|
-
pointerPressure,
|
|
34
18
|
FreeTransformTool,
|
|
35
|
-
getViewportProjection,
|
|
36
|
-
ShapeDrawGesture,
|
|
37
|
-
TypeToolGesture,
|
|
38
19
|
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
39
20
|
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
40
21
|
type SelectionToolOptions,
|
|
@@ -42,6 +23,8 @@
|
|
|
42
23
|
type TextEditSession,
|
|
43
24
|
type TypeToolOptions,
|
|
44
25
|
} from '@pixerate/editor/image-editor';
|
|
26
|
+
import ImageEditorCanvas from './ImageEditorCanvas.svelte';
|
|
27
|
+
import type { ImageEditorCanvasErrorSource, ImageEditorCanvasMode } from './canvasTypes.js';
|
|
45
28
|
import ControlSlider from './ControlSlider.svelte';
|
|
46
29
|
import Histogram from './Histogram.svelte';
|
|
47
30
|
import AdjustmentLayersPanel from './AdjustmentLayersPanel.svelte';
|
|
@@ -115,204 +98,59 @@
|
|
|
115
98
|
normalizeProcessVersion(editor.state.adjustments.process) < LATEST_PROCESS_VERSION
|
|
116
99
|
);
|
|
117
100
|
|
|
118
|
-
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
119
|
-
let containerEl = $state<HTMLDivElement | null>(null);
|
|
120
101
|
let activeTab = $state<'crop' | 'adjust' | 'layers' | 'paint' | 'select' | 'annotate' | 'depth' | 'ai'>('crop');
|
|
121
102
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
103
|
+
/** The tabs with canvas gestures drive the canvas mode; the others only view. */
|
|
104
|
+
const canvasMode = $derived<ImageEditorCanvasMode>(
|
|
105
|
+
activeTab === 'crop' || activeTab === 'select' || activeTab === 'paint' || activeTab === 'annotate' ? activeTab : 'none'
|
|
106
|
+
);
|
|
122
107
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const box = rootEl.getBoundingClientRect();
|
|
131
|
-
const c = imageToCanvasPoint(p, editor.controller.getState(), canvasEl.width, canvasEl.height, activeTab === 'crop');
|
|
132
|
-
return { x: r.left - box.left + (c.x * r.width) / canvasEl.width, y: r.top - box.top + (c.y * r.height) / canvasEl.height };
|
|
133
|
-
};
|
|
134
|
-
/** Viewport (client) pixels → document pixels. */
|
|
135
|
-
const clientToDoc = (p: PlaybackPoint): PlaybackPoint => {
|
|
136
|
-
if (!canvasEl || !canvasEl.width) return p;
|
|
137
|
-
const r = canvasEl.getBoundingClientRect();
|
|
138
|
-
const sx = r.width > 0 ? canvasEl.width / r.width : 1;
|
|
139
|
-
const sy = r.height > 0 ? canvasEl.height / r.height : 1;
|
|
140
|
-
return canvasToImagePoint({ x: (p.x - r.left) * sx, y: (p.y - r.top) * sy }, editor.controller.getState(), canvasEl.width, canvasEl.height, activeTab === 'crop');
|
|
141
|
-
};
|
|
142
|
-
/** An element of the editor (a tab, slider or layer row) as a rectangle in document pixels. */
|
|
143
|
-
const elementRect = (selector: string): PlaybackRect | null => {
|
|
144
|
-
const r = rootEl?.querySelector(selector)?.getBoundingClientRect();
|
|
145
|
-
if (!r || r.width <= 0) return null;
|
|
146
|
-
const a = clientToDoc({ x: r.left, y: r.top });
|
|
147
|
-
const b = clientToDoc({ x: r.right, y: r.bottom });
|
|
148
|
-
return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
|
|
149
|
-
};
|
|
150
|
-
const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
|
|
151
|
-
defaultKind: 'layer',
|
|
152
|
-
locate: (ref: PlaybackRef) => {
|
|
153
|
-
if (ref.kind === 'panel') return elementRect(`[data-image-tab="${CSS.escape(ref.id)}"]`);
|
|
154
|
-
if (ref.kind === 'control') return elementRect(`[data-control-id="${CSS.escape(ref.id)}"]`);
|
|
155
|
-
return createImagePlaybackLocator(editor.controller)(ref) ?? (ref.kind === 'layer' || ref.kind === 'item' ? elementRect(`[data-layer-id="${CSS.escape(ref.id)}"]`) : null);
|
|
156
|
-
},
|
|
157
|
-
open: async (ref: PlaybackRef) => {
|
|
158
|
-
if (ref.kind !== 'panel' || activeTab === ref.id) return;
|
|
159
|
-
rootEl?.querySelector<HTMLElement>(`[data-image-tab="${CSS.escape(ref.id)}"]`)?.click();
|
|
160
|
-
// Let the panel render before the cursor looks for its controls.
|
|
161
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
162
|
-
},
|
|
163
|
-
bounds: () => {
|
|
164
|
-
const r = canvasEl?.getBoundingClientRect();
|
|
165
|
-
if (!r || r.width <= 0) return null;
|
|
166
|
-
const a = clientToDoc({ x: r.left, y: r.top });
|
|
167
|
-
const b = clientToDoc({ x: r.right, y: r.bottom });
|
|
168
|
-
return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
|
|
169
|
-
},
|
|
170
|
-
visible: () => {
|
|
171
|
-
const r = rootEl?.getBoundingClientRect();
|
|
172
|
-
return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
|
|
173
|
-
},
|
|
174
|
-
toViewport: (p: PlaybackPoint) => {
|
|
175
|
-
const box = rootEl?.getBoundingClientRect();
|
|
176
|
-
const q = docToRoot(p);
|
|
177
|
-
return { x: (box?.left ?? 0) + q.x, y: (box?.top ?? 0) + q.y };
|
|
178
|
-
},
|
|
179
|
-
fromViewport: clientToDoc,
|
|
180
|
-
reveal: () => onplaybackreveal?.()
|
|
181
|
-
}));
|
|
108
|
+
/** Agent playback opens a panel by clicking its tab, unless it is already open. */
|
|
109
|
+
async function openPanel(ref: PlaybackRef) {
|
|
110
|
+
if (ref.kind !== 'panel' || activeTab === ref.id) return;
|
|
111
|
+
rootEl?.querySelector<HTMLElement>(`[data-image-tab="${CSS.escape(ref.id)}"]`)?.click();
|
|
112
|
+
// Let the panel render before the cursor looks for its controls.
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
114
|
+
}
|
|
182
115
|
|
|
183
|
-
//
|
|
116
|
+
// Tool options for the canvas, set by the options bars.
|
|
184
117
|
let selectionOptions = $state<SelectionToolOptions>({ ...DEFAULT_SELECTION_TOOL_OPTIONS });
|
|
185
|
-
const selectionGesture = $derived(new SelectionGesture(editor.controller, () => selectionOptions));
|
|
186
|
-
// Paint tools (Move, Brush, Eraser): the pointer gesture and the brush cursor.
|
|
187
|
-
const paintGesture = $derived(new PaintGesture(editor.controller));
|
|
188
|
-
let brushCursor: ImagePoint | null = null;
|
|
189
|
-
|
|
190
|
-
// Shape and type tools: their options and gestures; type is edited on the canvas through a hidden textarea.
|
|
191
118
|
let shapeOptions = $state<ShapeToolOptions>({ ...DEFAULT_SHAPE_TOOL_OPTIONS });
|
|
192
119
|
let typeOptions = $state<TypeToolOptions>({ ...DEFAULT_TYPE_TOOL_OPTIONS });
|
|
193
|
-
const shapeGesture = $derived(new ShapeDrawGesture(editor.controller, () => shapeOptions));
|
|
194
|
-
const typeTool = $derived(new TypeToolGesture(editor.controller, () => typeOptions));
|
|
195
120
|
let typeSession = $state<TextEditSession | null>(null);
|
|
196
|
-
let typeInputEl = $state<HTMLTextAreaElement | null>(null);
|
|
197
121
|
let shapeTypeError = $state<string | null>(null);
|
|
198
|
-
$
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
untrack(() => {
|
|
207
|
-
if (!keep && typeTool.session) finishTyping();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
122
|
+
let paintError = $state<string | null>(null);
|
|
123
|
+
let selectionError = $state<string | null>(null);
|
|
124
|
+
let activeCropRatio = $state<number | null>(null);
|
|
125
|
+
function handleCanvasError(source: ImageEditorCanvasErrorSource, message: string | null) {
|
|
126
|
+
if (source === 'paint') paintError = message;
|
|
127
|
+
else if (source === 'select') selectionError = message;
|
|
128
|
+
else shapeTypeError = message;
|
|
129
|
+
}
|
|
210
130
|
function finishTyping() {
|
|
211
|
-
|
|
131
|
+
// The canvas finishes the edit when its bound session is cleared.
|
|
212
132
|
typeSession = null;
|
|
213
|
-
void renderCanvas();
|
|
214
|
-
}
|
|
215
|
-
function handleTypeKeyDown(e: KeyboardEvent) {
|
|
216
|
-
const session = typeTool.session;
|
|
217
|
-
if (!session || e.isComposing) return;
|
|
218
|
-
if (e.key === 'Escape') {
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
finishTyping();
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if ((e.metaKey || e.ctrlKey) && ['z', 'y', 'c', 'x', 'v'].includes(e.key.toLowerCase())) return;
|
|
224
|
-
if (session.handleKey(e)) {
|
|
225
|
-
e.preventDefault();
|
|
226
|
-
shapeTypeError = session.error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function handleTypeInput(e: Event) {
|
|
230
|
-
// Composed (IME) and pasted text arrives here; plain keys were handled on keydown.
|
|
231
|
-
const el = e.currentTarget as HTMLTextAreaElement;
|
|
232
|
-
const session = typeTool.session;
|
|
233
|
-
if (session && el.value && !(e as InputEvent).isComposing) {
|
|
234
|
-
session.insert(el.value);
|
|
235
|
-
el.value = '';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function handleTypeComposition(e: CompositionEvent) {
|
|
239
|
-
const session = typeTool.session;
|
|
240
|
-
if (session && e.data) session.insert(e.data);
|
|
241
|
-
(e.currentTarget as HTMLTextAreaElement).value = '';
|
|
242
133
|
}
|
|
243
134
|
|
|
244
|
-
// Free Transform (Ctrl/Cmd+T):
|
|
135
|
+
// Free Transform (Ctrl/Cmd+T): shared with the canvas, which drives and draws it.
|
|
245
136
|
const transformTool = $derived(
|
|
246
137
|
new FreeTransformTool(editor.controller, {
|
|
247
138
|
schedule: (run) => (typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(run) : run()),
|
|
248
139
|
})
|
|
249
140
|
);
|
|
250
141
|
let transformTick = $state(0);
|
|
251
|
-
let transformCursor = $state('default');
|
|
252
142
|
const transformActive = $derived((void transformTick, transformTool.active));
|
|
253
143
|
const transformError = $derived((void transformTick, transformTool.error));
|
|
254
144
|
$effect(() => {
|
|
255
145
|
const tool = transformTool;
|
|
256
146
|
const off = tool.subscribe(() => {
|
|
257
147
|
transformTick++;
|
|
258
|
-
void renderCanvas();
|
|
259
148
|
});
|
|
260
149
|
return () => {
|
|
261
150
|
off();
|
|
262
151
|
tool.cancel();
|
|
263
152
|
};
|
|
264
153
|
});
|
|
265
|
-
$effect(() => {
|
|
266
|
-
if (typeof window === 'undefined') return;
|
|
267
|
-
const tool = transformTool;
|
|
268
|
-
const onKey = (e: KeyboardEvent) => {
|
|
269
|
-
if (tool.key(e)) e.preventDefault();
|
|
270
|
-
};
|
|
271
|
-
window.addEventListener('keydown', onKey);
|
|
272
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
/** The document point of a pointer event and the hit tolerance (6 screen px). */
|
|
276
|
-
function transformPoint(e: PointerEvent) {
|
|
277
|
-
const rect = canvasEl!.getBoundingClientRect();
|
|
278
|
-
const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
|
|
279
|
-
const sy = rect.height > 0 ? canvasEl!.height / rect.height : 1;
|
|
280
|
-
const s = editor.controller.getState();
|
|
281
|
-
const cropping = activeTab === 'crop';
|
|
282
|
-
const projection = getViewportProjection(s, canvasEl!.width, canvasEl!.height, cropping);
|
|
283
|
-
const pt = canvasToImagePoint({ x: (e.clientX - rect.left) * sx, y: (e.clientY - rect.top) * sy }, s, canvasEl!.width, canvasEl!.height, cropping);
|
|
284
|
-
transformTool.setView(projection.scale);
|
|
285
|
-
return { pt, tolerance: 6 / (projection.scale || 1) };
|
|
286
|
-
}
|
|
287
|
-
function handleTransformPointerDown(e: PointerEvent): boolean {
|
|
288
|
-
if (!transformTool.active || !canvasEl) return false;
|
|
289
|
-
if (e.button !== 0) return true;
|
|
290
|
-
e.preventDefault();
|
|
291
|
-
try {
|
|
292
|
-
canvasEl.setPointerCapture?.(e.pointerId);
|
|
293
|
-
} catch {
|
|
294
|
-
// Synthetic events have no active pointer to capture.
|
|
295
|
-
}
|
|
296
|
-
const { pt, tolerance } = transformPoint(e);
|
|
297
|
-
transformTool.down(pt, tolerance);
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
|
-
function handleTransformPointerMove(e: PointerEvent): boolean {
|
|
301
|
-
if (!transformTool.active || !canvasEl) return false;
|
|
302
|
-
const { pt, tolerance } = transformPoint(e);
|
|
303
|
-
if (e.buttons & 1) transformTool.move(pt, e);
|
|
304
|
-
else transformCursor = transformTool.cursor(transformTool.hit(pt, tolerance));
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
function handleTransformPointerUp(): boolean {
|
|
308
|
-
if (!transformTool.active) return false;
|
|
309
|
-
transformTool.up();
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
let paintError = $state<string | null>(null);
|
|
313
|
-
let selectionDraft = $state.raw<ImagePoint[] | null>(null);
|
|
314
|
-
let selectionError = $state<string | null>(null);
|
|
315
|
-
let activeCropRatio = $state<number | null>(null);
|
|
316
154
|
|
|
317
155
|
// AI state
|
|
318
156
|
let inpaintPrompt = $state('');
|
|
@@ -398,24 +236,6 @@
|
|
|
398
236
|
let fontSize = $state(24);
|
|
399
237
|
let textInput = $state('');
|
|
400
238
|
|
|
401
|
-
// Live draft and drag references
|
|
402
|
-
let draftAnnotation = $state<Annotation | null>(null);
|
|
403
|
-
let draftCrop = $state<CropBox | null>(null);
|
|
404
|
-
let cropDragInfo: {
|
|
405
|
-
handle: CropHandle | 'new';
|
|
406
|
-
startCanvasPt: ImagePoint;
|
|
407
|
-
startImgPt: ImagePoint;
|
|
408
|
-
startCrop: CropBox;
|
|
409
|
-
} | null = null;
|
|
410
|
-
let annotationDragInfo: {
|
|
411
|
-
annId: string;
|
|
412
|
-
startImgPt: ImagePoint;
|
|
413
|
-
initialAnn: Annotation;
|
|
414
|
-
} | null = null;
|
|
415
|
-
let isDrawing = false;
|
|
416
|
-
let currentPath: ImagePoint[] = [];
|
|
417
|
-
let dragStartPoint: ImagePoint | null = null;
|
|
418
|
-
|
|
419
239
|
// Load the image when `src` changes. Only `src` is tracked: AI edits replace
|
|
420
240
|
// the editor's source with a data URL and must not be reverted to `src`.
|
|
421
241
|
let loadedSrc: string | undefined;
|
|
@@ -428,512 +248,12 @@
|
|
|
428
248
|
});
|
|
429
249
|
});
|
|
430
250
|
|
|
431
|
-
async function renderCanvas(overrideAnnotation?: Annotation | null, overrideCrop?: CropBox | null) {
|
|
432
|
-
if (!canvasEl || !containerEl) return;
|
|
433
|
-
const vpWidth = containerEl.clientWidth || 800;
|
|
434
|
-
const vpHeight = containerEl.clientHeight || 500;
|
|
435
|
-
|
|
436
|
-
await editor.controller.render(canvasEl, {
|
|
437
|
-
viewportWidth: vpWidth,
|
|
438
|
-
viewportHeight: vpHeight,
|
|
439
|
-
fit,
|
|
440
|
-
showCropOverlay: activeTab === 'crop',
|
|
441
|
-
showSelectionOverlay: true,
|
|
442
|
-
interactive: true,
|
|
443
|
-
draftAnnotation: overrideAnnotation !== undefined ? overrideAnnotation : draftAnnotation,
|
|
444
|
-
draftCrop: overrideCrop !== undefined ? overrideCrop : draftCrop,
|
|
445
|
-
draftSelectionPath: selectionDraft,
|
|
446
|
-
brushCursor:
|
|
447
|
-
activeTab === 'paint' && brushCursor && editor.controller.getState().activeTool !== 'move' && !transformTool.active
|
|
448
|
-
? { ...brushCursor, diameter: editor.controller.getState().paint.brush.size }
|
|
449
|
-
: null,
|
|
450
|
-
freeTransform: transformTool.overlay(),
|
|
451
|
-
textEdit: typeTool.session?.alive ? typeTool.session.overlay() : null,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Paint tools: pointer events (coalesced samples, pointer capture) into strokes and moves.
|
|
456
|
-
function paintSamples(e: PointerEvent) {
|
|
457
|
-
const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];
|
|
458
|
-
const list = events.length ? events : [e];
|
|
459
|
-
const rect = canvasEl!.getBoundingClientRect();
|
|
460
|
-
const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
|
|
461
|
-
const sy = rect.height > 0 ? canvasEl!.height / rect.height : 1;
|
|
462
|
-
const s = editor.controller.getState();
|
|
463
|
-
return list.map((ev) => ({
|
|
464
|
-
...canvasToImagePoint({ x: (ev.clientX - rect.left) * sx, y: (ev.clientY - rect.top) * sy }, s, canvasEl!.width, canvasEl!.height, false),
|
|
465
|
-
pressure: pointerPressure(ev),
|
|
466
|
-
}));
|
|
467
|
-
}
|
|
468
|
-
function handlePaintPointerDown(e: PointerEvent) {
|
|
469
|
-
if (handleTransformPointerDown(e)) return;
|
|
470
|
-
if (activeTab !== 'paint' || e.button !== 0 || !canvasEl) return;
|
|
471
|
-
e.preventDefault();
|
|
472
|
-
try {
|
|
473
|
-
canvasEl.setPointerCapture?.(e.pointerId);
|
|
474
|
-
} catch {
|
|
475
|
-
// Synthetic events have no active pointer to capture.
|
|
476
|
-
}
|
|
477
|
-
paintError = paintGesture.down({ ...paintSamples(e)[0], altKey: e.altKey });
|
|
478
|
-
}
|
|
479
|
-
function handlePaintPointerMove(e: PointerEvent) {
|
|
480
|
-
if (handleTransformPointerMove(e)) return;
|
|
481
|
-
if (activeTab !== 'paint' || !canvasEl) return;
|
|
482
|
-
const samples = paintSamples(e);
|
|
483
|
-
brushCursor = samples[samples.length - 1] ?? null;
|
|
484
|
-
if (paintGesture.active) paintGesture.moveTo(samples);
|
|
485
|
-
else void renderCanvas();
|
|
486
|
-
}
|
|
487
|
-
function handlePaintPointerUp() {
|
|
488
|
-
if (handleTransformPointerUp()) return;
|
|
489
|
-
if (activeTab !== 'paint' || !paintGesture.active) return;
|
|
490
|
-
paintGesture.up();
|
|
491
|
-
if (paintGesture.error) paintError = paintGesture.error;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
$effect(() => {
|
|
495
|
-
if (activeTab !== 'paint' || typeof window === 'undefined') return;
|
|
496
|
-
const c = editor.controller;
|
|
497
|
-
const onKey = (e: KeyboardEvent) => {
|
|
498
|
-
if (handlePaintShortcut(c, e)) e.preventDefault();
|
|
499
|
-
};
|
|
500
|
-
window.addEventListener('keydown', onKey);
|
|
501
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
$effect(() => {
|
|
505
|
-
// Re-render when editor state or activeTab changes
|
|
506
|
-
void editor.state;
|
|
507
|
-
void activeTab;
|
|
508
|
-
|
|
509
|
-
const raf = typeof requestAnimationFrame !== 'undefined'
|
|
510
|
-
? requestAnimationFrame
|
|
511
|
-
: (cb: () => void) => setTimeout(cb, 0) as unknown as number;
|
|
512
|
-
const caf = typeof cancelAnimationFrame !== 'undefined'
|
|
513
|
-
? cancelAnimationFrame
|
|
514
|
-
: (id: number) => clearTimeout(id);
|
|
515
|
-
|
|
516
|
-
const animId = raf(() => {
|
|
517
|
-
renderCanvas();
|
|
518
|
-
});
|
|
519
|
-
return () => caf(animId);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
251
|
$effect(() => {
|
|
523
252
|
if (activeTab === 'crop' && editor.state.activeTool !== 'crop') {
|
|
524
253
|
editor.setTool('crop');
|
|
525
254
|
}
|
|
526
255
|
});
|
|
527
256
|
|
|
528
|
-
$effect(() => {
|
|
529
|
-
if (!containerEl) return;
|
|
530
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
531
|
-
const observer = new ResizeObserver(() => {
|
|
532
|
-
renderCanvas();
|
|
533
|
-
});
|
|
534
|
-
observer.observe(containerEl);
|
|
535
|
-
return () => observer.disconnect();
|
|
536
|
-
} else {
|
|
537
|
-
const handleResize = () => renderCanvas();
|
|
538
|
-
window.addEventListener('resize', handleResize);
|
|
539
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
function getCanvasEventPoint(e: MouseEvent): ImagePoint {
|
|
544
|
-
if (!canvasEl) return { x: 0, y: 0 };
|
|
545
|
-
const rect = canvasEl.getBoundingClientRect();
|
|
546
|
-
const scaleX = rect.width > 0 ? canvasEl.width / rect.width : 1;
|
|
547
|
-
const scaleY = rect.height > 0 ? canvasEl.height / rect.height : 1;
|
|
548
|
-
return {
|
|
549
|
-
x: (e.clientX - rect.left) * scaleX,
|
|
550
|
-
y: (e.clientY - rect.top) * scaleY,
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function handleMouseDown(e: MouseEvent) {
|
|
555
|
-
if (!canvasEl || transformTool.active) return;
|
|
556
|
-
const canvasPt = getCanvasEventPoint(e);
|
|
557
|
-
|
|
558
|
-
if (activeTab === 'select') {
|
|
559
|
-
const gesture = selectionGesture;
|
|
560
|
-
const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
561
|
-
void gesture.begin(imgPt, e).then(() => (selectionError = gesture.error));
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (activeTab === 'crop') {
|
|
566
|
-
const activeCrop = draftCrop || editor.state.crop || {
|
|
567
|
-
x: 0,
|
|
568
|
-
y: 0,
|
|
569
|
-
width: editor.state.imageDimensions.width,
|
|
570
|
-
height: editor.state.imageDimensions.height,
|
|
571
|
-
};
|
|
572
|
-
const handle = hitTestCrop(canvasPt, editor.state, canvasEl.width, canvasEl.height, 14, activeCrop);
|
|
573
|
-
const startImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, true);
|
|
574
|
-
|
|
575
|
-
if (handle) {
|
|
576
|
-
cropDragInfo = {
|
|
577
|
-
handle,
|
|
578
|
-
startCanvasPt: canvasPt,
|
|
579
|
-
startImgPt,
|
|
580
|
-
startCrop: activeCrop,
|
|
581
|
-
};
|
|
582
|
-
} else {
|
|
583
|
-
// Clicked outside crop box: start new crop selection
|
|
584
|
-
cropDragInfo = {
|
|
585
|
-
handle: 'new',
|
|
586
|
-
startCanvasPt: canvasPt,
|
|
587
|
-
startImgPt,
|
|
588
|
-
startCrop: { x: startImgPt.x, y: startImgPt.y, width: 0, height: 0 },
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (activeTab === 'annotate' && (editor.state.activeTool === 'shape' || editor.state.activeTool === 'type')) {
|
|
595
|
-
const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
596
|
-
shapeTypeError = null;
|
|
597
|
-
if (editor.state.activeTool === 'shape') shapeGesture.down(imgPt);
|
|
598
|
-
else {
|
|
599
|
-
typeTool.down(imgPt, { shift: e.shiftKey });
|
|
600
|
-
typeSession = typeTool.session;
|
|
601
|
-
void renderCanvas();
|
|
602
|
-
}
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (activeTab === 'annotate') {
|
|
607
|
-
const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
608
|
-
dragStartPoint = imgPt;
|
|
609
|
-
|
|
610
|
-
if (editor.state.activeTool === 'select') {
|
|
611
|
-
for (let i = editor.state.annotations.length - 1; i >= 0; i--) {
|
|
612
|
-
const ann = editor.state.annotations[i];
|
|
613
|
-
if (hitTestAnnotation(imgPt, ann)) {
|
|
614
|
-
editor.selectAnnotation(ann.id);
|
|
615
|
-
annotationDragInfo = {
|
|
616
|
-
annId: ann.id,
|
|
617
|
-
startImgPt: imgPt,
|
|
618
|
-
initialAnn: { ...ann },
|
|
619
|
-
};
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
editor.selectAnnotation(null);
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (editor.state.activeTool === 'pen') {
|
|
628
|
-
isDrawing = true;
|
|
629
|
-
currentPath = [imgPt];
|
|
630
|
-
const draft: Annotation = {
|
|
631
|
-
id: 'draft',
|
|
632
|
-
type: 'pen',
|
|
633
|
-
x: 0,
|
|
634
|
-
y: 0,
|
|
635
|
-
points: [imgPt],
|
|
636
|
-
strokeColor: activeColor,
|
|
637
|
-
strokeWidth,
|
|
638
|
-
};
|
|
639
|
-
draftAnnotation = draft;
|
|
640
|
-
renderCanvas(draft);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function handleMouseMove(e: MouseEvent) {
|
|
646
|
-
if (!canvasEl || transformTool.active) return;
|
|
647
|
-
const canvasPt = getCanvasEventPoint(e);
|
|
648
|
-
|
|
649
|
-
if (activeTab === 'select') {
|
|
650
|
-
const gesture = selectionGesture;
|
|
651
|
-
if (gesture.active && gesture.move(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false))) {
|
|
652
|
-
selectionDraft = gesture.draftPath;
|
|
653
|
-
renderCanvas();
|
|
654
|
-
}
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (activeTab === 'crop') {
|
|
659
|
-
if (cropDragInfo) {
|
|
660
|
-
const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, true);
|
|
661
|
-
let newCrop: CropBox;
|
|
662
|
-
|
|
663
|
-
if (cropDragInfo.handle === 'new') {
|
|
664
|
-
const minX = Math.max(0, Math.min(cropDragInfo.startImgPt.x, currentImgPt.x));
|
|
665
|
-
const minY = Math.max(0, Math.min(cropDragInfo.startImgPt.y, currentImgPt.y));
|
|
666
|
-
const maxX = Math.min(editor.state.imageDimensions.width, Math.max(cropDragInfo.startImgPt.x, currentImgPt.x));
|
|
667
|
-
const maxY = Math.min(editor.state.imageDimensions.height, Math.max(cropDragInfo.startImgPt.y, currentImgPt.y));
|
|
668
|
-
newCrop = {
|
|
669
|
-
x: Math.round(minX),
|
|
670
|
-
y: Math.round(minY),
|
|
671
|
-
width: Math.round(Math.max(20, maxX - minX)),
|
|
672
|
-
height: Math.round(Math.max(20, maxY - minY)),
|
|
673
|
-
};
|
|
674
|
-
} else {
|
|
675
|
-
newCrop = calculateCropDrag(
|
|
676
|
-
cropDragInfo.startCrop,
|
|
677
|
-
cropDragInfo.handle,
|
|
678
|
-
cropDragInfo.startImgPt,
|
|
679
|
-
currentImgPt,
|
|
680
|
-
editor.state.imageDimensions.width,
|
|
681
|
-
editor.state.imageDimensions.height,
|
|
682
|
-
activeCropRatio,
|
|
683
|
-
editor.state.transform
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
draftCrop = newCrop;
|
|
688
|
-
renderCanvas(null, newCrop);
|
|
689
|
-
} else {
|
|
690
|
-
const activeCrop = draftCrop || editor.state.crop || {
|
|
691
|
-
x: 0,
|
|
692
|
-
y: 0,
|
|
693
|
-
width: editor.state.imageDimensions.width,
|
|
694
|
-
height: editor.state.imageDimensions.height,
|
|
695
|
-
};
|
|
696
|
-
const handle = hitTestCrop(canvasPt, editor.state, canvasEl.width, canvasEl.height, 14, activeCrop);
|
|
697
|
-
canvasEl.style.cursor = getCropCursor(handle, editor.state.transform);
|
|
698
|
-
}
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (activeTab === 'annotate' && editor.state.activeTool === 'shape') {
|
|
703
|
-
if (shapeGesture.active) {
|
|
704
|
-
shapeGesture.move(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false), { shift: e.shiftKey, alt: e.altKey });
|
|
705
|
-
if (shapeGesture.error) shapeTypeError = shapeGesture.error;
|
|
706
|
-
}
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (activeTab === 'annotate') {
|
|
711
|
-
if (annotationDragInfo) {
|
|
712
|
-
const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
713
|
-
const dx = currentImgPt.x - annotationDragInfo.startImgPt.x;
|
|
714
|
-
const dy = currentImgPt.y - annotationDragInfo.startImgPt.y;
|
|
715
|
-
editor.updateAnnotation(annotationDragInfo.annId, {
|
|
716
|
-
x: Math.round(annotationDragInfo.initialAnn.x + dx),
|
|
717
|
-
y: Math.round(annotationDragInfo.initialAnn.y + dy),
|
|
718
|
-
});
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (!dragStartPoint) {
|
|
723
|
-
if (editor.state.activeTool === 'select') {
|
|
724
|
-
const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
725
|
-
let hovered = false;
|
|
726
|
-
for (let i = editor.state.annotations.length - 1; i >= 0; i--) {
|
|
727
|
-
if (hitTestAnnotation(imgPt, editor.state.annotations[i])) {
|
|
728
|
-
hovered = true;
|
|
729
|
-
break;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
canvasEl.style.cursor = hovered ? 'move' : 'default';
|
|
733
|
-
} else {
|
|
734
|
-
canvasEl.style.cursor = 'crosshair';
|
|
735
|
-
}
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
740
|
-
|
|
741
|
-
if (editor.state.activeTool === 'pen' && isDrawing) {
|
|
742
|
-
currentPath.push(currentImgPt);
|
|
743
|
-
const draft: Annotation = {
|
|
744
|
-
id: 'draft',
|
|
745
|
-
type: 'pen',
|
|
746
|
-
x: 0,
|
|
747
|
-
y: 0,
|
|
748
|
-
points: [...currentPath],
|
|
749
|
-
strokeColor: activeColor,
|
|
750
|
-
strokeWidth,
|
|
751
|
-
};
|
|
752
|
-
draftAnnotation = draft;
|
|
753
|
-
renderCanvas(draft);
|
|
754
|
-
} else if (editor.state.activeTool === 'rect') {
|
|
755
|
-
const w = currentImgPt.x - dragStartPoint.x;
|
|
756
|
-
const h = currentImgPt.y - dragStartPoint.y;
|
|
757
|
-
const draft: Annotation = {
|
|
758
|
-
id: 'draft',
|
|
759
|
-
type: 'rect',
|
|
760
|
-
x: Math.min(dragStartPoint.x, currentImgPt.x),
|
|
761
|
-
y: Math.min(dragStartPoint.y, currentImgPt.y),
|
|
762
|
-
width: Math.abs(w),
|
|
763
|
-
height: Math.abs(h),
|
|
764
|
-
strokeColor: activeColor,
|
|
765
|
-
strokeWidth,
|
|
766
|
-
};
|
|
767
|
-
draftAnnotation = draft;
|
|
768
|
-
renderCanvas(draft);
|
|
769
|
-
} else if (editor.state.activeTool === 'circle') {
|
|
770
|
-
const rx = Math.abs(currentImgPt.x - dragStartPoint.x) / 2;
|
|
771
|
-
const ry = Math.abs(currentImgPt.y - dragStartPoint.y) / 2;
|
|
772
|
-
const draft: Annotation = {
|
|
773
|
-
id: 'draft',
|
|
774
|
-
type: 'circle',
|
|
775
|
-
x: Math.min(dragStartPoint.x, currentImgPt.x) + rx,
|
|
776
|
-
y: Math.min(dragStartPoint.y, currentImgPt.y) + ry,
|
|
777
|
-
radiusX: rx,
|
|
778
|
-
radiusY: ry,
|
|
779
|
-
strokeColor: activeColor,
|
|
780
|
-
strokeWidth,
|
|
781
|
-
};
|
|
782
|
-
draftAnnotation = draft;
|
|
783
|
-
renderCanvas(draft);
|
|
784
|
-
} else if (editor.state.activeTool === 'arrow') {
|
|
785
|
-
const draft: Annotation = {
|
|
786
|
-
id: 'draft',
|
|
787
|
-
type: 'arrow',
|
|
788
|
-
x: dragStartPoint.x,
|
|
789
|
-
y: dragStartPoint.y,
|
|
790
|
-
endX: currentImgPt.x,
|
|
791
|
-
endY: currentImgPt.y,
|
|
792
|
-
strokeColor: activeColor,
|
|
793
|
-
strokeWidth,
|
|
794
|
-
};
|
|
795
|
-
draftAnnotation = draft;
|
|
796
|
-
renderCanvas(draft);
|
|
797
|
-
} else if (editor.state.activeTool === 'line') {
|
|
798
|
-
const draft: Annotation = {
|
|
799
|
-
id: 'draft',
|
|
800
|
-
type: 'line',
|
|
801
|
-
x: dragStartPoint.x,
|
|
802
|
-
y: dragStartPoint.y,
|
|
803
|
-
endX: currentImgPt.x,
|
|
804
|
-
endY: currentImgPt.y,
|
|
805
|
-
strokeColor: activeColor,
|
|
806
|
-
strokeWidth,
|
|
807
|
-
};
|
|
808
|
-
draftAnnotation = draft;
|
|
809
|
-
renderCanvas(draft);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function handleMouseUp(e: MouseEvent) {
|
|
815
|
-
if (!canvasEl || transformTool.active) return;
|
|
816
|
-
const canvasPt = getCanvasEventPoint(e);
|
|
817
|
-
|
|
818
|
-
if (activeTab === 'select') {
|
|
819
|
-
const gesture = selectionGesture;
|
|
820
|
-
if (gesture.active) {
|
|
821
|
-
gesture.end(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false), e);
|
|
822
|
-
selectionDraft = null;
|
|
823
|
-
renderCanvas();
|
|
824
|
-
}
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (activeTab === 'crop') {
|
|
829
|
-
if (cropDragInfo) {
|
|
830
|
-
if (draftCrop && draftCrop.width >= 20 && draftCrop.height >= 20) {
|
|
831
|
-
editor.setCrop(draftCrop, true);
|
|
832
|
-
}
|
|
833
|
-
cropDragInfo = null;
|
|
834
|
-
draftCrop = null;
|
|
835
|
-
renderCanvas();
|
|
836
|
-
}
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
if (activeTab === 'annotate' && editor.state.activeTool === 'shape') {
|
|
841
|
-
shapeGesture.up();
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
if (activeTab === 'annotate' && editor.state.activeTool === 'type') {
|
|
845
|
-
typeTool.up(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false));
|
|
846
|
-
typeSession = typeTool.session;
|
|
847
|
-
if (typeTool.error) shapeTypeError = typeTool.error;
|
|
848
|
-
void renderCanvas();
|
|
849
|
-
// Keystrokes go to the hidden textarea while text is being edited.
|
|
850
|
-
if (typeTool.session) setTimeout(() => typeInputEl?.focus(), 0);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (activeTab === 'annotate') {
|
|
855
|
-
if (annotationDragInfo) {
|
|
856
|
-
annotationDragInfo = null;
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (dragStartPoint) {
|
|
861
|
-
const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
|
|
862
|
-
|
|
863
|
-
if (editor.state.activeTool === 'pen' && isDrawing) {
|
|
864
|
-
if (currentPath.length > 1) {
|
|
865
|
-
editor.addAnnotation({
|
|
866
|
-
type: 'pen',
|
|
867
|
-
x: 0,
|
|
868
|
-
y: 0,
|
|
869
|
-
points: [...currentPath],
|
|
870
|
-
strokeColor: activeColor,
|
|
871
|
-
strokeWidth,
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
isDrawing = false;
|
|
875
|
-
currentPath = [];
|
|
876
|
-
} else if (editor.state.activeTool === 'rect') {
|
|
877
|
-
const w = Math.abs(currentImgPt.x - dragStartPoint.x);
|
|
878
|
-
const h = Math.abs(currentImgPt.y - dragStartPoint.y);
|
|
879
|
-
if (w > 4 && h > 4) {
|
|
880
|
-
editor.addAnnotation({
|
|
881
|
-
type: 'rect',
|
|
882
|
-
x: Math.min(dragStartPoint.x, currentImgPt.x),
|
|
883
|
-
y: Math.min(dragStartPoint.y, currentImgPt.y),
|
|
884
|
-
width: w,
|
|
885
|
-
height: h,
|
|
886
|
-
strokeColor: activeColor,
|
|
887
|
-
strokeWidth,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
} else if (editor.state.activeTool === 'circle') {
|
|
891
|
-
const rx = Math.abs(currentImgPt.x - dragStartPoint.x) / 2;
|
|
892
|
-
const ry = Math.abs(currentImgPt.y - dragStartPoint.y) / 2;
|
|
893
|
-
if (rx > 4 && ry > 4) {
|
|
894
|
-
editor.addAnnotation({
|
|
895
|
-
type: 'circle',
|
|
896
|
-
x: Math.min(dragStartPoint.x, currentImgPt.x) + rx,
|
|
897
|
-
y: Math.min(dragStartPoint.y, currentImgPt.y) + ry,
|
|
898
|
-
radiusX: rx,
|
|
899
|
-
radiusY: ry,
|
|
900
|
-
strokeColor: activeColor,
|
|
901
|
-
strokeWidth,
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
} else if (editor.state.activeTool === 'arrow') {
|
|
905
|
-
if (Math.hypot(currentImgPt.x - dragStartPoint.x, currentImgPt.y - dragStartPoint.y) > 4) {
|
|
906
|
-
editor.addAnnotation({
|
|
907
|
-
type: 'arrow',
|
|
908
|
-
x: dragStartPoint.x,
|
|
909
|
-
y: dragStartPoint.y,
|
|
910
|
-
endX: currentImgPt.x,
|
|
911
|
-
endY: currentImgPt.y,
|
|
912
|
-
strokeColor: activeColor,
|
|
913
|
-
strokeWidth,
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
} else if (editor.state.activeTool === 'line') {
|
|
917
|
-
if (Math.hypot(currentImgPt.x - dragStartPoint.x, currentImgPt.y - dragStartPoint.y) > 4) {
|
|
918
|
-
editor.addAnnotation({
|
|
919
|
-
type: 'line',
|
|
920
|
-
x: dragStartPoint.x,
|
|
921
|
-
y: dragStartPoint.y,
|
|
922
|
-
endX: currentImgPt.x,
|
|
923
|
-
endY: currentImgPt.y,
|
|
924
|
-
strokeColor: activeColor,
|
|
925
|
-
strokeWidth,
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
draftAnnotation = null;
|
|
931
|
-
dragStartPoint = null;
|
|
932
|
-
renderCanvas();
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
257
|
function handleAddText() {
|
|
938
258
|
if (!textInput.trim()) return;
|
|
939
259
|
editor.addAnnotation({
|
|
@@ -962,16 +282,13 @@
|
|
|
962
282
|
|
|
963
283
|
<div
|
|
964
284
|
bind:this={rootEl}
|
|
965
|
-
class="
|
|
285
|
+
class="{imageEditorFrameClass} {className}"
|
|
966
286
|
{style}
|
|
967
287
|
>
|
|
968
|
-
{#if playing}
|
|
969
|
-
<PresenceLayer presence={agentPresence} viewport={docToRoot} />
|
|
970
|
-
{/if}
|
|
971
288
|
<!-- Top Action Header -->
|
|
972
|
-
<div class=
|
|
289
|
+
<div class={imageEditorHeaderClass}>
|
|
973
290
|
<!-- Navigation Tabs -->
|
|
974
|
-
<div
|
|
291
|
+
<div role="tablist" class={imageTabListClass}>
|
|
975
292
|
<button
|
|
976
293
|
type="button"
|
|
977
294
|
data-image-tab="crop"
|
|
@@ -979,9 +296,7 @@
|
|
|
979
296
|
activeTab = 'crop';
|
|
980
297
|
editor.setTool('crop');
|
|
981
298
|
}}
|
|
982
|
-
|
|
983
|
-
? 'bg-indigo-600 text-white shadow'
|
|
984
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
299
|
+
role="tab" aria-selected={activeTab === 'crop'} class="{imageTabClass(activeTab === 'crop')}"
|
|
985
300
|
>
|
|
986
301
|
Transform & Crop
|
|
987
302
|
</button>
|
|
@@ -992,9 +307,7 @@
|
|
|
992
307
|
activeTab = 'adjust';
|
|
993
308
|
editor.setTool('select');
|
|
994
309
|
}}
|
|
995
|
-
|
|
996
|
-
? 'bg-indigo-600 text-white shadow'
|
|
997
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
310
|
+
role="tab" aria-selected={activeTab === 'adjust'} class="{imageTabClass(activeTab === 'adjust')}"
|
|
998
311
|
>
|
|
999
312
|
Adjustments
|
|
1000
313
|
</button>
|
|
@@ -1005,9 +318,7 @@
|
|
|
1005
318
|
activeTab = 'layers';
|
|
1006
319
|
editor.setTool('select');
|
|
1007
320
|
}}
|
|
1008
|
-
|
|
1009
|
-
? 'bg-indigo-600 text-white shadow'
|
|
1010
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
321
|
+
role="tab" aria-selected={activeTab === 'layers'} class="{imageTabClass(activeTab === 'layers')}"
|
|
1011
322
|
>
|
|
1012
323
|
Layers
|
|
1013
324
|
</button>
|
|
@@ -1018,9 +329,7 @@
|
|
|
1018
329
|
activeTab = 'paint';
|
|
1019
330
|
editor.setTool('brush');
|
|
1020
331
|
}}
|
|
1021
|
-
|
|
1022
|
-
? 'bg-indigo-600 text-white shadow'
|
|
1023
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
332
|
+
role="tab" aria-selected={activeTab === 'paint'} class="{imageTabClass(activeTab === 'paint')}"
|
|
1024
333
|
>
|
|
1025
334
|
Paint
|
|
1026
335
|
</button>
|
|
@@ -1031,9 +340,7 @@
|
|
|
1031
340
|
activeTab = 'select';
|
|
1032
341
|
editor.setTool('select');
|
|
1033
342
|
}}
|
|
1034
|
-
|
|
1035
|
-
? 'bg-indigo-600 text-white shadow'
|
|
1036
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
343
|
+
role="tab" aria-selected={activeTab === 'select'} class="{imageTabClass(activeTab === 'select')}"
|
|
1037
344
|
>
|
|
1038
345
|
Select & Mask
|
|
1039
346
|
</button>
|
|
@@ -1044,9 +351,7 @@
|
|
|
1044
351
|
activeTab = 'annotate';
|
|
1045
352
|
editor.setTool('pen');
|
|
1046
353
|
}}
|
|
1047
|
-
|
|
1048
|
-
? 'bg-indigo-600 text-white shadow'
|
|
1049
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
354
|
+
role="tab" aria-selected={activeTab === 'annotate'} class="{imageTabClass(activeTab === 'annotate')}"
|
|
1050
355
|
>
|
|
1051
356
|
Annotations
|
|
1052
357
|
</button>
|
|
@@ -1057,9 +362,7 @@
|
|
|
1057
362
|
activeTab = 'depth';
|
|
1058
363
|
editor.setTool('select');
|
|
1059
364
|
}}
|
|
1060
|
-
|
|
1061
|
-
? 'bg-indigo-600 text-white shadow'
|
|
1062
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
365
|
+
role="tab" aria-selected={activeTab === 'depth'} class="{imageTabClass(activeTab === 'depth')}"
|
|
1063
366
|
>
|
|
1064
367
|
Depth Mask
|
|
1065
368
|
</button>
|
|
@@ -1070,9 +373,7 @@
|
|
|
1070
373
|
activeTab = 'ai';
|
|
1071
374
|
editor.setTool('select');
|
|
1072
375
|
}}
|
|
1073
|
-
|
|
1074
|
-
? 'bg-indigo-600 text-white shadow'
|
|
1075
|
-
: 'text-slate-400 hover:text-slate-200'}"
|
|
376
|
+
role="tab" aria-selected={activeTab === 'ai'} class="{imageTabClass(activeTab === 'ai')} flex items-center gap-1.5"
|
|
1076
377
|
>
|
|
1077
378
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1078
379
|
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
|
@@ -1087,7 +388,7 @@
|
|
|
1087
388
|
type="button"
|
|
1088
389
|
disabled={!editor.canUndo}
|
|
1089
390
|
onclick={() => editor.undo()}
|
|
1090
|
-
class=
|
|
391
|
+
class={imageHeaderButtonClass}
|
|
1091
392
|
title="Undo"
|
|
1092
393
|
>
|
|
1093
394
|
↩
|
|
@@ -1096,7 +397,7 @@
|
|
|
1096
397
|
type="button"
|
|
1097
398
|
disabled={!editor.canRedo}
|
|
1098
399
|
onclick={() => editor.redo()}
|
|
1099
|
-
class=
|
|
400
|
+
class={imageHeaderButtonClass}
|
|
1100
401
|
title="Redo"
|
|
1101
402
|
>
|
|
1102
403
|
↪
|
|
@@ -1104,33 +405,33 @@
|
|
|
1104
405
|
<button
|
|
1105
406
|
type="button"
|
|
1106
407
|
onclick={() => editor.reset()}
|
|
1107
|
-
class="px-2
|
|
408
|
+
class="{imageHeaderButtonClass} px-2 text-muted-foreground"
|
|
1108
409
|
>
|
|
1109
410
|
Reset
|
|
1110
411
|
</button>
|
|
1111
412
|
|
|
1112
|
-
<div class="h-4 w-px bg-
|
|
413
|
+
<div class="h-4 w-px bg-muted mx-1"></div>
|
|
1113
414
|
|
|
1114
415
|
<!-- Zoom -->
|
|
1115
416
|
<button
|
|
1116
417
|
type="button"
|
|
1117
418
|
onclick={() => editor.zoomOut()}
|
|
1118
|
-
class=
|
|
419
|
+
class={imageHeaderButtonClass}
|
|
1119
420
|
title="Zoom Out"
|
|
1120
421
|
>
|
|
1121
422
|
-
|
|
1122
423
|
</button>
|
|
1123
|
-
<span class="font-mono text-
|
|
424
|
+
<span class="font-mono text-muted-foreground w-10 text-center">{Math.round(editor.state.zoom * 100)}%</span>
|
|
1124
425
|
<button
|
|
1125
426
|
type="button"
|
|
1126
427
|
onclick={() => editor.zoomIn()}
|
|
1127
|
-
class=
|
|
428
|
+
class={imageHeaderButtonClass}
|
|
1128
429
|
title="Zoom In"
|
|
1129
430
|
>
|
|
1130
431
|
+
|
|
1131
432
|
</button>
|
|
1132
433
|
|
|
1133
|
-
<div class="h-4 w-px bg-
|
|
434
|
+
<div class="h-4 w-px bg-muted mx-1"></div>
|
|
1134
435
|
|
|
1135
436
|
<!-- Live Compare -->
|
|
1136
437
|
<button
|
|
@@ -1138,7 +439,7 @@
|
|
|
1138
439
|
onmousedown={() => editor.setComparing(true)}
|
|
1139
440
|
onmouseup={() => editor.setComparing(false)}
|
|
1140
441
|
onmouseleave={() => editor.setComparing(false)}
|
|
1141
|
-
class="px-2.5 py-1.5 rounded-lg border border-
|
|
442
|
+
class="px-2.5 py-1.5 rounded-lg border border-border bg-background text-amber-600 hover:bg-muted select-none active:bg-amber-500/10"
|
|
1142
443
|
>
|
|
1143
444
|
Hold Compare
|
|
1144
445
|
</button>
|
|
@@ -1147,7 +448,7 @@
|
|
|
1147
448
|
<button
|
|
1148
449
|
type="button"
|
|
1149
450
|
onclick={handleExport}
|
|
1150
|
-
class="px-4 py-1.5
|
|
451
|
+
class="{imagePrimaryButtonClass} px-4 py-1.5"
|
|
1151
452
|
>
|
|
1152
453
|
Export Image
|
|
1153
454
|
</button>
|
|
@@ -1157,53 +458,40 @@
|
|
|
1157
458
|
<!-- Main Viewport + Sidebar Layout -->
|
|
1158
459
|
<!-- Free Transform options bar (Ctrl/Cmd+T), above the viewport while a transform is open -->
|
|
1159
460
|
{#if transformActive}
|
|
1160
|
-
<FreeTransformOptionsBar tool={transformTool} class="border-b border-
|
|
461
|
+
<FreeTransformOptionsBar tool={transformTool} class="border-b border-border bg-background/95 px-4 py-2" />
|
|
1161
462
|
{:else if transformError}
|
|
1162
|
-
<div role="alert" class=
|
|
463
|
+
<div role="alert" class={imageErrorBannerClass}>{transformError}</div>
|
|
1163
464
|
{/if}
|
|
1164
465
|
|
|
1165
466
|
<div class="flex flex-col md:flex-row flex-1 min-h-[520px] md:h-[560px]">
|
|
1166
467
|
<!-- Canvas Area -->
|
|
1167
|
-
<
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
bind:this={typeInputEl}
|
|
1188
|
-
aria-label="Edit text"
|
|
1189
|
-
data-testid="type-input"
|
|
1190
|
-
autocapitalize="off"
|
|
1191
|
-
spellcheck={false}
|
|
1192
|
-
onkeydown={handleTypeKeyDown}
|
|
1193
|
-
oninput={handleTypeInput}
|
|
1194
|
-
oncompositionend={handleTypeComposition}
|
|
1195
|
-
class="absolute left-0 top-0 h-px w-px opacity-0"
|
|
1196
|
-
></textarea>
|
|
1197
|
-
{/if}
|
|
1198
|
-
|
|
1199
|
-
</div>
|
|
468
|
+
<ImageEditorCanvas
|
|
469
|
+
{editor}
|
|
470
|
+
mode={canvasMode}
|
|
471
|
+
{fit}
|
|
472
|
+
{selectionOptions}
|
|
473
|
+
{shapeOptions}
|
|
474
|
+
{typeOptions}
|
|
475
|
+
annotationColor={activeColor}
|
|
476
|
+
annotationWidth={strokeWidth}
|
|
477
|
+
cropRatio={activeCropRatio}
|
|
478
|
+
onerror={handleCanvasError}
|
|
479
|
+
bind:typeSession
|
|
480
|
+
{transformTool}
|
|
481
|
+
{playback}
|
|
482
|
+
{playbackId}
|
|
483
|
+
{onplaybackreveal}
|
|
484
|
+
chromeRoot={() => rootEl}
|
|
485
|
+
open={openPanel}
|
|
486
|
+
class="flex-1 h-[360px] md:h-full min-h-[320px] bg-muted"
|
|
487
|
+
/>
|
|
1200
488
|
|
|
1201
489
|
<!-- Tab Controls Sidebar -->
|
|
1202
|
-
<div class=
|
|
490
|
+
<div class={imageSidebarClass}>
|
|
1203
491
|
<!-- TAB 1: CROP & TRANSFORM -->
|
|
1204
492
|
{#if activeTab === 'crop'}
|
|
1205
493
|
<div class="flex flex-col gap-4 text-xs">
|
|
1206
|
-
<span class=
|
|
494
|
+
<span class={imageSectionTitleClass}>
|
|
1207
495
|
Aspect Ratio Presets
|
|
1208
496
|
</span>
|
|
1209
497
|
<div class="grid grid-cols-3 gap-2">
|
|
@@ -1214,8 +502,8 @@
|
|
|
1214
502
|
editor.applyCropPreset(null);
|
|
1215
503
|
}}
|
|
1216
504
|
class="px-2.5 py-2 rounded-lg border font-medium {activeCropRatio === null && editor.state.crop
|
|
1217
|
-
? 'bg-
|
|
1218
|
-
: 'bg-
|
|
505
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
506
|
+
: 'bg-muted hover:bg-accent text-foreground border-border'}"
|
|
1219
507
|
>
|
|
1220
508
|
Freeform
|
|
1221
509
|
</button>
|
|
@@ -1226,8 +514,8 @@
|
|
|
1226
514
|
editor.applyCropPreset(1);
|
|
1227
515
|
}}
|
|
1228
516
|
class="px-2.5 py-2 rounded-lg border font-medium {activeCropRatio === 1
|
|
1229
|
-
? 'bg-
|
|
1230
|
-
: 'bg-
|
|
517
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
518
|
+
: 'bg-muted hover:bg-accent text-foreground border-border'}"
|
|
1231
519
|
>
|
|
1232
520
|
1:1 Square
|
|
1233
521
|
</button>
|
|
@@ -1238,8 +526,8 @@
|
|
|
1238
526
|
editor.applyCropPreset(4 / 3);
|
|
1239
527
|
}}
|
|
1240
528
|
class="px-2.5 py-2 rounded-lg border font-medium {activeCropRatio === 4 / 3
|
|
1241
|
-
? 'bg-
|
|
1242
|
-
: 'bg-
|
|
529
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
530
|
+
: 'bg-muted hover:bg-accent text-foreground border-border'}"
|
|
1243
531
|
>
|
|
1244
532
|
4:3 Standard
|
|
1245
533
|
</button>
|
|
@@ -1250,8 +538,8 @@
|
|
|
1250
538
|
editor.applyCropPreset(16 / 9);
|
|
1251
539
|
}}
|
|
1252
540
|
class="px-2.5 py-2 rounded-lg border font-medium {activeCropRatio === 16 / 9
|
|
1253
|
-
? 'bg-
|
|
1254
|
-
: 'bg-
|
|
541
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
542
|
+
: 'bg-muted hover:bg-accent text-foreground border-border'}"
|
|
1255
543
|
>
|
|
1256
544
|
16:9 Wide
|
|
1257
545
|
</button>
|
|
@@ -1262,8 +550,8 @@
|
|
|
1262
550
|
editor.applyCropPreset(3 / 2);
|
|
1263
551
|
}}
|
|
1264
552
|
class="px-2.5 py-2 rounded-lg border font-medium {activeCropRatio === 3 / 2
|
|
1265
|
-
? 'bg-
|
|
1266
|
-
: 'bg-
|
|
553
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
554
|
+
: 'bg-muted hover:bg-accent text-foreground border-border'}"
|
|
1267
555
|
>
|
|
1268
556
|
3:2 Photo
|
|
1269
557
|
</button>
|
|
@@ -1273,29 +561,29 @@
|
|
|
1273
561
|
activeCropRatio = null;
|
|
1274
562
|
editor.resetCrop();
|
|
1275
563
|
}}
|
|
1276
|
-
class="px-2.5 py-2 rounded-lg bg-
|
|
564
|
+
class="px-2.5 py-2 rounded-lg bg-destructive/10 hover:bg-destructive/20 text-destructive border border-destructive/40 font-medium"
|
|
1277
565
|
>
|
|
1278
566
|
Clear Crop
|
|
1279
567
|
</button>
|
|
1280
568
|
</div>
|
|
1281
569
|
|
|
1282
|
-
<div class=
|
|
570
|
+
<div class={imageDividerClass}></div>
|
|
1283
571
|
|
|
1284
|
-
<span class=
|
|
572
|
+
<span class={imageSectionTitleClass}>
|
|
1285
573
|
Orientation & Flips
|
|
1286
574
|
</span>
|
|
1287
575
|
<div class="grid grid-cols-2 gap-2">
|
|
1288
576
|
<button
|
|
1289
577
|
type="button"
|
|
1290
578
|
onclick={() => editor.rotate(-90)}
|
|
1291
|
-
class="px-3 py-2 rounded-lg bg-
|
|
579
|
+
class="px-3 py-2 rounded-lg bg-muted hover:bg-accent text-foreground border border-border"
|
|
1292
580
|
>
|
|
1293
581
|
Rotate -90°
|
|
1294
582
|
</button>
|
|
1295
583
|
<button
|
|
1296
584
|
type="button"
|
|
1297
585
|
onclick={() => editor.rotate(90)}
|
|
1298
|
-
class="px-3 py-2 rounded-lg bg-
|
|
586
|
+
class="px-3 py-2 rounded-lg bg-muted hover:bg-accent text-foreground border border-border"
|
|
1299
587
|
>
|
|
1300
588
|
Rotate +90°
|
|
1301
589
|
</button>
|
|
@@ -1303,8 +591,8 @@
|
|
|
1303
591
|
type="button"
|
|
1304
592
|
onclick={() => editor.flipHorizontal()}
|
|
1305
593
|
class="px-3 py-2 rounded-lg border transition-colors {editor.state.transform.flipH
|
|
1306
|
-
? 'bg-
|
|
1307
|
-
: 'bg-
|
|
594
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
595
|
+
: 'bg-muted border-border text-foreground hover:bg-accent'}"
|
|
1308
596
|
>
|
|
1309
597
|
Flip Horizontal
|
|
1310
598
|
</button>
|
|
@@ -1312,8 +600,8 @@
|
|
|
1312
600
|
type="button"
|
|
1313
601
|
onclick={() => editor.flipVertical()}
|
|
1314
602
|
class="px-3 py-2 rounded-lg border transition-colors {editor.state.transform.flipV
|
|
1315
|
-
? 'bg-
|
|
1316
|
-
: 'bg-
|
|
603
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
604
|
+
: 'bg-muted border-border text-foreground hover:bg-accent'}"
|
|
1317
605
|
>
|
|
1318
606
|
Flip Vertical
|
|
1319
607
|
</button>
|
|
@@ -1325,13 +613,13 @@
|
|
|
1325
613
|
{#if activeTab === 'adjust'}
|
|
1326
614
|
<div class="flex flex-col gap-3 text-xs">
|
|
1327
615
|
<div class="flex items-center justify-between">
|
|
1328
|
-
<span class=
|
|
616
|
+
<span class={imageSectionTitleClass}>
|
|
1329
617
|
Color Adjustments
|
|
1330
618
|
</span>
|
|
1331
619
|
<button
|
|
1332
620
|
type="button"
|
|
1333
621
|
onclick={() => editor.resetAdjustments()}
|
|
1334
|
-
class="text-[10px] text-
|
|
622
|
+
class="text-[10px] text-destructive hover:underline"
|
|
1335
623
|
>
|
|
1336
624
|
Reset Sliders
|
|
1337
625
|
</button>
|
|
@@ -1348,7 +636,7 @@
|
|
|
1348
636
|
/>
|
|
1349
637
|
{#if spec.id === 'tint' && legacyProcess}
|
|
1350
638
|
<div
|
|
1351
|
-
class="flex items-center justify-between gap-2 rounded border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-[10px] text-amber-
|
|
639
|
+
class="flex items-center justify-between gap-2 rounded border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-[10px] text-amber-600"
|
|
1352
640
|
data-testid="image-editor-process-notice"
|
|
1353
641
|
>
|
|
1354
642
|
<span>Legacy rendering (process 1). Tint needs the current process.</span>
|
|
@@ -1363,7 +651,7 @@
|
|
|
1363
651
|
{/if}
|
|
1364
652
|
{/each}
|
|
1365
653
|
|
|
1366
|
-
<div class=
|
|
654
|
+
<div class={imageSectionClass}>
|
|
1367
655
|
<AdjustmentLayersPanel
|
|
1368
656
|
controller={editor.controller}
|
|
1369
657
|
layers={editor.state.adjustments.layers}
|
|
@@ -1376,34 +664,34 @@
|
|
|
1376
664
|
<!-- TAB 3: ANNOTATIONS -->
|
|
1377
665
|
{#if activeTab === 'layers'}
|
|
1378
666
|
<div class="flex flex-col gap-3 text-xs">
|
|
1379
|
-
<span class=
|
|
667
|
+
<span class={imageSectionTitleClass}>Layers</span>
|
|
1380
668
|
<button
|
|
1381
669
|
type="button"
|
|
1382
670
|
title="Edit › Free Transform (Ctrl/Cmd+T): the selected pixels, or the whole active layer"
|
|
1383
|
-
class=
|
|
671
|
+
class={imageSmallButtonClass}
|
|
1384
672
|
disabled={transformActive}
|
|
1385
673
|
onclick={() => transformTool.begin()}>Free Transform</button>
|
|
1386
674
|
<LayersPanel controller={editor.controller} />
|
|
1387
|
-
{#if effectsShown}<ImageProviderEffectsPanel controller={editor.controller} class=
|
|
1388
|
-
<span class=
|
|
675
|
+
{#if effectsShown}<ImageProviderEffectsPanel controller={editor.controller} class={imageSectionClass} />{/if}
|
|
676
|
+
<span class={imageSectionTitleClass}>Colour</span>
|
|
1389
677
|
<ColorProfilePanel controller={editor.controller} />
|
|
1390
678
|
</div>
|
|
1391
679
|
{/if}
|
|
1392
680
|
|
|
1393
681
|
{#if activeTab === 'paint'}
|
|
1394
682
|
<div class="flex flex-col gap-3 text-xs">
|
|
1395
|
-
<span class=
|
|
683
|
+
<span class={imageSectionTitleClass}>Paint</span>
|
|
1396
684
|
<BrushOptionsBar controller={editor.controller} />
|
|
1397
685
|
<button
|
|
1398
686
|
type="button"
|
|
1399
687
|
title="Edit › Free Transform (Ctrl/Cmd+T): the selected pixels, or the whole active layer"
|
|
1400
|
-
class=
|
|
688
|
+
class={imageSmallButtonClass}
|
|
1401
689
|
disabled={transformActive}
|
|
1402
690
|
onclick={() => transformTool.begin()}>Free Transform</button>
|
|
1403
691
|
{#if paintError}
|
|
1404
|
-
<p role="alert" class="text-
|
|
692
|
+
<p role="alert" class="text-destructive" data-testid="paint-error">{paintError}</p>
|
|
1405
693
|
{/if}
|
|
1406
|
-
<div class=
|
|
694
|
+
<div class={imageSectionClass}>
|
|
1407
695
|
<LayersPanel controller={editor.controller} />
|
|
1408
696
|
</div>
|
|
1409
697
|
</div>
|
|
@@ -1411,7 +699,7 @@
|
|
|
1411
699
|
|
|
1412
700
|
{#if activeTab === 'annotate'}
|
|
1413
701
|
<div class="flex flex-col gap-3 text-xs">
|
|
1414
|
-
<span class=
|
|
702
|
+
<span class={imageSectionTitleClass}>Shape & Type Layers</span>
|
|
1415
703
|
<div class="grid grid-cols-2 gap-1.5">
|
|
1416
704
|
{#each ['shape', 'type'] as const as t (t)}
|
|
1417
705
|
<button
|
|
@@ -1419,8 +707,8 @@
|
|
|
1419
707
|
aria-pressed={editor.state.activeTool === t}
|
|
1420
708
|
onclick={() => editor.setTool(t)}
|
|
1421
709
|
class="py-1.5 px-2 rounded-lg font-medium border transition-colors {editor.state.activeTool === t
|
|
1422
|
-
? 'bg-
|
|
1423
|
-
: 'bg-
|
|
710
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
711
|
+
: 'bg-muted border-border text-foreground hover:bg-accent'}">{t === 'shape' ? 'Shape' : 'Type'}</button>
|
|
1424
712
|
{/each}
|
|
1425
713
|
</div>
|
|
1426
714
|
{#if editor.state.activeTool === 'shape'}
|
|
@@ -1429,15 +717,15 @@
|
|
|
1429
717
|
{#if editor.state.activeTool === 'type'}
|
|
1430
718
|
<TypeOptionsBar controller={editor.controller} options={typeOptions} onchange={(o) => (typeOptions = o)} session={typeSession} />
|
|
1431
719
|
{#if typeSession}
|
|
1432
|
-
<button type="button" onclick={finishTyping} class="self-start rounded bg-
|
|
720
|
+
<button type="button" onclick={finishTyping} class="self-start rounded bg-muted px-2 py-1 text-[11px] text-foreground hover:bg-accent">Done editing text</button>
|
|
1433
721
|
{/if}
|
|
1434
722
|
{/if}
|
|
1435
723
|
{#if shapeTypeError}
|
|
1436
|
-
<p role="alert" class="text-
|
|
724
|
+
<p role="alert" class="text-destructive" data-testid="shape-type-error">{shapeTypeError}</p>
|
|
1437
725
|
{/if}
|
|
1438
|
-
<div class=
|
|
726
|
+
<div class={imageDividerClass}></div>
|
|
1439
727
|
|
|
1440
|
-
<span class=
|
|
728
|
+
<span class={imageSectionTitleClass}>
|
|
1441
729
|
Draw & Annotate
|
|
1442
730
|
</span>
|
|
1443
731
|
|
|
@@ -1448,8 +736,8 @@
|
|
|
1448
736
|
type="button"
|
|
1449
737
|
onclick={() => editor.setTool(t)}
|
|
1450
738
|
class="py-1.5 px-2 rounded-lg font-medium capitalize border transition-colors {editor.state.activeTool === t
|
|
1451
|
-
? 'bg-
|
|
1452
|
-
: 'bg-
|
|
739
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
740
|
+
: 'bg-muted border-border text-foreground hover:bg-accent'}"
|
|
1453
741
|
>
|
|
1454
742
|
{t}
|
|
1455
743
|
</button>
|
|
@@ -1458,14 +746,14 @@
|
|
|
1458
746
|
|
|
1459
747
|
<!-- Color & Size -->
|
|
1460
748
|
<div class="flex items-center gap-2 mt-2">
|
|
1461
|
-
<span class="text-
|
|
749
|
+
<span class="text-muted-foreground">Color:</span>
|
|
1462
750
|
<input
|
|
1463
751
|
type="color"
|
|
1464
752
|
bind:value={activeColor}
|
|
1465
|
-
class="w-8 h-8 rounded border border-
|
|
753
|
+
class="w-8 h-8 rounded border border-border cursor-pointer bg-transparent"
|
|
1466
754
|
/>
|
|
1467
755
|
<div class="flex-1 ml-2">
|
|
1468
|
-
<div class="flex justify-between text-
|
|
756
|
+
<div class="flex justify-between text-muted-foreground mb-0.5">
|
|
1469
757
|
<span>Width:</span>
|
|
1470
758
|
<span class="font-mono">{strokeWidth}px</span>
|
|
1471
759
|
</div>
|
|
@@ -1474,32 +762,32 @@
|
|
|
1474
762
|
min="1"
|
|
1475
763
|
max="20"
|
|
1476
764
|
bind:value={strokeWidth}
|
|
1477
|
-
class=
|
|
765
|
+
class={imageRangeClass}
|
|
1478
766
|
/>
|
|
1479
767
|
</div>
|
|
1480
768
|
</div>
|
|
1481
769
|
|
|
1482
|
-
<div class=
|
|
770
|
+
<div class={imageDividerClass}></div>
|
|
1483
771
|
|
|
1484
772
|
<!-- Text Tool -->
|
|
1485
|
-
<span class=
|
|
773
|
+
<span class={imageSectionTitleClass}>Add Text</span>
|
|
1486
774
|
<div class="flex gap-2">
|
|
1487
775
|
<input
|
|
1488
776
|
type="text"
|
|
1489
777
|
placeholder="Type watermark or label..."
|
|
1490
778
|
bind:value={textInput}
|
|
1491
779
|
onkeydown={(e) => e.key === 'Enter' && handleAddText()}
|
|
1492
|
-
class="flex-1 bg-
|
|
780
|
+
class="flex-1 bg-background border border-border rounded-lg px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary"
|
|
1493
781
|
/>
|
|
1494
782
|
<button
|
|
1495
783
|
type="button"
|
|
1496
784
|
onclick={handleAddText}
|
|
1497
|
-
class="px-3 py-1.5 rounded-lg bg-
|
|
785
|
+
class="px-3 py-1.5 rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium shadow"
|
|
1498
786
|
>
|
|
1499
787
|
Add
|
|
1500
788
|
</button>
|
|
1501
789
|
</div>
|
|
1502
|
-
<div class="flex items-center justify-between text-
|
|
790
|
+
<div class="flex items-center justify-between text-muted-foreground">
|
|
1503
791
|
<span>Font Size:</span>
|
|
1504
792
|
<span class="font-mono">{fontSize}px</span>
|
|
1505
793
|
</div>
|
|
@@ -1508,18 +796,18 @@
|
|
|
1508
796
|
min="12"
|
|
1509
797
|
max="72"
|
|
1510
798
|
bind:value={fontSize}
|
|
1511
|
-
class=
|
|
799
|
+
class={imageRangeClass}
|
|
1512
800
|
/>
|
|
1513
801
|
|
|
1514
802
|
<!-- Layers & Selected Annotation -->
|
|
1515
|
-
<div class=
|
|
803
|
+
<div class={imageDividerClass}></div>
|
|
1516
804
|
<div class="flex items-center justify-between">
|
|
1517
|
-
<span class="text-
|
|
805
|
+
<span class="text-muted-foreground">Layers ({editor.state.annotations.length})</span>
|
|
1518
806
|
{#if editor.state.selectedAnnotationId}
|
|
1519
807
|
<button
|
|
1520
808
|
type="button"
|
|
1521
809
|
onclick={() => editor.removeAnnotation(editor.state.selectedAnnotationId!)}
|
|
1522
|
-
class="text-
|
|
810
|
+
class="text-destructive hover:underline text-[11px]"
|
|
1523
811
|
>
|
|
1524
812
|
Delete Selected
|
|
1525
813
|
</button>
|
|
@@ -1528,7 +816,7 @@
|
|
|
1528
816
|
<button
|
|
1529
817
|
type="button"
|
|
1530
818
|
onclick={() => editor.clearAnnotations()}
|
|
1531
|
-
class="text-
|
|
819
|
+
class="text-muted-foreground hover:text-foreground text-[11px]"
|
|
1532
820
|
>
|
|
1533
821
|
Clear All
|
|
1534
822
|
</button>
|
|
@@ -1541,37 +829,37 @@
|
|
|
1541
829
|
{#if activeTab === 'depth'}
|
|
1542
830
|
<div class="flex flex-col gap-3 text-xs">
|
|
1543
831
|
<div class="flex items-center justify-between">
|
|
1544
|
-
<span class=
|
|
832
|
+
<span class={imageSectionTitleClass}>
|
|
1545
833
|
Depth Masking
|
|
1546
834
|
</span>
|
|
1547
835
|
<button
|
|
1548
836
|
type="button"
|
|
1549
837
|
onclick={() => editor.resetDepthMask()}
|
|
1550
|
-
class="text-[10px] text-
|
|
838
|
+
class="text-[10px] text-destructive hover:underline"
|
|
1551
839
|
>
|
|
1552
840
|
Reset Mask
|
|
1553
841
|
</button>
|
|
1554
842
|
</div>
|
|
1555
843
|
|
|
1556
|
-
<label class="flex items-center gap-2 cursor-pointer text-
|
|
844
|
+
<label class="flex items-center gap-2 cursor-pointer text-foreground">
|
|
1557
845
|
<input
|
|
1558
846
|
type="checkbox"
|
|
1559
847
|
checked={editor.state.depthMask.enabled}
|
|
1560
848
|
onchange={(e) => editor.setDepthMask({ enabled: (e.target as HTMLInputElement).checked })}
|
|
1561
|
-
class="rounded accent-
|
|
849
|
+
class="rounded accent-primary"
|
|
1562
850
|
/>
|
|
1563
851
|
<span>Enable Depth / Alpha Mask</span>
|
|
1564
852
|
</label>
|
|
1565
853
|
|
|
1566
854
|
{#if editor.state.depthMask.enabled}
|
|
1567
855
|
<div>
|
|
1568
|
-
<span class="text-
|
|
856
|
+
<span class="text-muted-foreground block mb-1">Mask Image URL:</span>
|
|
1569
857
|
<input
|
|
1570
858
|
type="text"
|
|
1571
859
|
placeholder="https://.../depthmap.png"
|
|
1572
860
|
value={editor.state.depthMask.maskSource || ''}
|
|
1573
861
|
oninput={(e) => editor.setDepthMask({ maskSource: (e.target as HTMLInputElement).value || null })}
|
|
1574
|
-
class="w-full bg-
|
|
862
|
+
class="w-full bg-background border border-border rounded-lg px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground"
|
|
1575
863
|
/>
|
|
1576
864
|
</div>
|
|
1577
865
|
|
|
@@ -1580,8 +868,8 @@
|
|
|
1580
868
|
type="button"
|
|
1581
869
|
onclick={() => editor.setDepthMask({ mode: 'threshold' })}
|
|
1582
870
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium {editor.state.depthMask.mode === 'threshold'
|
|
1583
|
-
? 'bg-
|
|
1584
|
-
: 'bg-
|
|
871
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
872
|
+
: 'bg-muted border-border text-foreground'}"
|
|
1585
873
|
>
|
|
1586
874
|
Threshold Mode
|
|
1587
875
|
</button>
|
|
@@ -1589,15 +877,15 @@
|
|
|
1589
877
|
type="button"
|
|
1590
878
|
onclick={() => editor.setDepthMask({ mode: 'brightness' })}
|
|
1591
879
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium {editor.state.depthMask.mode === 'brightness'
|
|
1592
|
-
? 'bg-
|
|
1593
|
-
: 'bg-
|
|
880
|
+
? 'bg-primary border-primary text-primary-foreground'
|
|
881
|
+
: 'bg-muted border-border text-foreground'}"
|
|
1594
882
|
>
|
|
1595
883
|
Brightness Mode
|
|
1596
884
|
</button>
|
|
1597
885
|
</div>
|
|
1598
886
|
|
|
1599
887
|
<div>
|
|
1600
|
-
<div class="flex justify-between text-
|
|
888
|
+
<div class="flex justify-between text-muted-foreground mb-1">
|
|
1601
889
|
<span>Depth Start (Lower):</span>
|
|
1602
890
|
<span class="font-mono">{editor.state.depthMask.depthRange[0]}</span>
|
|
1603
891
|
</div>
|
|
@@ -1610,12 +898,12 @@
|
|
|
1610
898
|
editor.setDepthMask({
|
|
1611
899
|
depthRange: [Number((e.target as HTMLInputElement).value), editor.state.depthMask.depthRange[1]],
|
|
1612
900
|
})}
|
|
1613
|
-
class=
|
|
901
|
+
class={imageRangeClass}
|
|
1614
902
|
/>
|
|
1615
903
|
</div>
|
|
1616
904
|
|
|
1617
905
|
<div>
|
|
1618
|
-
<div class="flex justify-between text-
|
|
906
|
+
<div class="flex justify-between text-muted-foreground mb-1">
|
|
1619
907
|
<span>Depth Stop (Upper):</span>
|
|
1620
908
|
<span class="font-mono">{editor.state.depthMask.depthRange[1]}</span>
|
|
1621
909
|
</div>
|
|
@@ -1628,12 +916,12 @@
|
|
|
1628
916
|
editor.setDepthMask({
|
|
1629
917
|
depthRange: [editor.state.depthMask.depthRange[0], Number((e.target as HTMLInputElement).value)],
|
|
1630
918
|
})}
|
|
1631
|
-
class=
|
|
919
|
+
class={imageRangeClass}
|
|
1632
920
|
/>
|
|
1633
921
|
</div>
|
|
1634
922
|
|
|
1635
923
|
<div>
|
|
1636
|
-
<div class="flex justify-between text-
|
|
924
|
+
<div class="flex justify-between text-muted-foreground mb-1">
|
|
1637
925
|
<span>Softness:</span>
|
|
1638
926
|
<span class="font-mono">{editor.state.depthMask.softness}</span>
|
|
1639
927
|
</div>
|
|
@@ -1643,16 +931,16 @@
|
|
|
1643
931
|
max="50"
|
|
1644
932
|
value={editor.state.depthMask.softness}
|
|
1645
933
|
oninput={(e) => editor.setDepthMask({ softness: Number((e.target as HTMLInputElement).value) })}
|
|
1646
|
-
class=
|
|
934
|
+
class={imageRangeClass}
|
|
1647
935
|
/>
|
|
1648
936
|
</div>
|
|
1649
937
|
|
|
1650
|
-
<label class="flex items-center gap-2 cursor-pointer text-
|
|
938
|
+
<label class="flex items-center gap-2 cursor-pointer text-foreground mt-1">
|
|
1651
939
|
<input
|
|
1652
940
|
type="checkbox"
|
|
1653
941
|
checked={editor.state.depthMask.invert}
|
|
1654
942
|
onchange={(e) => editor.setDepthMask({ invert: (e.target as HTMLInputElement).checked })}
|
|
1655
|
-
class="rounded accent-
|
|
943
|
+
class="rounded accent-primary"
|
|
1656
944
|
/>
|
|
1657
945
|
<span>Invert Mask</span>
|
|
1658
946
|
</label>
|
|
@@ -1671,19 +959,19 @@
|
|
|
1671
959
|
quickMask={Boolean(editor.state.quickMask)}
|
|
1672
960
|
/>
|
|
1673
961
|
{#if selectionError}
|
|
1674
|
-
<p role="alert" class="text-
|
|
962
|
+
<p role="alert" class="text-destructive">{selectionError}</p>
|
|
1675
963
|
{/if}
|
|
1676
964
|
<div class="flex gap-1">
|
|
1677
965
|
<button
|
|
1678
966
|
type="button"
|
|
1679
967
|
title="Select › Transform Selection: the outline only"
|
|
1680
|
-
class=
|
|
968
|
+
class={imageSmallButtonClass}
|
|
1681
969
|
disabled={transformActive}
|
|
1682
970
|
onclick={() => transformTool.begin({ content: 'selection-outline' })}>Transform Selection</button>
|
|
1683
971
|
<button
|
|
1684
972
|
type="button"
|
|
1685
973
|
title="Edit › Free Transform: the selected pixels of the active layer"
|
|
1686
|
-
class=
|
|
974
|
+
class={imageSmallButtonClass}
|
|
1687
975
|
disabled={transformActive}
|
|
1688
976
|
onclick={() => transformTool.begin({ content: 'selection' })}>Free Transform Pixels</button>
|
|
1689
977
|
</div>
|
|
@@ -1698,12 +986,12 @@
|
|
|
1698
986
|
<!-- TAB 5: AI TOOLS / INPAINTING & BACKGROUND REMOVAL -->
|
|
1699
987
|
{#if activeTab === 'ai'}
|
|
1700
988
|
<div class="flex flex-col gap-4 text-xs">
|
|
1701
|
-
<label class="flex items-center justify-between gap-2 text-
|
|
989
|
+
<label class="flex items-center justify-between gap-2 text-foreground">
|
|
1702
990
|
<span>Results go to</span>
|
|
1703
991
|
<select
|
|
1704
992
|
aria-label="AI result target"
|
|
1705
993
|
bind:value={aiTarget}
|
|
1706
|
-
class=
|
|
994
|
+
class={imageSelectClass}
|
|
1707
995
|
>
|
|
1708
996
|
<option value="auto">{editor.controller.isMultiLayer ? 'Auto (new layer)' : 'Auto (replace image)'}</option>
|
|
1709
997
|
<option value="new-layer">New layer / mask</option>
|
|
@@ -1711,22 +999,22 @@
|
|
|
1711
999
|
</select>
|
|
1712
1000
|
</label>
|
|
1713
1001
|
<!-- Background Removal Section -->
|
|
1714
|
-
<div class="p-3 rounded-lg bg-
|
|
1002
|
+
<div class="p-3 rounded-lg bg-muted/40 border border-border flex flex-col gap-2.5">
|
|
1715
1003
|
<div class="flex items-center justify-between">
|
|
1716
|
-
<span class="font-semibold text-
|
|
1717
|
-
<svg class="w-3.5 h-3.5 text-
|
|
1004
|
+
<span class="font-semibold text-foreground uppercase tracking-wider text-[11px] flex items-center gap-1.5">
|
|
1005
|
+
<svg class="w-3.5 h-3.5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1718
1006
|
<path d="M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
|
1719
1007
|
</svg>
|
|
1720
1008
|
Background Removal
|
|
1721
1009
|
</span>
|
|
1722
1010
|
{#if isRemovingBg}
|
|
1723
|
-
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-
|
|
1011
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-primary/15 text-primary animate-pulse">
|
|
1724
1012
|
Removing...
|
|
1725
1013
|
</span>
|
|
1726
1014
|
{/if}
|
|
1727
1015
|
</div>
|
|
1728
1016
|
|
|
1729
|
-
<p class="text-
|
|
1017
|
+
<p class="text-muted-foreground text-[11px] leading-relaxed">
|
|
1730
1018
|
Isolate foreground subject with AI alpha transparency segmentation.
|
|
1731
1019
|
</p>
|
|
1732
1020
|
|
|
@@ -1734,10 +1022,10 @@
|
|
|
1734
1022
|
type="button"
|
|
1735
1023
|
onclick={handleRemoveBackground}
|
|
1736
1024
|
disabled={isRemovingBg || isInpainting}
|
|
1737
|
-
class="mt-1 px-3 py-2 rounded-lg bg-
|
|
1025
|
+
class="mt-1 px-3 py-2 rounded-lg bg-primary hover:bg-primary/90 disabled:opacity-50 text-primary-foreground font-medium shadow transition-colors flex items-center justify-center gap-2"
|
|
1738
1026
|
>
|
|
1739
1027
|
{#if isRemovingBg}
|
|
1740
|
-
<div class=
|
|
1028
|
+
<div class={imageSpinnerClass}></div>
|
|
1741
1029
|
Removing Background...
|
|
1742
1030
|
{:else}
|
|
1743
1031
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -1749,46 +1037,46 @@
|
|
|
1749
1037
|
</div>
|
|
1750
1038
|
|
|
1751
1039
|
<!-- AI Inpainting / Generative Fill Section -->
|
|
1752
|
-
<div class="p-3 rounded-lg bg-
|
|
1040
|
+
<div class="p-3 rounded-lg bg-muted/40 border border-border flex flex-col gap-2.5">
|
|
1753
1041
|
<div class="flex items-center justify-between">
|
|
1754
|
-
<span class="font-semibold text-
|
|
1755
|
-
<svg class="w-3.5 h-3.5 text-
|
|
1042
|
+
<span class="font-semibold text-foreground uppercase tracking-wider text-[11px] flex items-center gap-1.5">
|
|
1043
|
+
<svg class="w-3.5 h-3.5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1756
1044
|
<path d="m21.64 3.64-1.28-1.28a1.2 1.2 0 0 0-1.7 0l-9.72 9.72-1.94 4.86 4.86-1.94 9.72-9.72a1.2 1.2 0 0 0 .06-1.64z"/>
|
|
1757
1045
|
<path d="m14 7 3 3"/>
|
|
1758
1046
|
</svg>
|
|
1759
1047
|
Generative Inpainting
|
|
1760
1048
|
</span>
|
|
1761
1049
|
{#if isInpainting}
|
|
1762
|
-
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-
|
|
1050
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-primary/15 text-primary animate-pulse">
|
|
1763
1051
|
Inpainting...
|
|
1764
1052
|
</span>
|
|
1765
1053
|
{/if}
|
|
1766
1054
|
</div>
|
|
1767
1055
|
|
|
1768
|
-
<p class="text-
|
|
1056
|
+
<p class="text-muted-foreground text-[11px] leading-relaxed">
|
|
1769
1057
|
Replace, modify, or fill selected regions using prompt-guided generative inpainting.
|
|
1770
1058
|
</p>
|
|
1771
1059
|
|
|
1772
1060
|
<div class="flex flex-col gap-1">
|
|
1773
|
-
<label for="ai-inpaint-prompt" class="text-
|
|
1061
|
+
<label for="ai-inpaint-prompt" class="text-foreground text-[11px] font-medium">Prompt</label>
|
|
1774
1062
|
<textarea
|
|
1775
1063
|
id="ai-inpaint-prompt"
|
|
1776
1064
|
bind:value={inpaintPrompt}
|
|
1777
1065
|
placeholder="e.g. replace sunglasses with steampunk goggles, remove person in background..."
|
|
1778
1066
|
rows="3"
|
|
1779
|
-
class="w-full bg-
|
|
1067
|
+
class="w-full bg-background border border-border rounded-lg p-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary resize-none"
|
|
1780
1068
|
></textarea>
|
|
1781
1069
|
</div>
|
|
1782
1070
|
|
|
1783
1071
|
<div class="flex flex-col gap-1">
|
|
1784
|
-
<span class="text-
|
|
1072
|
+
<span class="text-foreground text-[11px] font-medium">Target Mask Region</span>
|
|
1785
1073
|
<div class="grid grid-cols-2 gap-1.5">
|
|
1786
1074
|
<button
|
|
1787
1075
|
type="button"
|
|
1788
1076
|
onclick={() => inpaintMaskMode = 'annotations'}
|
|
1789
1077
|
class="px-2 py-1.5 rounded border text-[11px] font-medium text-left transition-colors {inpaintMaskMode === 'annotations'
|
|
1790
|
-
? 'bg-
|
|
1791
|
-
: 'bg-
|
|
1078
|
+
? 'bg-primary/30 border-primary text-primary'
|
|
1079
|
+
: 'bg-background border-border text-muted-foreground hover:text-foreground'}"
|
|
1792
1080
|
>
|
|
1793
1081
|
Drawn Shapes ({editor.state.annotations.length})
|
|
1794
1082
|
</button>
|
|
@@ -1796,8 +1084,8 @@
|
|
|
1796
1084
|
type="button"
|
|
1797
1085
|
onclick={() => inpaintMaskMode = 'crop'}
|
|
1798
1086
|
class="px-2 py-1.5 rounded border text-[11px] font-medium text-left transition-colors {inpaintMaskMode === 'crop'
|
|
1799
|
-
? 'bg-
|
|
1800
|
-
: 'bg-
|
|
1087
|
+
? 'bg-primary/30 border-primary text-primary'
|
|
1088
|
+
: 'bg-background border-border text-muted-foreground hover:text-foreground'}"
|
|
1801
1089
|
>
|
|
1802
1090
|
Crop Box ({editor.state.crop ? 'Selected' : 'Full'})
|
|
1803
1091
|
</button>
|
|
@@ -1808,10 +1096,10 @@
|
|
|
1808
1096
|
type="button"
|
|
1809
1097
|
onclick={handleInpaint}
|
|
1810
1098
|
disabled={isInpainting || isRemovingBg || !inpaintPrompt.trim()}
|
|
1811
|
-
class="mt-1 px-3 py-2 rounded-lg bg-
|
|
1099
|
+
class="mt-1 px-3 py-2 rounded-lg bg-primary hover:bg-primary/90 disabled:opacity-50 text-primary-foreground font-medium shadow transition-colors flex items-center justify-center gap-2"
|
|
1812
1100
|
>
|
|
1813
1101
|
{#if isInpainting}
|
|
1814
|
-
<div class=
|
|
1102
|
+
<div class={imageSpinnerClass}></div>
|
|
1815
1103
|
Generating Inpaint...
|
|
1816
1104
|
{:else}
|
|
1817
1105
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -1823,13 +1111,13 @@
|
|
|
1823
1111
|
</div>
|
|
1824
1112
|
|
|
1825
1113
|
{#if aiError}
|
|
1826
|
-
<div class="p-2.5 rounded-lg bg-
|
|
1114
|
+
<div class="p-2.5 rounded-lg bg-destructive/10 border border-destructive/40 text-destructive text-[11px]">
|
|
1827
1115
|
{aiError}
|
|
1828
1116
|
</div>
|
|
1829
1117
|
{/if}
|
|
1830
1118
|
|
|
1831
1119
|
{#if aiSuccess}
|
|
1832
|
-
<div class="p-2.5 rounded-lg bg-emerald-
|
|
1120
|
+
<div class="p-2.5 rounded-lg bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 text-[11px]">
|
|
1833
1121
|
{aiSuccess}
|
|
1834
1122
|
</div>
|
|
1835
1123
|
{/if}
|