@pixerate/editor-svelte 0.110.0 → 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>
@@ -17,6 +17,8 @@
17
17
  pointerPressure,
18
18
  FreeTransformTool,
19
19
  getViewportProjection,
20
+ measureCanvasViewport,
21
+ watchCanvasViewport,
20
22
  ShapeDrawGesture,
21
23
  TypeToolGesture,
22
24
  DEFAULT_SHAPE_TOOL_OPTIONS,
@@ -117,6 +119,11 @@
117
119
  let canvasEl = $state<HTMLCanvasElement | null>(null);
118
120
  let containerEl = $state<HTMLDivElement | null>(null);
119
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 };
120
127
  const report = (source: ImageEditorCanvasErrorSource, message: string | null) => onerror?.(source, message);
121
128
 
122
129
  // Agent playback: the agent's cursor in document pixels, drawn over the canvas without clipping so
@@ -125,19 +132,19 @@
125
132
  const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
126
133
  /** Document pixels → pixels in the canvas root, through the canvas projection. */
127
134
  const docToRoot = (p: PlaybackPoint): PlaybackPoint => {
128
- if (!canvasEl || !rootEl || !canvasEl.width) return p;
135
+ if (!canvasEl || !rootEl || !view.width) return p;
129
136
  const r = canvasEl.getBoundingClientRect();
130
137
  const box = rootEl.getBoundingClientRect();
131
- const c = imageToCanvasPoint(p, editor.controller.getState(), canvasEl.width, canvasEl.height, mode === '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 };
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 };
133
140
  };
134
141
  /** Viewport (client) pixels → document pixels. */
135
142
  const clientToDoc = (p: PlaybackPoint): PlaybackPoint => {
136
- if (!canvasEl || !canvasEl.width) return p;
143
+ if (!canvasEl || !view.width) return p;
137
144
  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, 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');
141
148
  };
142
149
  /** A client box as a rectangle in document pixels. */
143
150
  const clientRect = (r: DOMRect | null | undefined): PlaybackRect | null => {
@@ -274,12 +281,12 @@
274
281
  /** The document point of a pointer event and the hit tolerance (6 screen px). */
275
282
  function transformPoint(e: PointerEvent) {
276
283
  const rect = canvasEl!.getBoundingClientRect();
277
- const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
278
- 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;
279
286
  const s = editor.controller.getState();
280
287
  const cropping = mode === 'crop';
281
- const projection = getViewportProjection(s, canvasEl!.width, canvasEl!.height, cropping);
282
- 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);
283
290
  transformTool.setView(projection.scale);
284
291
  return { pt, tolerance: 6 / (projection.scale || 1) };
285
292
  }
@@ -358,12 +365,13 @@
358
365
 
359
366
  async function renderCanvas(overrideAnnotation?: Annotation | null, overrideCrop?: CropBox | null) {
360
367
  if (!canvasEl || !containerEl) return;
361
- const vpWidth = containerEl.clientWidth || 800;
362
- const vpHeight = containerEl.clientHeight || 500;
368
+ const viewport = measureCanvasViewport(containerEl);
369
+ view = { width: viewport.width, height: viewport.height };
363
370
 
364
371
  await editor.controller.render(canvasEl, {
365
- viewportWidth: vpWidth,
366
- viewportHeight: vpHeight,
372
+ viewportWidth: viewport.width,
373
+ viewportHeight: viewport.height,
374
+ pixelRatio: viewport.pixelRatio,
367
375
  fit,
368
376
  showCropOverlay: mode === 'crop',
369
377
  showSelectionOverlay: true,
@@ -385,11 +393,11 @@
385
393
  const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];
386
394
  const list = events.length ? events : [e];
387
395
  const rect = canvasEl!.getBoundingClientRect();
388
- const sx = rect.width > 0 ? canvasEl!.width / rect.width : 1;
389
- 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;
390
398
  const s = editor.controller.getState();
391
399
  return list.map((ev) => ({
392
- ...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),
393
401
  pressure: pointerPressure(ev),
394
402
  }));
395
403
  }
@@ -450,24 +458,17 @@
450
458
 
451
459
  $effect(() => {
452
460
  if (!containerEl) return;
453
- if (typeof ResizeObserver !== 'undefined') {
454
- const observer = new ResizeObserver(() => {
455
- renderCanvas();
456
- });
457
- observer.observe(containerEl);
458
- return () => observer.disconnect();
459
- } else {
460
- const handleResize = () => renderCanvas();
461
- window.addEventListener('resize', handleResize);
462
- return () => window.removeEventListener('resize', handleResize);
463
- }
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());
464
465
  });
465
466
 
466
467
  function getCanvasEventPoint(e: MouseEvent): ImagePoint {
467
468
  if (!canvasEl) return { x: 0, y: 0 };
468
469
  const rect = canvasEl.getBoundingClientRect();
469
- const scaleX = rect.width > 0 ? canvasEl.width / rect.width : 1;
470
- 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;
471
472
  return {
472
473
  x: (e.clientX - rect.left) * scaleX,
473
474
  y: (e.clientY - rect.top) * scaleY,
@@ -480,7 +481,7 @@
480
481
 
481
482
  if (mode === 'select') {
482
483
  const gesture = selectionGesture;
483
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
484
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
484
485
  void gesture.begin(imgPt, e).then(() => report('select', gesture.error));
485
486
  return;
486
487
  }
@@ -492,8 +493,8 @@
492
493
  width: editor.state.imageDimensions.width,
493
494
  height: editor.state.imageDimensions.height,
494
495
  };
495
- const handle = hitTestCrop(canvasPt, editor.state, canvasEl.width, canvasEl.height, 14, activeCrop);
496
- 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);
497
498
 
