@canvas-harness/react 0.1.3 → 0.1.5

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/index.d.cts CHANGED
@@ -131,6 +131,24 @@ type CanvasProps = {
131
131
  * <Canvas selectionColor="#10b981" />
132
132
  */
133
133
  selectionColor?: string;
134
+ /**
135
+ * Cap on the canvas backing-store DPR. Defaults to `1`.
136
+ *
137
+ * At native device-pixel ratio on hi-DPI displays (Mac Retina ≈ 2,
138
+ * Windows 4K @ 175% ≈ 1.75), the canvas backing buffer can hit
139
+ * 20-30 megapixels per frame — the per-frame GPU-upload cost alone
140
+ * eats a sizable slice of the frame budget. Capping DPR at 1 keeps
141
+ * perf consistent across hardware at the cost of slightly softer
142
+ * shape outlines on hi-DPI displays. Text remains crisp regardless
143
+ * (the text bitmap cache handles its own DPR).
144
+ *
145
+ * Bump to `2` (or `window.devicePixelRatio`) when crispness matters
146
+ * more than FPS — e.g. presentation slides, print-export views.
147
+ *
148
+ * @example
149
+ * <Canvas maxDpr={2} /> // pixel-crisp at the cost of FPS on hi-DPI
150
+ */
151
+ maxDpr?: number;
134
152
  /**
135
153
  * Render a custom node's React subtree. Called once per
136
154
  * library-mounted custom-node id; positioning is handled by the
package/dist/index.d.ts CHANGED
@@ -131,6 +131,24 @@ type CanvasProps = {
131
131
  * <Canvas selectionColor="#10b981" />
132
132
  */
133
133
  selectionColor?: string;
134
+ /**
135
+ * Cap on the canvas backing-store DPR. Defaults to `1`.
136
+ *
137
+ * At native device-pixel ratio on hi-DPI displays (Mac Retina ≈ 2,
138
+ * Windows 4K @ 175% ≈ 1.75), the canvas backing buffer can hit
139
+ * 20-30 megapixels per frame — the per-frame GPU-upload cost alone
140
+ * eats a sizable slice of the frame budget. Capping DPR at 1 keeps
141
+ * perf consistent across hardware at the cost of slightly softer
142
+ * shape outlines on hi-DPI displays. Text remains crisp regardless
143
+ * (the text bitmap cache handles its own DPR).
144
+ *
145
+ * Bump to `2` (or `window.devicePixelRatio`) when crispness matters
146
+ * more than FPS — e.g. presentation slides, print-export views.
147
+ *
148
+ * @example
149
+ * <Canvas maxDpr={2} /> // pixel-crisp at the cost of FPS on hi-DPI
150
+ */
151
+ maxDpr?: number;
134
152
  /**
135
153
  * Render a custom node's React subtree. Called once per
136
154
  * library-mounted custom-node id; positioning is handled by the
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRenderer, DEFAULT_MINIMAP_MAX_NODES, renderMinimapContent, sceneBounds, drawMinimapViewport, worldViewportFromCamera, screenToWorld, hitTestAny, copy, cut, paste, createPalmRejectionState, createDefaultTextareaEditor, minimapScreenToWorld, notePenActive, shouldRejectTouch, notePenInactive, asEdgeId, midpointToCubicControls, projectToNodeBoundary, marqueeNodes, shouldAutoFit, computeAutoFitHeight, hitTestPoint, worldToNodeLocal, edgeLabelBoundsWorld, asNodeId, zoomAtScreenPoint, clampZoom, panByScreen } from '@canvas-harness/core';
2
- import { createContext, useContext, useSyncExternalStore, useRef, useState, useEffect } from 'react';
2
+ import { createContext, useContext, useSyncExternalStore, useRef, useEffect, useState } from 'react';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
 
5
5
  // src/Canvas.tsx
@@ -1045,6 +1045,7 @@ function CanvasSurface({
1045
1045
  arrowDefaults,
1046
1046
  background,
1047
1047
  selectionColor,
1048
+ maxDpr,
1048
1049
  renderCustomNodeView,
1049
1050
  children
1050
1051
  }) {
@@ -1062,8 +1063,15 @@ function CanvasSurface({
1062
1063
  const interactionMode = useInteractionMode();
1063
1064
  useArrowTool(wrapRef, store, tool === "arrow", arrowDefaults);
1064
1065
  const { mountedIds, setMountedIds } = useOverlayHost();
1065
- const [camera, setCamera] = useState(() => store.getCamera());
1066
- useEffect(() => store.subscribe("camera", (c) => setCamera({ ...c })), [store]);
1066
+ useEffect(() => {
1067
+ const el = overlayRef.current;
1068
+ if (!el) return;
1069
+ const apply = (c) => {
1070
+ el.style.transform = `translate(${-c.x * c.z}px, ${-c.y * c.z}px) scale(${c.z})`;
1071
+ };
1072
+ apply(store.getCamera());
1073
+ return store.subscribe("camera", apply);
1074
+ }, [store]);
1067
1075
  useEffect(() => {
1068
1076
  if (!staticRef.current || !interactiveRef.current || w === 0 || h === 0) return;
1069
1077
  if (rendererRef.current) {
@@ -1079,6 +1087,7 @@ function CanvasSurface({
1079
1087
  height: h,
1080
1088
  background,
1081
1089
  selectionColor,
1090
+ maxDpr,
1082
1091
  onOverlayChange: (ids) => setMountedIds(ids)
1083
1092
  });
1084
1093
  r.start();
@@ -1088,7 +1097,7 @@ function CanvasSurface({
1088
1097
  r.dispose();
1089
1098
  rendererRef.current = null;
1090
1099
  };
1091
- }, [store, theme, w, h, onRenderer, setMountedIds]);
1100
+ }, [store, theme, w, h, maxDpr, onRenderer, setMountedIds]);
1092
1101
  useEffect(() => {
1093
1102
  rendererRef.current?.setBackground(background);
1094
1103
  }, [background]);
@@ -1110,9 +1119,9 @@ function CanvasSurface({
1110
1119
  if (toolRef.current === "select") {
1111
1120
  const rect = el.getBoundingClientRect();
1112
1121
  const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1113
- const camera2 = store.getCamera();
1114
- const world = screenToWorld(screen, camera2);
1115
- const hit = hitTestAny(store, world, camera2.z);
1122
+ const camera = store.getCamera();
1123
+ const world = screenToWorld(screen, camera);
1124
+ const hit = hitTestAny(store, world, camera.z);
1116
1125
  if (hit && hit.kind === "body" && "nodeId" in hit) {
1117
1126
  store.beginEdit(hit.nodeId);
1118
1127
  } else if (hit && hit.kind === "body" && "edgeId" in hit) {
@@ -1150,9 +1159,9 @@ function CanvasSurface({
1150
1159
  if (e.button !== 0) return;
1151
1160
  if (!isShapeTool(toolRef.current)) return;
1152
1161
  if (store.getInteractionState().mode === "editing") return;
1153
- const camera2 = store.getCamera();
1154
- const world = screenToWorld(screenFromEvent(e), camera2);
1155
- if (hitTestAny(store, world, camera2.z)) return;
1162
+ const camera = store.getCamera();
1163
+ const world = screenToWorld(screenFromEvent(e), camera);
1164
+ if (hitTestAny(store, world, camera.z)) return;
1156
1165
  startWorld = world;
1157
1166
  startScreen = screenFromEvent(e);
1158
1167
  activePointerId = e.pointerId;
@@ -1259,7 +1268,8 @@ function CanvasSurface({
1259
1268
  window.addEventListener("keydown", onKey);
1260
1269
  return () => window.removeEventListener("keydown", onKey);
1261
1270
  }, [store]);
1262
- const overlayTransform = `translate(${-camera.x * camera.z}px, ${-camera.y * camera.z}px) scale(${camera.z})`;
1271
+ const initialCamera = store.getCamera();
1272
+ const overlayTransform = `translate(${-initialCamera.x * initialCamera.z}px, ${-initialCamera.y * initialCamera.z}px) scale(${initialCamera.z})`;
1263
1273
  return /* @__PURE__ */ jsxs(
1264
1274
  "div",
1265
1275
  {