perf: cache the temp canvas created for labeled arrows (#8267)

* perf: cache the temp canvas created for labeled arrows

* use allEleemntsMap so bound text element can be retrieved when editing

* remove logs

* fix rotation

* pass isRotating

* feat: cache `element.angle` instead of relying on `appState.isRotating`

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
pull/8188/merge
Aakansha Doshi 6 months ago committed by GitHub
parent 43b2476dfe
commit bd7b778f41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -118,11 +118,13 @@ export interface ExcalidrawElementWithCanvas {
canvas: HTMLCanvasElement;
theme: AppState["theme"];
scale: number;
angle: number;
zoomValue: AppState["zoom"]["value"];
canvasOffsetX: number;
canvasOffsetY: number;
boundTextElementVersion: number | null;
containingFrameOpacity: number;
boundTextCanvas: HTMLCanvasElement;
}
const cappedElementCanvasSize = (
@ -182,7 +184,7 @@ const cappedElementCanvasSize = (
const generateElementCanvas = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
elementsMap: NonDeletedSceneElementsMap,
zoom: Zoom,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
@ -234,8 +236,72 @@ const generateElementCanvas = (
}
drawElementOnCanvas(element, rc, context, renderConfig, appState);
context.restore();
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextCanvas = document.createElement("canvas");
const boundTextCanvasContext = boundTextCanvas.getContext("2d")!;
if (isArrowElement(element) && boundTextElement) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
boundTextCanvas.width =
maxDim * window.devicePixelRatio * scale + padding * scale * 10;
boundTextCanvas.height =
maxDim * window.devicePixelRatio * scale + padding * scale * 10;
boundTextCanvasContext.translate(
boundTextCanvas.width / 2,
boundTextCanvas.height / 2,
);
boundTextCanvasContext.rotate(element.angle);
boundTextCanvasContext.drawImage(
canvas!,
-canvas.width / 2,
-canvas.height / 2,
canvas.width,
canvas.height,
);
const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
boundTextElement,
elementsMap,
);
boundTextCanvasContext.rotate(-element.angle);
const offsetX = (boundTextCanvas.width - canvas!.width) / 2;
const offsetY = (boundTextCanvas.height - canvas!.height) / 2;
const shiftX =
boundTextCanvas.width / 2 -
(boundTextCx - x1) * window.devicePixelRatio * scale -
offsetX -
padding * scale;
const shiftY =
boundTextCanvas.height / 2 -
(boundTextCy - y1) * window.devicePixelRatio * scale -
offsetY -
padding * scale;
boundTextCanvasContext.translate(-shiftX, -shiftY);
// Clear the bound text area
boundTextCanvasContext.clearRect(
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
window.devicePixelRatio *
scale,
-(boundTextElement.height / 2 + BOUND_TEXT_PADDING) *
window.devicePixelRatio *
scale,
(boundTextElement.width + BOUND_TEXT_PADDING * 2) *
window.devicePixelRatio *
scale,
(boundTextElement.height + BOUND_TEXT_PADDING * 2) *
window.devicePixelRatio *
scale,
);
}
return {
element,
canvas,
@ -248,6 +314,8 @@ const generateElementCanvas = (
getBoundTextElement(element, elementsMap)?.version || null,
containingFrameOpacity:
getContainingFrame(element, elementsMap)?.opacity || 100,
boundTextCanvas,
angle: element.angle,
};
};
@ -423,7 +491,7 @@ export const elementWithCanvasCache = new WeakMap<
const generateElementWithCanvas = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
elementsMap: NonDeletedSceneElementsMap,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
) => {
@ -433,8 +501,8 @@ const generateElementWithCanvas = (
prevElementWithCanvas &&
prevElementWithCanvas.zoomValue !== zoom.value &&
!appState?.shouldCacheIgnoreZoom;
const boundTextElementVersion =
getBoundTextElement(element, elementsMap)?.version || null;
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElementVersion = boundTextElement?.version || null;
const containingFrameOpacity =
getContainingFrame(element, elementsMap)?.opacity || 100;
@ -444,7 +512,14 @@ const generateElementWithCanvas = (
shouldRegenerateBecauseZoom ||
prevElementWithCanvas.theme !== appState.theme ||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity ||
// since we rotate the canvas when copying from cached canvas, we don't
// regenerate the cached canvas. But we need to in case of labels which are
// cached alongside the arrow, and we want the labels to remain unrotated
// with respect to the arrow.
(isArrowElement(element) &&
boundTextElement &&
element.angle !== prevElementWithCanvas.angle)
) {
const elementWithCanvas = generateElementCanvas(
element,
@ -481,75 +556,21 @@ const drawElementFromCanvas = (
const boundTextElement = getBoundTextElement(element, allElementsMap);
if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas");
const tempCanvasContext = tempCanvas.getContext("2d")!;
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
tempCanvas.width =
maxDim * window.devicePixelRatio * zoom +
padding * elementWithCanvas.scale * 10;
tempCanvas.height =
maxDim * window.devicePixelRatio * zoom +
padding * elementWithCanvas.scale * 10;
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
tempCanvasContext.translate(tempCanvas.width / 2, tempCanvas.height / 2);
tempCanvasContext.rotate(element.angle);
tempCanvasContext.drawImage(
elementWithCanvas.canvas!,
-elementWithCanvas.canvas.width / 2,
-elementWithCanvas.canvas.height / 2,
elementWithCanvas.canvas.width,
elementWithCanvas.canvas.height,
);
const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
boundTextElement,
allElementsMap,
);
tempCanvasContext.rotate(-element.angle);
// Shift the canvas to the center of the bound text element
const shiftX =
tempCanvas.width / 2 -
(boundTextCx - x1) * window.devicePixelRatio * zoom -
offsetX -
padding * zoom;
const shiftY =
tempCanvas.height / 2 -
(boundTextCy - y1) * window.devicePixelRatio * zoom -
offsetY -
padding * zoom;
tempCanvasContext.translate(-shiftX, -shiftY);
// Clear the bound text area
tempCanvasContext.clearRect(
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
window.devicePixelRatio *
zoom,
-(boundTextElement.height / 2 + BOUND_TEXT_PADDING) *
window.devicePixelRatio *
zoom,
(boundTextElement.width + BOUND_TEXT_PADDING * 2) *
window.devicePixelRatio *
zoom,
(boundTextElement.height + BOUND_TEXT_PADDING * 2) *
window.devicePixelRatio *
zoom,
);
const offsetX =
(elementWithCanvas.boundTextCanvas.width -
elementWithCanvas.canvas!.width) /
2;
const offsetY =
(elementWithCanvas.boundTextCanvas.height -
elementWithCanvas.canvas!.height) /
2;
context.translate(cx, cy);
context.drawImage(
tempCanvas,
elementWithCanvas.boundTextCanvas,
(-(x2 - x1) / 2) * window.devicePixelRatio - offsetX / zoom - padding,
(-(y2 - y1) / 2) * window.devicePixelRatio - offsetY / zoom - padding,
tempCanvas.width / zoom,
tempCanvas.height / zoom,
elementWithCanvas.boundTextCanvas.width / zoom,
elementWithCanvas.boundTextCanvas.height / zoom,
);
} else {
// we translate context to element center so that rotation and scale
@ -705,7 +726,7 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas(
element,
elementsMap,
allElementsMap,
renderConfig,
appState,
);
@ -843,7 +864,7 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas(
element,
elementsMap,
allElementsMap,
renderConfig,
appState,
);

Loading…
Cancel
Save