|
@@ -87,12 +87,66 @@ export interface ExcalidrawElementWithCanvas {
|
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
|
canvas: HTMLCanvasElement;
|
|
canvas: HTMLCanvasElement;
|
|
theme: RenderConfig["theme"];
|
|
theme: RenderConfig["theme"];
|
|
- canvasZoom: Zoom["value"];
|
|
|
|
|
|
+ scale: number;
|
|
canvasOffsetX: number;
|
|
canvasOffsetX: number;
|
|
canvasOffsetY: number;
|
|
canvasOffsetY: number;
|
|
boundTextElementVersion: number | null;
|
|
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 = (
|
|
const generateElementCanvas = (
|
|
element: NonDeletedExcalidrawElement,
|
|
element: NonDeletedExcalidrawElement,
|
|
zoom: Zoom,
|
|
zoom: Zoom,
|
|
@@ -102,44 +156,35 @@ const generateElementCanvas = (
|
|
const context = canvas.getContext("2d")!;
|
|
const context = canvas.getContext("2d")!;
|
|
const padding = getCanvasPadding(element);
|
|
const padding = getCanvasPadding(element);
|
|
|
|
|
|
|
|
+ const { width, height, scale } = cappedElementCanvasSize(element, zoom);
|
|
|
|
+
|
|
|
|
+ canvas.width = width;
|
|
|
|
+ canvas.height = height;
|
|
|
|
+
|
|
let canvasOffsetX = 0;
|
|
let canvasOffsetX = 0;
|
|
let canvasOffsetY = 0;
|
|
let canvasOffsetY = 0;
|
|
|
|
|
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
|
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 =
|
|
canvasOffsetX =
|
|
element.x > x1
|
|
element.x > x1
|
|
- ? distance(element.x, x1) * window.devicePixelRatio * zoom.value
|
|
|
|
|
|
+ ? distance(element.x, x1) * window.devicePixelRatio * scale
|
|
: 0;
|
|
: 0;
|
|
|
|
|
|
canvasOffsetY =
|
|
canvasOffsetY =
|
|
element.y > y1
|
|
element.y > y1
|
|
- ? distance(element.y, y1) * window.devicePixelRatio * zoom.value
|
|
|
|
|
|
+ ? distance(element.y, y1) * window.devicePixelRatio * scale
|
|
: 0;
|
|
: 0;
|
|
|
|
|
|
context.translate(canvasOffsetX, canvasOffsetY);
|
|
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.save();
|
|
- context.translate(padding * zoom.value, padding * zoom.value);
|
|
|
|
|
|
+ context.translate(padding * scale, padding * scale);
|
|
context.scale(
|
|
context.scale(
|
|
- window.devicePixelRatio * zoom.value,
|
|
|
|
- window.devicePixelRatio * zoom.value,
|
|
|
|
|
|
+ window.devicePixelRatio * scale,
|
|
|
|
+ window.devicePixelRatio * scale,
|
|
);
|
|
);
|
|
|
|
|
|
const rc = rough.canvas(canvas);
|
|
const rc = rough.canvas(canvas);
|
|
@@ -156,7 +201,7 @@ const generateElementCanvas = (
|
|
element,
|
|
element,
|
|
canvas,
|
|
canvas,
|
|
theme: renderConfig.theme,
|
|
theme: renderConfig.theme,
|
|
- canvasZoom: zoom.value,
|
|
|
|
|
|
+ scale,
|
|
canvasOffsetX,
|
|
canvasOffsetX,
|
|
canvasOffsetY,
|
|
canvasOffsetY,
|
|
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
|
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
|
@@ -670,7 +715,7 @@ const generateElementWithCanvas = (
|
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
|
const shouldRegenerateBecauseZoom =
|
|
const shouldRegenerateBecauseZoom =
|
|
prevElementWithCanvas &&
|
|
prevElementWithCanvas &&
|
|
- prevElementWithCanvas.canvasZoom !== zoom.value &&
|
|
|
|
|
|
+ prevElementWithCanvas.scale !== zoom.value &&
|
|
!renderConfig?.shouldCacheIgnoreZoom;
|
|
!renderConfig?.shouldCacheIgnoreZoom;
|
|
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
|
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
|
|
|
|
|
@@ -701,7 +746,7 @@ const drawElementFromCanvas = (
|
|
) => {
|
|
) => {
|
|
const element = elementWithCanvas.element;
|
|
const element = elementWithCanvas.element;
|
|
const padding = getCanvasPadding(element);
|
|
const padding = getCanvasPadding(element);
|
|
- const zoom = elementWithCanvas.canvasZoom;
|
|
|
|
|
|
+ const zoom = elementWithCanvas.scale;
|
|
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
|
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
|
|
|
|
|
// Free draw elements will otherwise "shuffle" as the min x and y change
|
|
// 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));
|
|
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
|
|
tempCanvas.width =
|
|
tempCanvas.width =
|
|
maxDim * window.devicePixelRatio * zoom +
|
|
maxDim * window.devicePixelRatio * zoom +
|
|
- padding * elementWithCanvas.canvasZoom * 10;
|
|
|
|
|
|
+ padding * elementWithCanvas.scale * 10;
|
|
tempCanvas.height =
|
|
tempCanvas.height =
|
|
maxDim * window.devicePixelRatio * zoom +
|
|
maxDim * window.devicePixelRatio * zoom +
|
|
- padding * elementWithCanvas.canvasZoom * 10;
|
|
|
|
|
|
+ padding * elementWithCanvas.scale * 10;
|
|
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
|
|
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
|
|
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
|
|
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
|
|
|
|
|
|
@@ -812,11 +857,11 @@ const drawElementFromCanvas = (
|
|
context.drawImage(
|
|
context.drawImage(
|
|
elementWithCanvas.canvas!,
|
|
elementWithCanvas.canvas!,
|
|
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
|
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
|
- (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
|
|
|
|
|
+ (padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
|
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
|
(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 (
|
|
if (
|