diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index 785788e1c..a3fc095de 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -24,6 +24,7 @@ export const actionChangeStrokeColor: Action = { return { elements: changeProperty(elements, el => ({ ...el, + shape: null, strokeColor: value })), appState: { ...appState, currentItemStrokeColor: value } @@ -50,6 +51,7 @@ export const actionChangeBackgroundColor: Action = { return { elements: changeProperty(elements, el => ({ ...el, + shape: null, backgroundColor: value })), appState: { ...appState, currentItemBackgroundColor: value } @@ -76,6 +78,7 @@ export const actionChangeFillStyle: Action = { return { elements: changeProperty(elements, el => ({ ...el, + shape: null, fillStyle: value })) }; @@ -104,6 +107,7 @@ export const actionChangeStrokeWidth: Action = { return { elements: changeProperty(elements, el => ({ ...el, + shape: null, strokeWidth: value })) }; @@ -130,6 +134,7 @@ export const actionChangeSloppiness: Action = { return { elements: changeProperty(elements, el => ({ ...el, + shape: null, roughness: value })) }; @@ -156,6 +161,7 @@ export const actionChangeOpacity: Action = { return { elements: changeProperty(elements, el => ({ ...el, + shape: null, opacity: value })) }; @@ -185,6 +191,7 @@ export const actionChangeFontSize: Action = { if (isTextElement(el)) { const element: ExcalidrawTextElement = { ...el, + shape: null, font: `${value}px ${el.font.split("px ")[1]}` }; redrawTextBoundingBox(element); @@ -223,6 +230,7 @@ export const actionChangeFontFamily: Action = { if (isTextElement(el)) { const element: ExcalidrawTextElement = { ...el, + shape: null, font: `${el.font.split("px ")[0]}px ${value}` }; redrawTextBoundingBox(element); diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index 8dd0c0580..f927c5342 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -27,6 +27,7 @@ export const actionPasteStyles: Action = { if (element.isSelected) { const newElement = { ...element, + shape: null, backgroundColor: pastedElement?.backgroundColor, strokeWidth: pastedElement?.strokeWidth, strokeColor: pastedElement?.strokeColor, diff --git a/src/element/newElement.ts b/src/element/newElement.ts index de5369dbd..4ab6c0810 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -1,5 +1,6 @@ import { randomSeed } from "../random"; import nanoid from "nanoid"; +import { Drawable } from "roughjs/bin/core"; export function newElement( type: string, @@ -28,7 +29,8 @@ export function newElement( roughness, opacity, isSelected: false, - seed: randomSeed() + seed: randomSeed(), + shape: null as Drawable | Drawable[] | null }; return element; } diff --git a/src/history.ts b/src/history.ts index 514421848..4fc44445f 100644 --- a/src/history.ts +++ b/src/history.ts @@ -7,7 +7,10 @@ class SceneHistory { generateCurrentEntry(elements: readonly ExcalidrawElement[]) { return JSON.stringify( - elements.map(element => ({ ...element, isSelected: false })) + elements.map(({ shape, ...element }) => ({ + ...element, + isSelected: false + })) ); } diff --git a/src/index.tsx b/src/index.tsx index 0a2fbdbfb..e145955e7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -279,7 +279,9 @@ export class App extends React.Component<{}, AppState> { private copyToClipboard = () => { if (navigator.clipboard) { const text = JSON.stringify( - elements.filter(element => element.isSelected) + elements + .filter(element => element.isSelected) + .map(({ shape, ...el }) => el) ); navigator.clipboard.writeText(text); } @@ -303,7 +305,11 @@ export class App extends React.Component<{}, AppState> { onCut={e => { e.clipboardData.setData( "text/plain", - JSON.stringify(elements.filter(element => element.isSelected)) + JSON.stringify( + elements + .filter(element => element.isSelected) + .map(({ shape, ...el }) => el) + ) ); elements = deleteSelectedElements(elements); this.forceUpdate(); @@ -312,7 +318,11 @@ export class App extends React.Component<{}, AppState> { onCopy={e => { e.clipboardData.setData( "text/plain", - JSON.stringify(elements.filter(element => element.isSelected)) + JSON.stringify( + elements + .filter(element => element.isSelected) + .map(({ shape, ...el }) => el) + ) ); e.preventDefault(); }} @@ -465,6 +475,7 @@ export class App extends React.Component<{}, AppState> { 1, 100 ); + type ResizeTestType = ReturnType; let resizeHandle: ResizeTestType = false; let isResizingElements = false; @@ -670,6 +681,7 @@ export class App extends React.Component<{}, AppState> { el.x = element.x; el.y = element.y; + el.shape = null; }); lastX = x; lastY = y; @@ -705,6 +717,7 @@ export class App extends React.Component<{}, AppState> { // otherwise we would read a stale one! const draggingElement = this.state.draggingElement; if (!draggingElement) return; + let width = e.clientX - CANVAS_WINDOW_OFFSET_LEFT - @@ -720,6 +733,7 @@ export class App extends React.Component<{}, AppState> { draggingElement.height = e.shiftKey ? Math.abs(width) * Math.sign(height) : height; + draggingElement.shape = null; if (this.state.elementType === "selection") { elements = setSelection(elements, draggingElement); diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 80e2ff2c9..75e4bf05a 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -4,6 +4,7 @@ import { ExcalidrawElement } from "../element/types"; import { isTextElement } from "../element/typeChecks"; import { getDiamondPoints, getArrowPoints } from "../element/bounds"; import { RoughCanvas } from "roughjs/bin/canvas"; +import { Drawable } from "roughjs/bin/core"; export function renderElement( element: ExcalidrawElement, @@ -17,69 +18,76 @@ export function renderElement( context.fillRect(0, 0, element.width, element.height); context.fillStyle = fillStyle; } else if (element.type === "rectangle") { - const shape = withCustomMathRandom(element.seed, () => { - return generator.rectangle(0, 0, element.width, element.height, { - stroke: element.strokeColor, - fill: element.backgroundColor, - fillStyle: element.fillStyle, - strokeWidth: element.strokeWidth, - roughness: element.roughness + if (!element.shape) { + element.shape = withCustomMathRandom(element.seed, () => { + return generator.rectangle(0, 0, element.width, element.height, { + stroke: element.strokeColor, + fill: element.backgroundColor, + fillStyle: element.fillStyle, + strokeWidth: element.strokeWidth, + roughness: element.roughness + }); }); - }); + } context.globalAlpha = element.opacity / 100; - rc.draw(shape); + rc.draw(element.shape as Drawable); context.globalAlpha = 1; } else if (element.type === "diamond") { - const shape = withCustomMathRandom(element.seed, () => { - const [ - topX, - topY, - rightX, - rightY, - bottomX, - bottomY, - leftX, - leftY - ] = getDiamondPoints(element); - return generator.polygon( - [ - [topX, topY], - [rightX, rightY], - [bottomX, bottomY], - [leftX, leftY] - ], - { - stroke: element.strokeColor, - fill: element.backgroundColor, - fillStyle: element.fillStyle, - strokeWidth: element.strokeWidth, - roughness: element.roughness - } - ); - }); - context.globalAlpha = element.opacity / 100; - rc.draw(shape); - context.globalAlpha = 1; - } else if (element.type === "ellipse") { - const shape = withCustomMathRandom(element.seed, () => - generator.ellipse( - element.width / 2, - element.height / 2, - element.width, - element.height, - { - stroke: element.strokeColor, - fill: element.backgroundColor, - fillStyle: element.fillStyle, - strokeWidth: element.strokeWidth, - roughness: element.roughness - } - ) - ); + if (!element.shape) { + element.shape = withCustomMathRandom(element.seed, () => { + const [ + topX, + topY, + rightX, + rightY, + bottomX, + bottomY, + leftX, + leftY + ] = getDiamondPoints(element); + return generator.polygon( + [ + [topX, topY], + [rightX, rightY], + [bottomX, bottomY], + [leftX, leftY] + ], + { + stroke: element.strokeColor, + fill: element.backgroundColor, + fillStyle: element.fillStyle, + strokeWidth: element.strokeWidth, + roughness: element.roughness + } + ); + }); + } context.globalAlpha = element.opacity / 100; - rc.draw(shape); + rc.draw(element.shape as Drawable); + context.globalAlpha = 1; + } else if (element.type === "ellipse") { + if (!element.shape) { + element.shape = withCustomMathRandom(element.seed, () => + generator.ellipse( + element.width / 2, + element.height / 2, + element.width, + element.height, + { + stroke: element.strokeColor, + fill: element.backgroundColor, + fillStyle: element.fillStyle, + strokeWidth: element.strokeWidth, + roughness: element.roughness + } + ) + ); + } + + context.globalAlpha = element.opacity / 100; + rc.draw(element.shape as Drawable); context.globalAlpha = 1; } else if (element.type === "arrow") { const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element); @@ -89,17 +97,19 @@ export function renderElement( roughness: element.roughness }; - const shapes = withCustomMathRandom(element.seed, () => [ - // \ - generator.line(x3, y3, x2, y2, options), - // ----- - generator.line(x1, y1, x2, y2, options), - // / - generator.line(x4, y4, x2, y2, options) - ]); + if (!element.shape) { + element.shape = withCustomMathRandom(element.seed, () => [ + // \ + generator.line(x3, y3, x2, y2, options), + // ----- + generator.line(x1, y1, x2, y2, options), + // / + generator.line(x4, y4, x2, y2, options) + ]); + } context.globalAlpha = element.opacity / 100; - shapes.forEach(shape => rc.draw(shape)); + (element.shape as Drawable[]).forEach(shape => rc.draw(shape)); context.globalAlpha = 1; return; } else if (isTextElement(element)) { diff --git a/src/scene/data.ts b/src/scene/data.ts index f99f88daf..9aedeb408 100644 --- a/src/scene/data.ts +++ b/src/scene/data.ts @@ -35,7 +35,7 @@ export function saveAsJSON( const serialized = JSON.stringify({ version: 1, source: window.location.origin, - elements + elements: elements.map(({ shape, ...el }) => el) }); saveFile(