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;
|
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…
Add table
Add a link
Reference in a new issue