mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Refactor ExcalidrawElement (#874)
* Get rid of isSelected, canvas, canvasZoom, canvasOffsetX and canvasOffsetY on ExcalidrawElement. * Fix most unit tests. Fix cmd a. Fix alt drag * Focus on paste * shift select should include previously selected items * Fix last test * Move this.shape out of ExcalidrawElement and into a WeakMap
This commit is contained in:
parent
8ecb4201db
commit
ccbbdb75a6
39 changed files with 416 additions and 306 deletions
|
@ -16,12 +16,26 @@ import rough from "roughjs/bin/rough";
|
|||
|
||||
const CANVAS_PADDING = 20;
|
||||
|
||||
function generateElementCanvas(element: ExcalidrawElement, zoom: number) {
|
||||
export interface ExcalidrawElementWithCanvas {
|
||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||
canvas: HTMLCanvasElement;
|
||||
canvasZoom: number;
|
||||
canvasOffsetX: number;
|
||||
canvasOffsetY: number;
|
||||
}
|
||||
|
||||
function generateElementCanvas(
|
||||
element: ExcalidrawElement,
|
||||
zoom: number,
|
||||
): ExcalidrawElementWithCanvas {
|
||||
const canvas = document.createElement("canvas");
|
||||
var context = canvas.getContext("2d")!;
|
||||
const context = canvas.getContext("2d")!;
|
||||
|
||||
const isLinear = /\b(arrow|line)\b/.test(element.type);
|
||||
|
||||
let canvasOffsetX = 0;
|
||||
let canvasOffsetY = 0;
|
||||
|
||||
if (isLinear) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
canvas.width =
|
||||
|
@ -29,18 +43,15 @@ function generateElementCanvas(element: ExcalidrawElement, zoom: number) {
|
|||
canvas.height =
|
||||
distance(y1, y2) * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
||||
|
||||
element.canvasOffsetX =
|
||||
canvasOffsetX =
|
||||
element.x > x1
|
||||
? Math.floor(distance(element.x, x1)) * window.devicePixelRatio
|
||||
: 0;
|
||||
element.canvasOffsetY =
|
||||
canvasOffsetY =
|
||||
element.y > y1
|
||||
? Math.floor(distance(element.y, y1)) * window.devicePixelRatio
|
||||
: 0;
|
||||
context.translate(
|
||||
element.canvasOffsetX * zoom,
|
||||
element.canvasOffsetY * zoom,
|
||||
);
|
||||
context.translate(canvasOffsetX * zoom, canvasOffsetY * zoom);
|
||||
} else {
|
||||
canvas.width =
|
||||
element.width * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
||||
|
@ -53,9 +64,8 @@ function generateElementCanvas(element: ExcalidrawElement, zoom: number) {
|
|||
|
||||
const rc = rough.canvas(canvas);
|
||||
drawElementOnCanvas(element, rc, context);
|
||||
element.canvas = canvas;
|
||||
element.canvasZoom = zoom;
|
||||
context.translate(-CANVAS_PADDING, -CANVAS_PADDING);
|
||||
return { element, canvas, canvasZoom: zoom, canvasOffsetX, canvasOffsetY };
|
||||
}
|
||||
|
||||
function drawElementOnCanvas(
|
||||
|
@ -68,12 +78,14 @@ function drawElementOnCanvas(
|
|||
case "rectangle":
|
||||
case "diamond":
|
||||
case "ellipse": {
|
||||
rc.draw(element.shape as Drawable);
|
||||
rc.draw(getShapeForElement(element) as Drawable);
|
||||
break;
|
||||
}
|
||||
case "arrow":
|
||||
case "line": {
|
||||
(element.shape as Drawable[]).forEach(shape => rc.draw(shape));
|
||||
(getShapeForElement(element) as Drawable[]).forEach(shape =>
|
||||
rc.draw(shape),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
@ -99,32 +111,44 @@ function drawElementOnCanvas(
|
|||
context.globalAlpha = 1;
|
||||
}
|
||||
|
||||
const elementWithCanvasCache = new WeakMap<
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementWithCanvas
|
||||
>();
|
||||
|
||||
const shapeCache = new WeakMap<
|
||||
ExcalidrawElement,
|
||||
Drawable | Drawable[] | null
|
||||
>();
|
||||
|
||||
export function getShapeForElement(element: ExcalidrawElement) {
|
||||
return shapeCache.get(element);
|
||||
}
|
||||
|
||||
export function invalidateShapeForElement(element: ExcalidrawElement) {
|
||||
shapeCache.delete(element);
|
||||
}
|
||||
|
||||
function generateElement(
|
||||
element: ExcalidrawElement,
|
||||
generator: RoughGenerator,
|
||||
sceneState?: SceneState,
|
||||
) {
|
||||
if (!element.shape) {
|
||||
element.canvas = null;
|
||||
let shape = shapeCache.get(element) || null;
|
||||
if (!shape) {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
element.shape = generator.rectangle(
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
{
|
||||
stroke: element.strokeColor,
|
||||
fill:
|
||||
element.backgroundColor === "transparent"
|
||||
? undefined
|
||||
: element.backgroundColor,
|
||||
fillStyle: element.fillStyle,
|
||||
strokeWidth: element.strokeWidth,
|
||||
roughness: element.roughness,
|
||||
seed: element.seed,
|
||||
},
|
||||
);
|
||||
shape = generator.rectangle(0, 0, element.width, element.height, {
|
||||
stroke: element.strokeColor,
|
||||
fill:
|
||||
element.backgroundColor === "transparent"
|
||||
? undefined
|
||||
: element.backgroundColor,
|
||||
fillStyle: element.fillStyle,
|
||||
strokeWidth: element.strokeWidth,
|
||||
roughness: element.roughness,
|
||||
seed: element.seed,
|
||||
});
|
||||
|
||||
break;
|
||||
case "diamond": {
|
||||
|
@ -138,7 +162,7 @@ function generateElement(
|
|||
leftX,
|
||||
leftY,
|
||||
] = getDiamondPoints(element);
|
||||
element.shape = generator.polygon(
|
||||
shape = generator.polygon(
|
||||
[
|
||||
[topX, topY],
|
||||
[rightX, rightY],
|
||||
|
@ -160,7 +184,7 @@ function generateElement(
|
|||
break;
|
||||
}
|
||||
case "ellipse":
|
||||
element.shape = generator.ellipse(
|
||||
shape = generator.ellipse(
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
element.width,
|
||||
|
@ -195,12 +219,12 @@ function generateElement(
|
|||
|
||||
// curve is always the first element
|
||||
// this simplifies finding the curve for an element
|
||||
element.shape = [generator.curve(points, options)];
|
||||
shape = [generator.curve(points, options)];
|
||||
|
||||
// add lines only in arrow
|
||||
if (element.type === "arrow") {
|
||||
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
||||
element.shape.push(
|
||||
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element, shape);
|
||||
shape.push(
|
||||
...[
|
||||
generator.line(x3, y3, x2, y2, options),
|
||||
generator.line(x4, y4, x2, y2, options),
|
||||
|
@ -211,19 +235,22 @@ function generateElement(
|
|||
}
|
||||
case "text": {
|
||||
// just to ensure we don't regenerate element.canvas on rerenders
|
||||
element.shape = [];
|
||||
shape = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
shapeCache.set(element, shape);
|
||||
}
|
||||
const zoom = sceneState ? sceneState.zoom : 1;
|
||||
if (!element.canvas || element.canvasZoom !== zoom) {
|
||||
generateElementCanvas(element, zoom);
|
||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||
if (!prevElementWithCanvas || prevElementWithCanvas.canvasZoom !== zoom) {
|
||||
return generateElementCanvas(element, zoom);
|
||||
}
|
||||
return prevElementWithCanvas;
|
||||
}
|
||||
|
||||
function drawElementFromCanvas(
|
||||
element: ExcalidrawElement | ExcalidrawTextElement,
|
||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
sceneState: SceneState,
|
||||
|
@ -234,17 +261,19 @@ function drawElementFromCanvas(
|
|||
-CANVAS_PADDING / sceneState.zoom,
|
||||
);
|
||||
context.drawImage(
|
||||
element.canvas!,
|
||||
elementWithCanvas.canvas!,
|
||||
Math.floor(
|
||||
-element.canvasOffsetX +
|
||||
(Math.floor(element.x) + sceneState.scrollX) * window.devicePixelRatio,
|
||||
-elementWithCanvas.canvasOffsetX +
|
||||
(Math.floor(elementWithCanvas.element.x) + sceneState.scrollX) *
|
||||
window.devicePixelRatio,
|
||||
),
|
||||
Math.floor(
|
||||
-element.canvasOffsetY +
|
||||
(Math.floor(element.y) + sceneState.scrollY) * window.devicePixelRatio,
|
||||
-elementWithCanvas.canvasOffsetY +
|
||||
(Math.floor(elementWithCanvas.element.y) + sceneState.scrollY) *
|
||||
window.devicePixelRatio,
|
||||
),
|
||||
element.canvas!.width / sceneState.zoom,
|
||||
element.canvas!.height / sceneState.zoom,
|
||||
elementWithCanvas.canvas!.width / sceneState.zoom,
|
||||
elementWithCanvas.canvas!.height / sceneState.zoom,
|
||||
);
|
||||
context.translate(
|
||||
CANVAS_PADDING / sceneState.zoom,
|
||||
|
@ -279,10 +308,10 @@ export function renderElement(
|
|||
case "line":
|
||||
case "arrow":
|
||||
case "text": {
|
||||
generateElement(element, generator, sceneState);
|
||||
const elementWithCanvas = generateElement(element, generator, sceneState);
|
||||
|
||||
if (renderOptimizations) {
|
||||
drawElementFromCanvas(element, rc, context, sceneState);
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
|
||||
} else {
|
||||
const offsetX = Math.floor(element.x + sceneState.scrollX);
|
||||
const offsetY = Math.floor(element.y + sceneState.scrollY);
|
||||
|
@ -316,7 +345,7 @@ export function renderElementToSvg(
|
|||
case "diamond":
|
||||
case "ellipse": {
|
||||
generateElement(element, generator);
|
||||
const node = rsvg.draw(element.shape as Drawable);
|
||||
const node = rsvg.draw(getShapeForElement(element) as Drawable);
|
||||
const opacity = element.opacity / 100;
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
|
@ -334,7 +363,7 @@ export function renderElementToSvg(
|
|||
generateElement(element, generator);
|
||||
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||
const opacity = element.opacity / 100;
|
||||
(element.shape as Drawable[]).forEach(shape => {
|
||||
(getShapeForElement(element) as Drawable[]).forEach(shape => {
|
||||
const node = rsvg.draw(shape);
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue