@pixerate/editor-svelte 0.109.1 → 0.110.1

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.
@@ -2,6 +2,7 @@
2
2
  import { imageChipClass, imageColorInputClass } from './styles.js';
3
3
  import type { ImageEditorController } from '@pixerate/editor/image-editor';
4
4
  import {
5
+ userErrorMessage,
5
6
  BRUSH_OPTION_CONTROLS,
6
7
  MIXER_MIXING_OPTIONS,
7
8
  MIXER_OPTION_CONTROLS,
@@ -49,7 +50,7 @@
49
50
  error = null;
50
51
  fn();
51
52
  } catch (err) {
52
- error = err instanceof Error ? err.message : String(err);
53
+ error = userErrorMessage(err);
53
54
  }
54
55
  }
55
56
  </script>
@@ -5,6 +5,7 @@
5
5
  canvasToImagePoint,
6
6
  imageToCanvasPoint,
7
7
  createImagePlaybackLocator,
8
+ agentDragDraft,
8
9
  hitTestCrop,
9
10
  getCropCursor,
10
11
  calculateCropDrag,
@@ -16,6 +17,8 @@
16
17
  pointerPressure,
17
18
  FreeTransformTool,
18
19
  getViewportProjection,
20
+ measureCanvasViewport,
21
+ watchCanvasViewport,
19
22
  ShapeDrawGesture,
20
23
  TypeToolGesture,
21
24
  DEFAULT_SHAPE_TOOL_OPTIONS,
@@ -26,6 +29,7 @@
26
29
  type TypeToolOptions,
27
30
  } from '@pixerate/editor/image-editor';
28
31
  import { untrack } from 'svelte';
32
+ import { trackPresenceDrag } from '@pixerate/editor/tools';
29
33
  import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
30
34
  import PresenceLayer from '../playback/PresenceLayer.svelte';
31
35
  import type { ReactiveImageEditor } from './imageEditorState.svelte.js';
@@ -115,6 +119,11 @@
115
119
  let canvasEl = $state<HTMLCanvasElement | null>(null);
116
120
  let containerEl = $state<HTMLDivElement | null>(null);
117
121
  let rootEl = $state<HTMLDivElement | null>(null);
122
+ /**
123
+ * The viewport in CSS pixels as last rendered. The backing store is larger (device pixel ratio,
124
+ * CSS transforms), so pointer mapping and the projection use this, never `canvasEl.width`.
125
+ */
126
+ let view = { width: 0, height: 0 };
118
127
  const report = (source: ImageEditorCanvasErrorSource, message: string | null) => onerror?.(source, message);
119
128
 
120
129
  // Agent playback: the agent's cursor in document pixels, drawn over the canvas without clipping so
@@ -123,19 +132,19 @@
123
132
  const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
124
133
  /** Document pixels → pixels in the canvas root, through the canvas projection. */
125
134
  const docToRoot = (p: PlaybackPoint): PlaybackPoint => {
126
- if (!canvasEl || !rootEl || !canvasEl.width) return p;
135
+ if (!canvasEl || !rootEl || !view.width) return p;
127
136
  const r = canvasEl.getBoundingClientRect();
128
137
  const box = rootEl.getBoundingClientRect();
129
- const c = imageToCanvasPoint(p, editor.controller.getState(), canvasEl.width, canvasEl.height, mode === 'crop');
130
- return { x: r.left - box.left + (c.x * r.width) / canvasEl.width, y: r.top - box.top + (c.y * r.height) / canvasEl.height };
138
+ const c = imageToCanvasPoint(p, editor.controller.getState(), view.width, view.height, mode === 'crop');
139
+ return { x: r.left - box.left + (c.x * r.width) / view.width, y: r.top - box.top + (c.y * r.height) / view.height };
131
140
  };
132
141
  /** Viewport (client) pixels → document pixels. */
133
142
  const clientToDoc = (p: PlaybackPoint): PlaybackPoint => {
134
- if (!canvasEl || !canvasEl.width) return p;
143
+ if (!canvasEl || !view.width) return p;
135
144
  const r = canvasEl.getBoundingClientRect();
136
- const sx = r.width > 0 ? canvasEl.width / r.width : 1;
137
- const sy = r.height > 0 ? canvasEl.height / r.height : 1;
138
- return canvasToImagePoint({ x: (p.x - r.left) * sx, y: (p.y - r.top) * sy }, editor.controller.getState(), canvasEl.width, canvasEl.height, mode === 'crop');
145
+ const sx = r.width > 0 ? view.width / r.width : 1;
146
+ const sy = r.height > 0 ? view.height / r.height : 1;
147
+ return canvasToImagePoint({ x: (p.x - r.left) * sx, y: (p.y - r.top) * sy }, editor.controller.getState(), view.width, view.height, mode === 'crop');
139
148
  };
