diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 0861315c..7adcc584 100644 --- a/src/renderer/renderElement.ts +++ b/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 (