|
@@ -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,
|
|
|
);
|