498
499
  if (handle) {
499
500
  cropDragInfo = {
@@ -515,7 +516,7 @@
515
516
  }
516
517
 
517
518
  if (mode === 'annotate' && (editor.state.activeTool === 'shape' || editor.state.activeTool === 'type')) {
518
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
519
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
519
520
  report(editor.state.activeTool, null);
520
521
  if (editor.state.activeTool === 'shape') shapeGesture.down(imgPt);
521
522
  else {
@@ -527,7 +528,7 @@
527
528
  }
528
529
 
529
530
  if (mode === 'annotate') {
530
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
531
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
531
532
  dragStartPoint = imgPt;
532
533
 
533
534
  if (editor.state.activeTool === 'select') {
@@ -571,7 +572,7 @@
571
572
 
572
573
  if (mode === 'select') {
573
574
  const gesture = selectionGesture;
574
- 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))) {
575
576
  selectionDraft = gesture.draftPath;
576
577
  renderCanvas();
577
578
  }
@@ -580,7 +581,7 @@
580
581
 
581
582
  if (mode === 'crop') {
582
583
  if (cropDragInfo) {
583
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, true);
584
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, true);
584
585
  let newCrop: CropBox;
585
586
 
586
587
  if (cropDragInfo.handle === 'new') {
@@ -616,7 +617,7 @@
616
617
  width: editor.state.imageDimensions.width,
617
618
  height: editor.state.imageDimensions.height,
618
619
  };
619
- 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);
620
621
  canvasEl.style.cursor = getCropCursor(handle, editor.state.transform);
621
622
  }
622
623
  return;
@@ -624,7 +625,7 @@
624
625
 
625
626
  if (mode === 'annotate' && editor.state.activeTool === 'shape') {
626
627
  if (shapeGesture.active) {
627
- 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 });
628
629
  if (shapeGesture.error) report('shape', shapeGesture.error);
629
630
  }
630
631
  return;
@@ -632,7 +633,7 @@
632
633
 
633
634
  if (mode === 'annotate') {
634
635
  if (annotationDragInfo) {
635
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
636
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
636
637
  const dx = currentImgPt.x - annotationDragInfo.startImgPt.x;
637
638
  const dy = currentImgPt.y - annotationDragInfo.startImgPt.y;
638
639
  editor.updateAnnotation(annotationDragInfo.annId, {
@@ -644,7 +645,7 @@
644
645
 
645
646
  if (!dragStartPoint) {
646
647
  if (editor.state.activeTool === 'select') {
647
- const imgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
648
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
648
649
  let hovered = false;
649
650
  for (let i = editor.state.annotations.length - 1; i >= 0; i--) {
650
651
  if (hitTestAnnotation(imgPt, editor.state.annotations[i])) {
@@ -659,7 +660,7 @@
659
660
  return;
660
661
  }
661
662
 
662
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
663
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
663
664
  const stroke = { strokeColor: annotationColor, strokeWidth: annotationWidth };
664
665
 
665
666
  if (editor.state.activeTool === 'pen' && isDrawing) {
@@ -718,7 +719,7 @@
718
719
  if (mode === 'select') {
719
720
  const gesture = selectionGesture;
720
721
  if (gesture.active) {
721
- 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);
722
723
  selectionDraft = null;
723
724
  renderCanvas();
724
725
  }
@@ -742,7 +743,7 @@
742
743
  return;
743
744
  }
744
745
  if (mode === 'annotate' && editor.state.activeTool === 'type') {
745
- typeTool.up(canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false));
746
+ typeTool.up(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false));
746
747
  typeSession = typeTool.session;
747
748
  if (typeTool.error) report('type', typeTool.error);
748
749
  void renderCanvas();
@@ -758,7 +759,7 @@
758
759
  }
759
760
 
760
761
  if (dragStartPoint) {
761
- const currentImgPt = canvasToImagePoint(canvasPt, editor.state, canvasEl.width, canvasEl.height, false);
762
+ const currentImgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
762
763
  const stroke = { strokeColor: annotationColor, strokeWidth: annotationWidth };
763
764
 
764
765
  if (editor.state.activeTool === 'pen' && isDrawing) {
@@ -833,7 +834,7 @@
833
834
  onpointerup={handlePaintPointerUp}
834
835
  onpointercancel={() => paintGesture.cancel()}
835
836
  data-testid="image-editor-canvas"
836
- style={transformActive ? `cursor: ${transformCursor}` : undefined}
837
+ style="width: 100%; height: 100%;{transformActive ? ` cursor: ${transformCursor};` : ''}"
837
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' : ''}"
838
839
  ></canvas>
839
840
  {#if typeSession}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.110.0",
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.150.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
  },