Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax

This commit is contained in:
Daniel J. Geiger
2023-04-08 09:52:03 -05:00
40 changed files with 1265 additions and 426 deletions

View File

@@ -41,10 +41,10 @@ import {
} from "../constants";
import { getStroke, StrokeOptions } from "perfect-freehand";
import {
getApproxLineHeight,
getBoundTextElement,
getContainerCoords,
getContainerElement,
getLineHeightInPx,
getMaxContainerHeight,
getMaxContainerWidth,
} from "../element/textElement";
@@ -88,12 +88,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,
@@ -103,44 +157,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);
@@ -157,7 +202,7 @@ const generateElementCanvas = (
element,
canvas,
theme: renderConfig.theme,
canvasZoom: zoom.value,
scale,
canvasOffsetX,
canvasOffsetY,
boundTextElementVersion: getBoundTextElement(element)?.version || null,
@@ -286,9 +331,7 @@ const drawElementOnCanvas = (
// Canvas does not support multiline text by default
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.containerId
? getApproxLineHeight(getFontString(element))
: element.height / lines.length;
const horizontalOffset =
element.textAlign === "center"
? element.width / 2
@@ -297,11 +340,16 @@ const drawElementOnCanvas = (
: 0;
context.textBaseline = "bottom";
const lineHeightPx = getLineHeightInPx(
element.fontSize,
element.lineHeight,
);
for (let index = 0; index < lines.length; index++) {
context.fillText(
lines[index],
horizontalOffset,
(index + 1) * lineHeight,
(index + 1) * lineHeightPx,
);
}
context.restore();
@@ -674,7 +722,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;
@@ -705,7 +753,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
@@ -732,10 +780,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;
@@ -816,11 +864,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 (
@@ -1325,7 +1373,10 @@ export const renderElementToSvg = (
}) rotate(${degree} ${cx} ${cy})`,
);
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.height / lines.length;
const lineHeightPx = getLineHeightInPx(
element.fontSize,
element.lineHeight,
);
const horizontalOffset =
element.textAlign === "center"
? element.width / 2
@@ -1343,7 +1394,7 @@ export const renderElementToSvg = (
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
text.textContent = lines[i];
text.setAttribute("x", `${horizontalOffset}`);
text.setAttribute("y", `${i * lineHeight}`);
text.setAttribute("y", `${i * lineHeightPx}`);
text.setAttribute("font-family", getFontFamilyString(element));
text.setAttribute("font-size", `${element.fontSize}px`);
text.setAttribute("fill", element.strokeColor);