Browse Source

fix: element vanishes when zoomed in (#6417)

Co-authored-by: David Luzar <[email protected]>
zsviczian 2 years ago
parent
commit
705ac9c1ab
1 changed files with 75 additions and 30 deletions
  1. 75 30
      src/renderer/renderElement.ts

+ 75 - 30
src/renderer/renderElement.ts

@@ -87,12 +87,66 @@ export interface ExcalidrawElementWithCanvas {
   element: ExcalidrawElement | ExcalidrawTextElement;
   canvas: HTMLCanvasElement;
   theme: RenderConfig["theme"];
-  canvasZoom: Zoom["value"];
+  scale: number;
   canvasOffsetX: number;
   canvasOffsetY: number;
   boundTextElementVersion: number | null;
 }
 
+const cappedElementCanvasSize = (
+  element: NonDeletedExcalidrawElement,
+  zoom: Zoom,
+): {
+  width: number;
+  height: number;
+  scale: number;
+} => {
+  // these limits are ballpark, they depend on specific browsers and device.
+  // We've chosen lower limits to be safe. We might want to change these limits
+  // based on browser/device type, if we get reports of low quality rendering
+  // on zoom.
+  //
+  // ~ safari mobile canvas area limit
+  const AREA_LIMIT = 16777216;
+  // ~ safari width/height limit based on developer.mozilla.org.
+  const WIDTH_HEIGHT_LIMIT = 32767;
+
+  const padding = getCanvasPadding(element);
+
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const elementWidth =
+    isLinearElement(element) || isFreeDrawElement(element)
+      ? distance(x1, x2)
+      : element.width;
+  const elementHeight =
+    isLinearElement(element) || isFreeDrawElement(element)
+      ? distance(y1, y2)
+      : element.height;
+
+  let width = elementWidth * window.devicePixelRatio + padding * 2;
+  let height = elementHeight * window.devicePixelRatio + padding * 2;
+
+  let scale: number = zoom.value;
+
+  // rescale to ensure width and height is within limits
+  if (
+    width * scale > WIDTH_HEIGHT_LIMIT ||
+    height * scale > WIDTH_HEIGHT_LIMIT
+  ) {
+    scale = Math.min(WIDTH_HEIGHT_LIMIT / width, WIDTH_HEIGHT_LIMIT / height);
+  }
+
+  // rescale to ensure canvas area is within limits
+  if (width * height * scale * scale > AREA_LIMIT) {
+    scale = Math.sqrt(AREA_LIMIT / (width * height));
+  }
+
+  width = Math.floor(width * scale);
+  height = Math.floor(height * scale);
+
+  return { width, height, scale };
+};
+
 const generateElementCanvas = (
   element: NonDeletedExcalidrawElement,
   zoom: Zoom,
@@ -102,44 +156,35 @@ const generateElementCanvas = (
   const context = canvas.getContext("2d")!;
   const padding = getCanvasPadding(element);
 
+  const { width, height, scale } = cappedElementCanvasSize(element, zoom);
+
+  canvas.width = width;
+  canvas.height = height;
+
   let canvasOffsetX = 0;
   let canvasOffsetY = 0;
 
   if (isLinearElement(element) || isFreeDrawElement(element)) {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
-
-    canvas.width =
-      distance(x1, x2) * window.devicePixelRatio * zoom.value +
-      padding * zoom.value * 2;
-    canvas.height =
-      distance(y1, y2) * window.devicePixelRatio * zoom.value +
-      padding * zoom.value * 2;
+    const [x1, y1] = getElementAbsoluteCoords(element);
 
     canvasOffsetX =
       element.x > x1
-        ? distance(element.x, x1) * window.devicePixelRatio * zoom.value
+        ? distance(element.x, x1) * window.devicePixelRatio * scale
         : 0;
 
     canvasOffsetY =
       element.y > y1
-        ? distance(element.y, y1) * window.devicePixelRatio * zoom.value
+        ? distance(element.y, y1) * window.devicePixelRatio * scale
         : 0;
 
     context.translate(canvasOffsetX, canvasOffsetY);
-  } else {
-    canvas.width =
-      element.width * window.devicePixelRatio * zoom.value +
-      padding * zoom.value * 2;
-    canvas.height =
-      element.height * window.devicePixelRatio * zoom.value +
-      padding * zoom.value * 2;
   }
 
   context.save();
-  context.translate(padding * zoom.value, padding * zoom.value);
+  context.translate(padding * scale, padding * scale);
   context.scale(
-    window.devicePixelRatio * zoom.value,
-    window.devicePixelRatio * zoom.value,
+    window.devicePixelRatio * scale,
+    window.devicePixelRatio * scale,
   );
 
   const rc = rough.canvas(canvas);
@@ -156,7 +201,7 @@ const generateElementCanvas = (
     element,
     canvas,
     theme: renderConfig.theme,
-    canvasZoom: zoom.value,
+    scale,
     canvasOffsetX,
     canvasOffsetY,
     boundTextElementVersion: getBoundTextElement(element)?.version || null,
@@ -670,7 +715,7 @@ const generateElementWithCanvas = (
   const prevElementWithCanvas = elementWithCanvasCache.get(element);
   const shouldRegenerateBecauseZoom =
     prevElementWithCanvas &&
-    prevElementWithCanvas.canvasZoom !== zoom.value &&
+    prevElementWithCanvas.scale !== zoom.value &&
     !renderConfig?.shouldCacheIgnoreZoom;
   const boundTextElementVersion = getBoundTextElement(element)?.version || null;
 
@@ -701,7 +746,7 @@ const drawElementFromCanvas = (
 ) => {
   const element = elementWithCanvas.element;
   const padding = getCanvasPadding(element);
-  const zoom = elementWithCanvas.canvasZoom;
+  const zoom = elementWithCanvas.scale;
   let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
 
   // Free draw elements will otherwise "shuffle" as the min x and y change
@@ -728,10 +773,10 @@ const drawElementFromCanvas = (
     const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
     tempCanvas.width =
       maxDim * window.devicePixelRatio * zoom +
-      padding * elementWithCanvas.canvasZoom * 10;
+      padding * elementWithCanvas.scale * 10;
     tempCanvas.height =
       maxDim * window.devicePixelRatio * zoom +
-      padding * elementWithCanvas.canvasZoom * 10;
+      padding * elementWithCanvas.scale * 10;
     const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
     const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
 
@@ -812,11 +857,11 @@ const drawElementFromCanvas = (
     context.drawImage(
       elementWithCanvas.canvas!,
       (x1 + renderConfig.scrollX) * window.devicePixelRatio -
-        (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
+        (padding * elementWithCanvas.scale) / elementWithCanvas.scale,
       (y1 + renderConfig.scrollY) * window.devicePixelRatio -
-        (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
-      elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
-      elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
+        (padding * elementWithCanvas.scale) / elementWithCanvas.scale,
+      elementWithCanvas.canvas!.width / elementWithCanvas.scale,
+      elementWithCanvas.canvas!.height / elementWithCanvas.scale,
     );
 
     if (