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 7 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; canvas: HTMLCanvasElement;
theme: AppState["theme"]; theme: AppState["theme"];
scale: number; scale: number;
angle: number;
zoomValue: AppState["zoom"]["value"]; zoomValue: AppState["zoom"]["value"];
canvasOffsetX: number; canvasOffsetX: number;
canvasOffsetY: number; canvasOffsetY: number;
boundTextElementVersion: number | null; boundTextElementVersion: number | null;
containingFrameOpacity: number; containingFrameOpacity: number;
boundTextCanvas: HTMLCanvasElement;
} }
const cappedElementCanvasSize = ( const cappedElementCanvasSize = (
@ -182,7 +184,7 @@ const cappedElementCanvasSize = (
const generateElementCanvas = ( const generateElementCanvas = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom: Zoom, zoom: Zoom,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
@ -234,8 +236,72 @@ const generateElementCanvas = (
} }
drawElementOnCanvas(element, rc, context, renderConfig, appState); drawElementOnCanvas(element, rc, context, renderConfig, appState);
context.restore(); 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 { return {
element, element,
canvas, canvas,
@ -248,6 +314,8 @@ const generateElementCanvas = (
getBoundTextElement(element, elementsMap)?.version || null, getBoundTextElement(element, elementsMap)?.version || null,
containingFrameOpacity: containingFrameOpacity:
getContainingFrame(element, elementsMap)?.opacity || 100, getContainingFrame(element, elementsMap)?.opacity || 100,
boundTextCanvas,
angle: element.angle,
}; };
}; };
@ -423,7 +491,7 @@ export const elementWithCanvasCache = new WeakMap<
const generateElementWithCanvas = ( const generateElementWithCanvas = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap, elementsMap: NonDeletedSceneElementsMap,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
) => { ) => {
@ -433,8 +501,8 @@ const generateElementWithCanvas = (
prevElementWithCanvas && prevElementWithCanvas &&
prevElementWithCanvas.zoomValue !== zoom.value && prevElementWithCanvas.zoomValue !== zoom.value &&
!appState?.shouldCacheIgnoreZoom; !appState?.shouldCacheIgnoreZoom;
const boundTextElementVersion = const boundTextElement = getBoundTextElement(element, elementsMap);
getBoundTextElement(element, elementsMap)?.version || null; const boundTextElementVersion = boundTextElement?.version || null;
const containingFrameOpacity = const containingFrameOpacity =
getContainingFrame(element, elementsMap)?.opacity || 100; getContainingFrame(element, elementsMap)?.opacity || 100;
@ -444,7 +512,14 @@ const generateElementWithCanvas = (
shouldRegenerateBecauseZoom || shouldRegenerateBecauseZoom ||
prevElementWithCanvas.theme !== appState.theme || prevElementWithCanvas.theme !== appState.theme ||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion || 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( const elementWithCanvas = generateElementCanvas(
element, element,
@ -481,75 +556,21 @@ const drawElementFromCanvas = (
const boundTextElement = getBoundTextElement(element, allElementsMap); const boundTextElement = getBoundTextElement(element, allElementsMap);
if (isArrowElement(element) && boundTextElement) { if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas"); const offsetX =
const tempCanvasContext = tempCanvas.getContext("2d")!; (elementWithCanvas.boundTextCanvas.width -
elementWithCanvas.canvas!.width) /
// Take max dimensions of arrow canvas so that when canvas is rotated 2;
// the arrow doesn't get clipped const offsetY =
const maxDim = Math.max(distance(x1, x2), distance(y1, y2)); (elementWithCanvas.boundTextCanvas.height -
tempCanvas.width = elementWithCanvas.canvas!.height) /
maxDim * window.devicePixelRatio * zoom + 2;
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,
);
context.translate(cx, cy); context.translate(cx, cy);
context.drawImage( context.drawImage(
tempCanvas, elementWithCanvas.boundTextCanvas,
(-(x2 - x1) / 2) * window.devicePixelRatio - offsetX / zoom - padding, (-(x2 - x1) / 2) * window.devicePixelRatio - offsetX / zoom - padding,
(-(y2 - y1) / 2) * window.devicePixelRatio - offsetY / zoom - padding, (-(y2 - y1) / 2) * window.devicePixelRatio - offsetY / zoom - padding,
tempCanvas.width / zoom, elementWithCanvas.boundTextCanvas.width / zoom,
tempCanvas.height / zoom, elementWithCanvas.boundTextCanvas.height / zoom,
); );
} else { } else {
// we translate context to element center so that rotation and scale // we translate context to element center so that rotation and scale
@ -705,7 +726,7 @@ export const renderElement = (
} else { } else {
const elementWithCanvas = generateElementWithCanvas( const elementWithCanvas = generateElementWithCanvas(
element, element,
elementsMap, allElementsMap,
renderConfig, renderConfig,
appState, appState,
); );
@ -843,7 +864,7 @@ export const renderElement = (
} else { } else {
const elementWithCanvas = generateElementWithCanvas( const elementWithCanvas = generateElementWithCanvas(
element, element,
elementsMap, allElementsMap,
renderConfig, renderConfig,
appState, appState,
); );

Loading…
Cancel
Save