canvu-react 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -711,6 +711,47 @@ var init_shape_builders = __esm({
711
711
  }
712
712
  });
713
713
 
714
+ // src/image/canvas-encode.ts
715
+ var DEFAULT_FALLBACK_MIME_TYPES = ["image/png"];
716
+ var tryCanvasToBlob = (canvas, mimeType, quality) => new Promise((resolve) => {
717
+ canvas.toBlob((blob) => resolve(blob), mimeType, quality);
718
+ });
719
+ var blobFromDataUrl = async (dataUrl) => {
720
+ const response = await fetch(dataUrl);
721
+ const blob = await response.blob();
722
+ if (blob.size === 0) {
723
+ throw new Error("Failed to encode canvas to blob");
724
+ }
725
+ return blob;
726
+ };
727
+ async function encodeCanvasToBlob(canvas, options) {
728
+ const primaryMimeType = options?.mimeType ?? "image/png";
729
+ const quality = options?.quality;
730
+ const mimeTypes = [
731
+ primaryMimeType,
732
+ ...options?.fallbackMimeTypes ?? DEFAULT_FALLBACK_MIME_TYPES
733
+ ].filter(
734
+ (mimeType, index, mimeTypeList) => mimeTypeList.indexOf(mimeType) === index
735
+ );
736
+ for (const mimeType of mimeTypes) {
737
+ const blob = await tryCanvasToBlob(
738
+ canvas,
739
+ mimeType,
740
+ mimeType === primaryMimeType ? quality : void 0
741
+ );
742
+ if (blob) {
743
+ return blob;
744
+ }
745
+ }
746
+ for (const mimeType of mimeTypes) {
747
+ const dataUrl = canvas.toDataURL(mimeType, quality);
748
+ if (dataUrl && dataUrl !== "data:,") {
749
+ return blobFromDataUrl(dataUrl);
750
+ }
751
+ }
752
+ throw new Error("Failed to encode canvas to blob");
753
+ }
754
+
714
755
  // src/image/indexed-db-image-store.ts
715
756
  var DB_NAME = "canvu-image-store";
716
757
  var DB_VERSION = 1;
@@ -829,19 +870,7 @@ function decodeImageToCanvas(blob, maxDimension) {
829
870
  });
830
871
  }
831
872
  function canvasToBlob(canvas, mime, quality) {
832
- return new Promise((resolve, reject) => {
833
- canvas.toBlob(
834
- (blob) => {
835
- if (!blob) {
836
- reject(new Error("Could not encode blob"));
837
- return;
838
- }
839
- resolve(blob);
840
- },
841
- mime,
842
- quality
843
- );
844
- });
873
+ return encodeCanvasToBlob(canvas, { mimeType: mime, quality });
845
874
  }
