mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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>
This commit is contained in:
parent
43b2476dfe
commit
bd7b778f41
1 changed files with 93 additions and 72 deletions
|
@ -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…
Add table
Add a link
Reference in a new issue