@pixerate/editor-svelte 0.107.0 → 0.108.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/image-editor/ImageEditor.svelte +45 -742
- package/dist/image-editor/ImageEditorCanvas.svelte +825 -0
- package/dist/image-editor/ImageEditorCanvas.svelte.d.ts +64 -0
- 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/playback/PresenceLayer.svelte +4 -2
- package/dist/playback/PresenceLayer.svelte.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type {
|
|
3
|
-
Annotation,
|
|
4
|
-
CropBox,
|
|
5
|
-
CropHandle,
|
|
6
3
|
ExportOptions,
|
|
7
4
|
ImageEditorTool,
|
|
8
|
-
ImagePoint,
|
|
9
5
|
InpaintHook,
|
|
10
6
|
RemoveBackgroundHook,
|
|
11
7
|
ImageEditorAIHooks,
|
|
12
8
|
ImageEditorFit,
|
|
13
9
|
} 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';
|
|
10
|
+
import type { AgentPlayback, PlaybackRef } from '@pixerate/editor/tools';
|
|
17
11
|
import {
|
|
18
|
-
canvasToImagePoint,
|
|
19
|
-
imageToCanvasPoint,
|
|
20
|
-
createImagePlaybackLocator,
|
|
21
|
-
hitTestCrop,
|
|
22
|
-
getCropCursor,
|
|
23
|
-
calculateCropDrag,
|
|
24
|
-
hitTestAnnotation,
|
|
25
12
|
normalizeProcessVersion,
|
|
26
13
|
LATEST_PROCESS_VERSION,
|
|
27
14
|
IMAGE_ADJUSTMENT_CONTROLS,
|
|
28
15
|
readAdjustmentControls,
|
|
29
16
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
30
|
-
SelectionGesture,
|
|
31
|
-
PaintGesture,
|
|
32
|
-
handlePaintShortcut,
|
|
33
|
-
pointerPressure,
|
|
34
17
|
FreeTransformTool,
|
|
35
|
-
getViewportProjection,
|
|
36
|
-
ShapeDrawGesture,
|
|
37
|
-
TypeToolGesture,
|
|
38
18
|
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
39
19
|
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
40
20
|
type SelectionToolOptions,
|
|
@@ -42,6 +22,8 @@
|
|
|
42
22
|
type TextEditSession,
|
|
43
23
|
type TypeToolOptions,
|
|
44
24
|
} from '@pixerate/editor/image-editor';
|
|
25
|
+
import ImageEditorCanvas from './ImageEditorCanvas.svelte';
|
|
26
|
+
import type { ImageEditorCanvasErrorSource, ImageEditorCanvasMode } from './canvasTypes.js';
|
|
45
27
|
import ControlSlider from './ControlSlider.svelte';
|
|
46
28
|
import Histogram from './Histogram.svelte';
|
|
47
29
|
import AdjustmentLayersPanel from './AdjustmentLayersPanel.svelte';
|
|
@@ -115,204 +97,59 @@
|
|
|
115
97
|
normalizeProcessVersion(editor.state.adjustments.process) < LATEST_PROCESS_VERSION
|
|
116
98
|
);
|
|
117
99
|
|
|
118
|
-
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
119
|
-
let containerEl = $state<HTMLDivElement | null>(null);
|
|
120
100
|
let activeTab = $state<'crop' | 'adjust' | 'layers' | 'paint' | 'select' | 'annotate' | 'depth' | 'ai'>('crop');
|
|
121
101
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
102
|
+
/** The tabs with canvas gestures drive the canvas mode; the others only view. */
|
|
103
|
+
const canvasMode = $derived<ImageEditorCanvasMode>(
|
|
104
|
+
activeTab === 'crop' || activeTab === 'select' || activeTab === 'paint' || activeTab === 'annotate' ? activeTab : 'none'
|
|
105
|
+
);
|
|
122
106
|
|
|
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
|
-
}));
|
|
107
|
+
/** Agent playback opens a panel by clicking its tab, unless it is already open. */
|
|
108
|
+
async function openPanel(ref: PlaybackRef) {
|
|
109
|
+
if (ref.kind !== 'panel' || activeTab === ref.id) return;
|
|
110
|
+
rootEl?.querySelector<HTMLElement>(`[data-image-tab="${CSS.escape(ref.id)}"]`)?.click();
|
|
111
|
+
// Let the panel render before the cursor looks for its controls.
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
113
|
+
}
|
|
182
114
|
|
|
183
|
-
//
|
|
115
|
+
// Tool options for the canvas, set by the options bars.
|
|
184
116
|
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
117
|
let shapeOptions = $state<ShapeToolOptions>({ ...DEFAULT_SHAPE_TOOL_OPTIONS });
|
|
192
118
|
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
119
|
let typeSession = $state<TextEditSession | null>(null);
|
|
196
|
-
let typeInputEl = $state<HTMLTextAreaElement | null>(null);
|
|
197
120
|
let shapeTypeError = $state<string | null>(null);
|
|
198
|
-
$
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
untrack(() => {
|
|
207
|
-
if (!keep && typeTool.session) finishTyping();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
121
|
+
let paintError = $state<string | null>(null);
|
|
122
|
+
let selectionError = $state<string | null>(null);
|
|
123
|
+
let activeCropRatio = $state<number | null>(null);
|
|
124
|
+
function handleCanvasError(source: ImageEditorCanvasErrorSource, message: string | null) {
|
|
125
|
+
if (source === 'paint') paintError = message;
|
|
126
|
+
else if (source === 'select') selectionError = message;
|
|
127
|
+
else shapeTypeError = message;
|
|
128
|
+
}
|
|
210
129
|
function finishTyping() {
|
|
211
|
-
|
|
130
|
+
// The canvas finishes the edit when its bound session is cleared.
|
|
212
131
|
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
132
|
}
|
|
243
133
|
|
|
244
|
-
// Free Transform (Ctrl/Cmd+T):
|
|
134
|
+
// Free Transform (Ctrl/Cmd+T): shared with the canvas, which drives and draws it.
|
|
245
135
|
const transformTool = $derived(
|
|
246
136
|
new FreeTransformTool(editor.controller, {
|
|
247
137
|
schedule: (run) => (typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(run) : run()),
|
|
248
138
|
})
|
|
249
139
|
);
|
|
250
140
|
let transformTick = $state(0);
|
|
251
|
-
let transformCursor = $state('default');
|
|
252
141
|
const transformActive = $derived((void transformTick, transformTool.active));
|
|
253
142
|
const transformError = $derived((void transformTick, transformTool.error));
|
|
254
143
|
$effect(() => {
|
|
255
144
|
const tool = transformTool;
|
|
256
145
|
const off = tool.subscribe(() => {
|
|
257
146
|
transformTick++;
|
|
258
|
-
void renderCanvas();
|
|
259
147
|
});
|
|
260
148
|
return () => {
|
|
261
149
|
off();
|
|
262
150
|
tool.cancel();
|
|
263
151
|
};
|
|
264
152
|
});
|
|
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
153
|
|
|
317
154
|
// AI state
|
|
318
155
|
let inpaintPrompt = $state('');
|
|
@@ -398,24 +235,6 @@
|
|
|
398
235
|
let fontSize = $state(24);
|
|
399
236
|
let textInput = $state('');
|
|
400
237
|
|
|
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
238
|
// Load the image when `src` changes. Only `src` is tracked: AI edits replace
|
|
420
239
|
// the editor's source with a data URL and must not be reverted to `src`.
|
|
421
240
|
let loadedSrc: string | undefined;
|
|
@@ -428,512 +247,12 @@
|
|
|
428
247
|
});
|
|
429
248
|
});
|
|
430
249
|
|
|
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
250
|
$effect(() => {
|
|
523
251
|
if (activeTab === 'crop' && editor.state.activeTool !== 'crop') {
|
|
524
252
|
editor.setTool('crop');
|
|
525
253
|
}
|
|
526
254
|
});
|
|
527
255
|
|
|
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
256
|
function handleAddText() {
|
|
938
257
|
if (!textInput.trim()) return;
|
|
939
258
|
editor.addAnnotation({
|
|
@@ -965,9 +284,6 @@
|
|
|
965
284
|
class="relative flex flex-col bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl text-slate-100 {className}"
|
|
966
285
|
{style}
|
|
967
286
|
>
|
|
968
|
-
{#if playing}
|
|
969
|
-
<PresenceLayer presence={agentPresence} viewport={docToRoot} />
|
|
970
|
-
{/if}
|
|
971
287
|
<!-- Top Action Header -->
|
|
972
288
|
<div class="flex flex-wrap items-center justify-between px-4 py-3 bg-slate-950/80 border-b border-slate-800 gap-3">
|
|
973
289
|
<!-- Navigation Tabs -->
|
|
@@ -1164,39 +480,26 @@
|
|
|
1164
480
|
|
|
1165
481
|
<div class="flex flex-col md:flex-row flex-1 min-h-[520px] md:h-[560px]">
|
|
1166
482
|
<!-- 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>
|
|
483
|
+
<ImageEditorCanvas
|
|
484
|
+
{editor}
|
|
485
|
+
mode={canvasMode}
|
|
486
|
+
{fit}
|
|
487
|
+
{selectionOptions}
|
|
488
|
+
{shapeOptions}
|
|
489
|
+
{typeOptions}
|
|
490
|
+
annotationColor={activeColor}
|
|
491
|
+
annotationWidth={strokeWidth}
|
|
492
|
+
cropRatio={activeCropRatio}
|
|
493
|
+
onerror={handleCanvasError}
|
|
494
|
+
bind:typeSession
|
|
495
|
+
{transformTool}
|
|
496
|
+
{playback}
|
|
497
|
+
{playbackId}
|
|
498
|
+
{onplaybackreveal}
|
|
499
|
+
chromeRoot={() => rootEl}
|
|
500
|
+
open={openPanel}
|
|
501
|
+
class="flex-1 h-[360px] md:h-full min-h-[320px] bg-slate-950"
|
|
502
|
+
/>
|
|
1200
503
|
|
|
1201
504
|
<!-- Tab Controls Sidebar -->
|
|
1202
505
|
<div class="w-full md:w-80 bg-slate-900/90 border-t md:border-t-0 md:border-l border-slate-800 p-4 overflow-y-auto h-auto md:h-full md:max-h-[560px]">
|