846
875
  async function loadImageToStore(file, store) {
847
876
  const originalBlob = file;
@@ -909,21 +938,6 @@ async function renderPageToCanvas(page, scale) {
909
938
  await page.render({ canvas, viewport }).promise;
910
939
  return { canvas, width: w, height: h };
911
940
  }
912
- function canvasToBlob2(canvas, mime, quality) {
913
- return new Promise((resolve, reject) => {
914
- canvas.toBlob(
915
- (blob) => {
916
- if (!blob) {
917
- reject(new Error("Could not encode blob"));
918
- return;
919
- }
920
- resolve(blob);
921
- },
922
- mime,
923
- quality
924
- );
925
- });
926
- }
927
941
  function normalizePdfPageNumbers(pageNumbers, pageCount) {
928
942
  if (!pageNumbers || pageNumbers.length === 0) {
929
943
  return Array.from({ length: pageCount }, (_, index) => index + 1);
@@ -985,7 +999,7 @@ async function loadPdfToStore(file, store, options) {
985
999
  const page = await pdf.getPage(pageNumber);
986
1000
  const { canvas, width, height } = await renderPageToCanvas(page, scale);
987
1001
  const mime = "image/png";
988
- const pageBlob = await canvasToBlob2(canvas, mime);
1002
+ const pageBlob = await encodeCanvasToBlob(canvas, { mimeType: mime });
989
1003
  const blobId = await store.storeOriginal(pageBlob);
990
1004
  const thumbnailBlobId = storeThumbnails ? await (async () => {
991
1005
  const thumbScale = Math.min(1, 256 / Math.max(width, height));
@@ -1000,7 +1014,9 @@ async function loadPdfToStore(file, store, options) {
1000
1014
  tCtx.imageSmoothingQuality = "high";
1001
1015
  tCtx.drawImage(canvas, 0, 0, tw, th);
1002
1016
  }
1003
- const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
1017
+ const thumbBlob = await encodeCanvasToBlob(thumbCanvas, {
1018
+ mimeType: mime
1019
+ });
1004
1020
  return await store.storeThumbnail(thumbBlob);
1005
1021
  })() : "";
1006
1022
  const pageResult = {
@@ -1604,6 +1620,21 @@ function reorderManagedImages(items, orderedManagedIds) {
1604
1620
  });
1605
1621
  return restackManagedImages(next);
1606
1622
  }
1623
+ var OPEN_KEYFRAME_ID = "canvu-images-menu-open-keyframe";
1624
+ var OPEN_KEYFRAME_CSS = `
1625
+ @keyframes canvu-images-menu-open {
1626
+ from { opacity: 0; transform: scale(0.6); }
1627
+ to { opacity: 1; transform: scale(1); }
1628
+ }
1629
+ `;
1630
+ function ensureOpenKeyframe() {
1631
+ if (typeof document === "undefined") return;
1632
+ if (document.getElementById(OPEN_KEYFRAME_ID)) return;
1633
+ const style = document.createElement("style");
1634
+ style.id = OPEN_KEYFRAME_ID;
1635
+ style.textContent = OPEN_KEYFRAME_CSS;
1636
+ document.head.appendChild(style);
1637
+ }
1607
1638
  var panelStyle = {
1608
1639
  width: "fit-content",
1609
1640
  maxHeight: "min(85dvh, 820px)",
@@ -1614,7 +1645,9 @@ var panelStyle = {
1614
1645
  boxShadow: "0 10px 40px rgba(15, 23, 42, 0.12)",
1615
1646
  fontFamily: "system-ui, sans-serif",
1616
1647
  fontSize: 14,
1617
- color: "#0f172a"
1648
+ color: "#0f172a",
1649
+ transformOrigin: "top right",
1650
+ animation: "canvu-images-menu-open 180ms cubic-bezier(0.2, 0.8, 0.2, 1)"
1618
1651
  };
1619
1652
  var headerStyle = {
1620
1653
  display: "flex",
@@ -1736,33 +1769,16 @@ var collapsedButtonStyle = {
1736
1769
  display: "inline-flex",
1737
1770
  alignItems: "center",
1738
1771
  justifyContent: "center",
1739
- gap: 6,
1772
+ width: 44,
1740
1773
  height: 44,
1741
- minWidth: 44,
1742
- padding: "0 12px",
1774
+ padding: 0,
1743
1775
  border: "1px solid #e2e8f0",
1744
- borderRadius: 22,
1776
+ borderRadius: 10,
1745
1777
  background: "#ffffff",
1746
1778
  color: "#0f172a",
1747
1779
  cursor: "pointer",
1748
- fontFamily: "system-ui, sans-serif",
1749
- fontSize: 13,
1750
- fontWeight: 600,
1751
1780
  boxShadow: "0 8px 24px rgba(15, 23, 42, 0.12)"
1752
1781
  };
1753
- var collapsedCountStyle = {
1754
- display: "inline-flex",
1755
- alignItems: "center",
1756
- justifyContent: "center",
1757
- minWidth: 20,
1758
- height: 20,
1759
- padding: "0 6px",
1760
- borderRadius: 10,
1761
- backgroundColor: "#0f172a",
1762
- color: "#ffffff",
1763
- fontSize: 11,
1764
- fontWeight: 600
1765
- };
1766
1782
  var defaultLabels = {
1767
1783
  title: "Images",
1768
1784
  dragHandle: "Drag to reorder",
@@ -1786,12 +1802,15 @@ function ImagesMenu({
1786
1802
  })
1787
1803
  );
1788
1804
  const [collapsed, setCollapsed] = useState(false);
1805
+ useEffect(() => {
1806
+ ensureOpenKeyframe();
1807
+ }, []);
1789
1808
  if (managed.length === 0) {
1790
1809
  return null;
1791
1810
  }
1792
1811
  const resolvedLabels = { ...defaultLabels, ...labels };
1793
1812
  if (collapsed) {
1794
- return /* @__PURE__ */ jsxs(
1813
+ return /* @__PURE__ */ jsx(
1795
1814
  "button",
1796
1815
  {
1797
1816
  type: "button",
@@ -1800,10 +1819,7 @@ function ImagesMenu({
1800
1819
  "aria-label": resolvedLabels.expand,
1801
1820
  title: resolvedLabels.expand,
1802
1821
  onClick: () => setCollapsed(false),
1803
- children: [
1804
- /* @__PURE__ */ jsx(Images, { size: 20 }),
1805
- /* @__PURE__ */ jsx("span", { style: collapsedCountStyle, children: managed.length })
1806
- ]
1822
+ children: /* @__PURE__ */ jsx(Images, { size: 20 })
1807
1823
  }
1808
1824
  );
1809
1825
  }
@@ -1872,11 +1888,20 @@ function ImagesMenuRow({
1872
1888
  onRotate,
1873
1889
  onDelete
1874
1890
  }) {
1875
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id });
1891
+ const {
1892
+ attributes,
1893
+ listeners,
1894
+ setNodeRef,
1895
+ setActivatorNodeRef,
1896
+ transform,
1897
+ transition,
1898
+ isDragging
1899
+ } = useSortable({ id: item.id });
1900
+ const feedbackTransition = "background-color 140ms ease, opacity 140ms ease";
1876
1901
  const wrapperStyle = {
1877
1902
  ...rowStyle,
1878
1903
  transform: CSS.Transform.toString(transform),
1879
- transition,
1904
+ transition: transition ? `${transition}, ${feedbackTransition}` : feedbackTransition,
1880
1905
  background: isDragging ? "#eef2f7" : "transparent",
1881
1906
  opacity: isDragging ? 0.85 : 1
1882
1907
  };
@@ -1885,6 +1910,7 @@ function ImagesMenuRow({
1885
1910
  /* @__PURE__ */ jsx(
1886
1911
  "button",
1887
1912
  {
1913
+ ref: setActivatorNodeRef,
1888
1914
  type: "button",
1889
1915
  style: handleStyle,
1890
1916
  "aria-label": labels.dragHandle,
@@ -4779,16 +4805,23 @@ function distanceBetween(a, b) {
4779
4805
  function midpoint(a, b) {
4780
4806
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
4781
4807
  }
4782
- function wheelDeltaYPixels(e) {
4783
- switch (e.deltaMode) {
4808
+ function wheelDeltaPixels(delta, deltaMode) {
4809
+ switch (deltaMode) {
4784
4810
  case WheelEvent.DOM_DELTA_LINE:
4785
- return e.deltaY * 16;
4811
+ return delta * 16;
4786
4812
  case WheelEvent.DOM_DELTA_PAGE:
4787
- return e.deltaY * 400;
4813
+ return delta * 400;
4788
4814
  default:
4789
- return e.deltaY;
4815
+ return delta;
4790
4816
  }
4791
4817
  }
4818
+ var TRACKPAD_MAX_DELTA = 20;
4819
+ var MOUSE_WHEEL_DAMPING = 0.4;
4820
+ function softenWheelDelta(d) {
4821
+ const a = Math.abs(d);
4822
+ if (a <= TRACKPAD_MAX_DELTA) return d;
4823
+ return Math.sign(d) * (TRACKPAD_MAX_DELTA + (a - TRACKPAD_MAX_DELTA) * MOUSE_WHEEL_DAMPING);
4824
+ }
4792
4825
  function attachViewportInput(options) {
4793
4826
  const {
4794
4827
  element,
@@ -4811,8 +4844,8 @@ function attachViewportInput(options) {
4811
4844
  const onWheel = (e) => {
4812
4845
  if (e.ctrlKey || e.metaKey) {
4813
4846
  e.preventDefault();
4814
- const dy = wheelDeltaYPixels(e);
4815
- const normDy = Math.abs(dy) < 20 ? dy * 12 : dy;
4847
+ const dy = wheelDeltaPixels(e.deltaY, e.deltaMode);
4848
+ const normDy = Math.abs(dy) < TRACKPAD_MAX_DELTA ? dy * 12 : dy;
4816
4849
  const factor = Math.exp(-normDy * wheelZoomSensitivity);
4817
4850
  const rect = element.getBoundingClientRect();
4818
4851
  camera.setZoom(camera.zoom * factor, {
@@ -4823,8 +4856,10 @@ function attachViewportInput(options) {
4823
4856
  return;
4824
4857
  }
4825
4858
  e.preventDefault();
4826
- camera.x -= e.deltaX * wheelPanSensitivity / camera.zoom;
4827
- camera.y -= e.deltaY * wheelPanSensitivity / camera.zoom;
4859
+ const panDx = softenWheelDelta(wheelDeltaPixels(e.deltaX, e.deltaMode));
4860
+ const panDy = softenWheelDelta(wheelDeltaPixels(e.deltaY, e.deltaMode));
4861
+ camera.x -= panDx * wheelPanSensitivity / camera.zoom;
4862
+ camera.y -= panDy * wheelPanSensitivity / camera.zoom;
4828
4863
  onUpdate();
4829
4864
  };
4830
4865
  const onPointerDown = (e) => {
@@ -7379,6 +7414,40 @@ var VectorViewport = forwardRef(
7379
7414
  onWorldPointerLeaveRef.current = onWorldPointerLeave;
7380
7415
  const onPlacementPreviewChangeRef = useRef(onPlacementPreviewChange);
7381
7416
  onPlacementPreviewChangeRef.current = onPlacementPreviewChange;
7417
+ const directRemoteStrokePreviewRef = useRef(false);
7418
+ const remoteStrokePreviewFrameRef = useRef(null);
7419
+ const pendingRemoteStrokePreviewRef = useRef(null);
7420
+ const flushRemoteStrokePreview = useCallback(() => {
7421
+ remoteStrokePreviewFrameRef.current = null;
7422
+ const pending = pendingRemoteStrokePreviewRef.current;
7423
+ if (!pending) return;
7424
+ onPlacementPreviewChangeRef.current?.({
7425
+ kind: "stroke",
7426
+ tool: pending.tool,
7427
+ points: pending.points
7428
+ });
7429
+ }, []);
7430
+ const emitRemoteStrokePreview = useCallback(
7431
+ (tool, points) => {
7432
+ if (tool === "laser") return;
7433
+ directRemoteStrokePreviewRef.current = true;
7434
+ pendingRemoteStrokePreviewRef.current = { tool, points };
7435
+ if (remoteStrokePreviewFrameRef.current != null) return;
7436
+ remoteStrokePreviewFrameRef.current = requestAnimationFrame(
7437
+ flushRemoteStrokePreview
7438
+ );
7439
+ },
7440
+ [flushRemoteStrokePreview]
7441
+ );
7442
+ const emitRemoteStrokePreviewClear = useCallback(() => {
7443
+ if (remoteStrokePreviewFrameRef.current != null) {
7444
+ cancelAnimationFrame(remoteStrokePreviewFrameRef.current);
7445
+ remoteStrokePreviewFrameRef.current = null;
7446
+ }
7447
+ pendingRemoteStrokePreviewRef.current = null;
7448
+ directRemoteStrokePreviewRef.current = false;
7449
+ onPlacementPreviewChangeRef.current?.(null);
7450
+ }, []);
7382
7451
  const overlayCameraTickRef = useRef(false);
7383
7452
  overlayCameraTickRef.current = interactive || remotePresence != null && remotePresence.length > 0 || presenceOverlay != null;
7384
7453
  const pruneEraserTrail = useCallback(
@@ -7497,6 +7566,7 @@ var VectorViewport = forwardRef(
7497
7566
  if (itemId) {
7498
7567
  renderSceneWithLivePenStroke(null);
7499
7568
  }
7569
+ emitRemoteStrokePreviewClear();
7500
7570
  setPlacementPreview(null);
7501
7571
  commitCompletedStroke({
7502
7572
  tool,
@@ -7508,6 +7578,7 @@ var VectorViewport = forwardRef(
7508
7578
  },
7509
7579
  [
7510
7580
  commitCompletedStroke,
7581
+ emitRemoteStrokePreviewClear,
7511
7582
  releaseInteractionPointer,
7512
7583
  renderSceneWithLivePenStroke
7513
7584
  ]
@@ -7619,8 +7690,18 @@ var VectorViewport = forwardRef(
7619
7690
  };
7620
7691
  }, [onWorldPointerMove]);
7621
7692
  useEffect(() => {
7693
+ if (directRemoteStrokePreviewRef.current && placementPreview === null) {
7694
+ return;
7695
+ }
7622
7696
  onPlacementPreviewChangeRef.current?.(placementPreview);
7623
7697
  }, [placementPreview]);
7698
+ useEffect(() => {
7699
+ return () => {
7700
+ if (remoteStrokePreviewFrameRef.current != null) {
7701
+ cancelAnimationFrame(remoteStrokePreviewFrameRef.current);
7702
+ }
7703
+ };
7704
+ }, []);
7624
7705
  useEffect(() => {
7625
7706
  const scene = sceneRef.current;
7626
7707
  if (scene) {
@@ -7651,15 +7732,22 @@ var VectorViewport = forwardRef(
7651
7732
  useEffect(() => {
7652
7733
  rememberImageBlobHrefs(items, rememberedImageBlobHrefsRef.current);
7653
7734
  }, [items]);
7654
- useEffect(
7655
- () => () => {
7656
- releaseRememberedBlobHrefs(
7657
- rememberedImageBlobHrefsRef.current,
7658
- (href) => URL.revokeObjectURL(href)
7659
- );
7660
- },
7661
- []
7662
- );
7735
+ const blobRevokeTimerRef = useRef(null);
7736
+ useEffect(() => {
7737
+ if (blobRevokeTimerRef.current !== null) {
7738
+ clearTimeout(blobRevokeTimerRef.current);
7739
+ blobRevokeTimerRef.current = null;
7740
+ }
7741
+ return () => {
7742
+ blobRevokeTimerRef.current = setTimeout(() => {
7743
+ releaseRememberedBlobHrefs(
7744
+ rememberedImageBlobHrefsRef.current,
7745
+ (href) => URL.revokeObjectURL(href)
7746
+ );
7747
+ blobRevokeTimerRef.current = null;
7748
+ }, 100);
7749
+ };
7750
+ }, []);
7663
7751
  const PASTE_OFFSET_WORLD = 24;
7664
7752
  const copyIdsToInternalClipboard = useCallback((ids) => {
7665
7753
  if (ids.length === 0) return;
@@ -8607,6 +8695,7 @@ var VectorViewport = forwardRef(
8607
8695
  setPlacementPreview(null);
8608
8696
  } else if (directPenStroke) {
8609
8697
  setPlacementPreview(null);
8698
+ emitRemoteStrokePreview(tool, [startPoint]);
8610
8699
  } else {
8611
8700
  setPlacementPreview({
8612
8701
  kind: "stroke",
@@ -8648,6 +8737,7 @@ var VectorViewport = forwardRef(
8648
8737
  [
8649
8738
  applePencilNav,
8650
8739
  captureInteractionPointer,
8740
+ emitRemoteStrokePreview,
8651
8741
  finalizeStrokeDragState,
8652
8742
  renderSceneWithLivePenStroke,
8653
8743
  screenToWorld
@@ -8706,6 +8796,7 @@ var VectorViewport = forwardRef(
8706
8796
  activeInteractionPointerIdRef.current = e.pointerId;
8707
8797
  activeInteractionPointerTargetRef.current = null;
8708
8798
  setPlacementPreview(null);
8799
+ emitRemoteStrokePreview(tool, [startPoint]);
8709
8800
  debugApplePencilPointer("native-pointerdown", {
8710
8801
  pointerType: e.pointerType,
8711
8802
  pointerId: e.pointerId,
@@ -8723,6 +8814,7 @@ var VectorViewport = forwardRef(
8723
8814
  };
8724
8815
  }, [
8725
8816
  applePencilNav,
8817
+ emitRemoteStrokePreview,
8726
8818
  finalizeStrokeDragState,
8727
8819
  interactive,
8728
8820
  renderSceneWithLivePenStroke,
@@ -8806,6 +8898,7 @@ var VectorViewport = forwardRef(
8806
8898
  activeInteractionPointerTargetRef.current = null;
8807
8899
  activeInteractionTouchIdRef.current = touch.identifier;
8808
8900
  setPlacementPreview(null);
8901
+ emitRemoteStrokePreview(tool, [startPoint]);
8809
8902
  debugApplePencilPointer("touchstart-stroke", {
8810
8903
  touchId: touch.identifier,
8811
8904
  touchType: touchKind(touch),
@@ -8837,6 +8930,7 @@ var VectorViewport = forwardRef(
8837
8930
  renderSceneWithLivePenStroke(item);
8838
8931
  }
8839
8932
  setPlacementPreview(null);
8933
+ emitRemoteStrokePreview(st.tool, interpolated);
8840
8934
  }
8841
8935
  debugApplePencilPointer("touchmove-stroke", {
8842
8936
  touchId: touch.identifier,
@@ -8899,6 +8993,7 @@ var VectorViewport = forwardRef(
8899
8993
  };
8900
8994
  }, [
8901
8995
  applePencilNav,
8996
+ emitRemoteStrokePreview,
8902
8997
  finalizeStrokeDragState,
8903
8998
  interactive,
8904
8999
  renderSceneWithLivePenStroke,
@@ -8953,6 +9048,7 @@ var VectorViewport = forwardRef(
8953
9048
  renderSceneWithLivePenStroke(item);
8954
9049
  }
8955
9050
  setPlacementPreview(null);
9051
+ emitRemoteStrokePreview(st.tool, interpolated);
8956
9052
  return;
8957
9053
  }
8958
9054
  if (st.tool === "laser") {
@@ -9094,6 +9190,9 @@ var VectorViewport = forwardRef(
9094
9190
  }
9095
9191
  const cam = cameraRef.current;
9096
9192
  if (!cam) {
9193
+ if (st.kind === "stroke") {
9194
+ emitRemoteStrokePreviewClear();
9195
+ }
9097
9196
  if (st.kind === "erase") {
9098
9197
  eraserPreviewIdsRef.current.clear();
9099
9198
  setEraserPreviewIds([]);
@@ -9325,6 +9424,8 @@ var VectorViewport = forwardRef(
9325
9424
  document.removeEventListener("pointercancel", onUp);
9326
9425
  };
9327
9426
  }, [
9427
+ emitRemoteStrokePreview,
9428
+ emitRemoteStrokePreviewClear,
9328
9429
  interactive,
9329
9430
  pruneEraserTrail,
9330
9431
  pruneLaserTrail,