140
149
  /** A client box as a rectangle in document pixels. */
141
150
  const clientRect = (r: DOMRect | null | undefined): PlaybackRect | null => {
@@ -272,12 +281,12 @@
272
281
  /** The document point of a pointer event and the hit tolerance (6 screen px). */
273
282
  function transformPoint(e: PointerEvent) {
274
283
  const rect = canvasEl!.getBoundingClientRect();
275
- const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
276
- const sy = rect.height > 0 ? canvasEl!.height / rect.height : 1;
284
+ const sx = rect.width > 0 ? view.width / rect.width : 1;
285
+ const sy = rect.height > 0 ? view.height / rect.height : 1;
277
286
  const s = editor.controller.getState();
278
287
  const cropping = mode === 'crop';
279
- const projection = getViewportProjection(s, canvasEl!.width, canvasEl!.height, cropping);
280
- const pt = canvasToImagePoint({ x: (e.clientX - rect.left) * sx, y: (e.clientY - rect.top) * sy }, s, canvasEl!.width, canvasEl!.height, cropping);
288
+ const projection = getViewportProjection(s, view.width, view.height, cropping);
289
+ const pt = canvasToImagePoint({ x: (e.clientX - rect.left) * sx, y: (e.clientY - rect.top) * sy }, s, view.width, view.height, cropping);
281
290
  transformTool.setView(projection.scale);
282
291
  return { pt, tolerance: 6 / (projection.scale || 1) };
283
292
  }
@@ -325,14 +334,44 @@
325
334
  let currentPath: ImagePoint[] = [];
326
335
  let dragStartPoint: ImagePoint | null = null;
327
336
 
337
+ // The agent's drags pull out the crop box or the marquee as it goes, like a user's drag; the call
338
+ // that follows sets the real crop or selection.
339
+ let agentDrafting = false;
340
+ $effect(() => {
341
+ const presence = agentPresence;
342
+ return untrack(() =>
343
+ presence.subscribe(
344
+ trackPresenceDrag({
345
+ onDrag: (box) => {
346
+ if (cropDragInfo || selectionGesture.active) return;
347
+ const draft = agentDragDraft(mode, box, editor.controller.getState().imageDimensions);
348
+ if (!draft) return;
349
+ agentDrafting = true;
350
+ if ('crop' in draft) draftCrop = draft.crop;
351
+ else selectionDraft = draft.selectionPath;
352
+ void renderCanvas();
353
+ },
354
+ onEnd: () => {
355
+ if (!agentDrafting) return;
356
+ agentDrafting = false;
357
+ draftCrop = null;
358
+ selectionDraft = null;
359
+ void renderCanvas();
360
+ },
361
+ }),
362
+ ),
363
+ );
364
+ });
365
+
328
366
  async function renderCanvas(overrideAnnotation?: Annotation | null, overrideCrop?: CropBox | null) {
329
367
  if (!canvasEl || !containerEl) return;
330
- const vpWidth = containerEl.clientWidth || 800;
331
- const vpHeight = containerEl.clientHeight || 500;
368
+ const viewport = measureCanvasViewport(containerEl);
369
+ view = { width: viewport.width, height: viewport.height };
332
370
 
333
371
  await editor.controller.render(canvasEl, {
334
- viewportWidth: vpWidth,
335
- viewportHeight: vpHeight,
372
+ viewportWidth: viewport.width,
373
+ viewportHeight: viewport.height,
374
+ pixelRatio: viewport.pixelRatio,
336
375
  fit,
337
376
  showCropOverlay: mode === 'crop',
338
377
  showSelectionOverlay: true,
@@ -354,11 +393,11 @@
354
393
  const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];
355
394
  const list = events.length ? events : [e];
356
395
  const rect = canvasEl!.getBoundingClientRect();
357
- const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
358
- const sy = rect.height > 0 ? canvasEl!.height / rect.height : 1;
396
+ const sx = rect.width > 0 ? view.width / rect.width : 1;
397
+ const sy = rect.height > 0 ? view.height / rect.height : 1;
359
398
  const s = editor.controller.getState();
360
399
  return list.map((ev) => ({
361
- ...canvasToImagePoint({ x: (ev.clientX - rect.left) * sx, y: (ev.clientY - rect.top) * sy }, s, canvasEl!.width, canvasEl!.height, false),
400
+ ...canvasToImagePoint({ x: (ev.clientX - rect.left) * sx, y: (ev.clientY - rect.top) * sy }, s, view.width, view.height, false),
362
401
  pressure: pointerPressure(ev),
363
402
  }));
364
403
  }
@@ -419,24 +458,17 @@
419
458
 
420
459
  $effect(() => {
421
460
  if (!containerEl) return;
422
- if (typeof ResizeObserver !== 'undefined') {
423
- const observer = new ResizeObserver(() => {
424
- renderCanvas();
425
- });
426
- observer.observe(containerEl);
427
- return () => observer.disconnect();
428
- } else {
429
- const handleResize = () => renderCanvas();
430
- window.addEventListener('resize', handleResize);
431
- return () => window.removeEventListener('resize', handleResize);
432
- }
461
+ const { width, height } = measureCanvasViewport(containerEl);
462
+ view = { width, height };
463
+ // Re-render when the layout size, the device pixel ratio or a host's CSS transform scale changes.
464
+ return watchCanvasViewport(containerEl, () => renderCanvas());
433
465
  });
434
466
 
435
467
  function getCanvasEventPoint(e: MouseEvent): ImagePoint {
436
468
  if (!canvasEl) return { x: 0, y: 0 };
437
469
  const rect = canvasEl.getBoundingClientRect();
438
- const scaleX = rect.width > 0 ? canvasEl.width / rect.width : 1;
439
- const scaleY = rect.height > 0 ? canvasEl.height / rect.height : 1;
470
+ const scaleX = rect.width > 0 ? view.width / rect.width : 1;
471
+ const scaleY = rect.height > 0 ? view.height / rect.height : 1;
440
472
  return {
441
473
  x: (e.clientX - rect.left) * scaleX,
442
474
  y: (e.clientY - rect.top) * scaleY,
@@ -449,7 +481,7 @@
449
481
 
450
482
  if (mode === 'select') {
451
483
  const gesture = selectionGesture;
452
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
484
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
453
485
  void gesture.begin(imgPt, e).then(() => report('select', gesture.error));
454
486
  return;
455
487
  }
@@ -461,8 +493,8 @@
461
493
  width: editor.state.imageDimensions.width,
462
494
  height: editor.state.imageDimensions.height,
463
495
  };
464
- const handle = hitTestCrop(canvasPt, editor.state, canvasEl.width, canvasEl.height, 14, activeCrop);
465
- const startImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, true);
496
+ const handle = hitTestCrop(canvasPt, editor.state, view.width, view.height, 14, activeCrop);
497
+ const startImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, true);
466
498
 
467
499
  if (handle) {
468
500
  cropDragInfo = {
@@ -484,7 +516,7 @@
484
516
  }
485
517
 
486
518
  if (mode === 'annotate' && (editor.state.activeTool === 'shape' || editor.state.activeTool === 'type')) {
487
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
519
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
488
520
  report(editor.state.activeTool, null);
489
521
  if (editor.state.activeTool === 'shape') shapeGesture.down(imgPt);
490
522
  else {
@@ -496,7 +528,7 @@
496
528
  }
497
529
 
498
530
  if (mode === 'annotate') {
499
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
531
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
500
532
  dragStartPoint = imgPt;
501
533
 
502
534
  if (editor.state.activeTool === 'select') {
@@ -540,7 +572,7 @@
540
572
 
541
573
  if (mode === 'select') {
542
574
  const gesture = selectionGesture;
543
- if (gesture.active && gesture.move(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false))) {
575
+ if (gesture.active && gesture.move(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false))) {
544
576
  selectionDraft = gesture.draftPath;
545
577
  renderCanvas();
546
578
  }
@@ -549,7 +581,7 @@
549
581
 
550
582
  if (mode === 'crop') {
551
583
  if (cropDragInfo) {
552
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, true);
584
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, true);
553
585
  let newCrop: CropBox;
554
586
 
555
587
  if (cropDragInfo.handle === 'new') {
@@ -585,7 +617,7 @@
585
617
  width: editor.state.imageDimensions.width,
586
618
  height: editor.state.imageDimensions.height,
587
619
  };
588
- const handle = hitTestCrop(canvasPt, editor.state, canvasEl.width, canvasEl.height, 14, activeCrop);
620
+ const handle = hitTestCrop(canvasPt, editor.state, view.width, view.height, 14, activeCrop);
589
621
  canvasEl.style.cursor = getCropCursor(handle, editor.state.transform);
590
622
  }
591
623
  return;
@@ -593,7 +625,7 @@
593
625
 
594
626
  if (mode === 'annotate' && editor.state.activeTool === 'shape') {
595
627
  if (shapeGesture.active) {
596
- shapeGesture.move(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false), { shift: e.shiftKey, alt: e.altKey });
628
+ shapeGesture.move(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false), { shift: e.shiftKey, alt: e.altKey });
597
629
  if (shapeGesture.error) report('shape', shapeGesture.error);
598
630
  }
599
631
  return;
@@ -601,7 +633,7 @@
601
633
 
602
634
  if (mode === 'annotate') {
603
635
  if (annotationDragInfo) {
604
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
636
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
605
637
  const dx = currentImgPt.x - annotationDragInfo.startImgPt.x;
606
638
  const dy = currentImgPt.y - annotationDragInfo.startImgPt.y;
607
639
  editor.updateAnnotation(annotationDragInfo.annId, {
@@ -613,7 +645,7 @@
613
645
 
614
646
  if (!dragStartPoint) {
615
647
  if (editor.state.activeTool === 'select') {
616
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
648
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
617
649
  let hovered = false;
618
650
  for (let i = editor.state.annotations.length - 1; i >= 0; i--) {
619
651
  if (hitTestAnnotation(imgPt, editor.state.annotations[i])) {
@@ -628,7 +660,7 @@
628
660
  return;
629
661
  }
630
662
 
631
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
663
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
632
664
  const stroke = { strokeColor: annotationColor, strokeWidth: annotationWidth };
633
665
 
634
666
  if (editor.state.activeTool === 'pen' && isDrawing) {
@@ -687,7 +719,7 @@
687
719
  if (mode === 'select') {
688
720
  const gesture = selectionGesture;
689
721
  if (gesture.active) {
690
- gesture.end(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false), e);
722
+ gesture.end(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false), e);
691
723
  selectionDraft = null;
692
724
  renderCanvas();
693
725
  }
@@ -711,7 +743,7 @@
711
743
  return;
712
744
  }
713
745
  if (mode === 'annotate' && editor.state.activeTool === 'type') {
714
- typeTool.up(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false));
746
+ typeTool.up(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false));
715
747
  typeSession = typeTool.session;
716
748
  if (typeTool.error) report('type', typeTool.error);
717
749
  void renderCanvas();
@@ -727,7 +759,7 @@
727
759
  }
728
760
 
729
761
  if (dragStartPoint) {
730
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
762
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
731
763
  const stroke = { strokeColor: annotationColor, strokeWidth: annotationWidth };
732
764
 
733
765
  if (editor.state.activeTool === 'pen' && isDrawing) {
@@ -802,7 +834,7 @@
802
834
  onpointerup={handlePaintPointerUp}
803
835
  onpointercancel={() => paintGesture.cancel()}
804
836
  data-testid="image-editor-canvas"
805
- style={transformActive ? `cursor: ${transformCursor}` : undefined}
837
+ style="width: 100%; height: 100%;{transformActive ? ` cursor: ${transformCursor};` : ''}"
806
838
  class="absolute inset-0 w-full h-full block {transformActive ? 'touch-none' : mode === 'paint' ? 'touch-none cursor-crosshair' : mode === 'annotate' && editor.state.activeTool === 'type' ? 'cursor-text' : mode === 'annotate' && editor.state.activeTool === 'shape' ? 'cursor-crosshair' : ''}"
807
839
  ></canvas>
808
840
  {#if typeSession}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.109.1",
3
+ "version": "0.110.1",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -160,7 +160,7 @@
160
160
  "@tiptap/extension-bubble-menu": "^2.27.3",
161
161
  "@tiptap/extension-placeholder": "^2.27.3",
162
162
  "@tiptap/starter-kit": "^2.27.3",
163
- "@pixerate/editor": "0.149.0"
163
+ "@pixerate/editor": "0.150.1"
164
164
  },
165
165
  "peerDependencies": {
166
166
  "@dagrejs/dagre": "^1.1.4",
@@ -179,14 +179,14 @@
179
179
  },
180
180
  "devDependencies": {
181
181
  "@dagrejs/dagre": "^1.1.4",
182
- "@sveltejs/package": "^3.0.0",
182
+ "@sveltejs/package": "^3.0.1",
183
183
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
184
184
  "@tiptap/core": "^2.27.3",
185
185
  "@tiptap/pm": "^2.27.3",
186
186
  "@types/dagre": "^0.7.52",
187
187
  "@xyflow/svelte": "^1.7.0",
188
188
  "svelte": "^5.57.2",
189
- "svelte-check": "^4.7.6",
189
+ "svelte-check": "^4.7.7",
190
190
  "typescript": "^5.7.3",
191
191
  "vitest": "^5.0.5"
192
192
  },