From e66bf1fe6dbe6f96158a81bc3c531f572522ec35 Mon Sep 17 00:00:00 2001 From: Gasim Gasimzada Date: Mon, 6 Jan 2020 12:04:07 +0400 Subject: [PATCH] Move math and random files into their respective modules - Move distanceBetweenPointAndSegment to math module - Move LCG, randomSeed, and withCustomMathRandom to random module --- src/index.tsx | 918 +------------------------------------------------- src/math.ts | 38 +++ src/random.ts | 18 + 3 files changed, 65 insertions(+), 909 deletions(-) create mode 100644 src/math.ts create mode 100644 src/random.ts diff --git a/src/index.tsx b/src/index.tsx index fc62e081e..44eaf88dd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,10 @@ import { RoughCanvas } from "roughjs/bin/canvas"; import { TwitterPicker } from "react-color"; import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex"; +import { LCG, randomSeed, withCustomMathRandom } from "./random"; +import { distanceBetweenPointAndSegment } from "./math"; import { roundRect } from "./roundRect"; + import EditableText from "./components/EditableText"; import "./styles.scss"; @@ -25,94 +28,6 @@ const elements = Array.of(); const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`; -let skipHistory = false; -const stateHistory: string[] = []; -const redoStack: string[] = []; - -function generateHistoryCurrentEntry() { - return JSON.stringify( - elements.map(element => ({ ...element, isSelected: false })) - ); -} -function pushHistoryEntry(newEntry: string) { - if ( - stateHistory.length > 0 && - stateHistory[stateHistory.length - 1] === newEntry - ) { - // If the last entry is the same as this one, ignore it - return; - } - stateHistory.push(newEntry); -} -function restoreHistoryEntry(entry: string) { - const newElements = JSON.parse(entry); - elements.splice(0, elements.length); - newElements.forEach((newElement: ExcalidrawElement) => { - generateDraw(newElement); - elements.push(newElement); - }); - // When restoring, we shouldn't add an history entry otherwise we'll be stuck with it and can't go back - skipHistory = true; -} - -// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript/47593316#47593316 -const LCG = (seed: number) => () => - ((2 ** 31 - 1) & (seed = Math.imul(48271, seed))) / 2 ** 31; - -function randomSeed() { - return Math.floor(Math.random() * 2 ** 31); -} - -// Unfortunately, roughjs doesn't support a seed attribute (https://github.com/pshihn/rough/issues/27). -// We can achieve the same result by overriding the Math.random function with a -// pseudo random generator that supports a random seed and swapping it back after. -function withCustomMathRandom(seed: number, cb: () => T): T { - const random = Math.random; - Math.random = LCG(seed); - const result = cb(); - Math.random = random; - return result; -} - -// https://stackoverflow.com/a/6853926/232122 -function distanceBetweenPointAndSegment( - x: number, - y: number, - x1: number, - y1: number, - x2: number, - y2: number -) { - const A = x - x1; - const B = y - y1; - const C = x2 - x1; - const D = y2 - y1; - - const dot = A * C + B * D; - const lenSquare = C * C + D * D; - let param = -1; - if (lenSquare !== 0) { - // in case of 0 length line - param = dot / lenSquare; - } - - let xx, yy; - if (param < 0) { - xx = x1; - yy = y1; - } else if (param > 1) { - xx = x2; - yy = y2; - } else { - xx = x1 + param * C; - yy = y1 + param * D; - } - - const dx = x - xx; - const dy = y - yy; - return Math.hypot(dx, dy); -} - function hitTest(element: ExcalidrawElement, x: number, y: number): boolean { // For shapes that are composed of lines, we only enable point-selection when the distance // of the click is less than x pixels of any of the lines that the shape is composed of @@ -797,7 +712,12 @@ function generateDraw(element: ExcalidrawElement) { leftY ] = getDiamondPoints(element); return generator.polygon( - [[topX, topY], [rightX, rightY], [bottomX, bottomY], [leftX, leftY]], + [ + [topX, topY], + [rightX, rightY], + [bottomX, bottomY], + [leftX, leftY] + ], { stroke: element.strokeColor, fill: element.backgroundColor, @@ -1253,823 +1173,3 @@ let lastCanvasWidth = -1; let lastCanvasHeight = -1; let lastMouseUp: ((e: any) => void) | null = null; - -class App extends React.Component<{}, AppState> { - public componentDidMount() { - document.addEventListener("keydown", this.onKeyDown, false); - window.addEventListener("resize", this.onResize, false); - - const savedState = restoreFromLocalStorage(); - if (savedState) { - this.setState(savedState); - } - } - - public componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown, false); - window.removeEventListener("resize", this.onResize, false); - } - - public state: AppState = { - draggingElement: null, - resizingElement: null, - elementType: "selection", - exportBackground: true, - currentItemStrokeColor: "#000000", - currentItemBackgroundColor: "#ffffff", - viewBackgroundColor: "#ffffff", - scrollX: 0, - scrollY: 0, - name: DEFAULT_PROJECT_NAME - }; - - private onResize = () => { - this.forceUpdate(); - }; - - private onKeyDown = (event: KeyboardEvent) => { - if (isInputLike(event.target)) return; - - if (event.key === KEYS.ESCAPE) { - clearSelection(); - this.forceUpdate(); - event.preventDefault(); - } else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) { - deleteSelectedElements(); - this.forceUpdate(); - event.preventDefault(); - } else if (isArrowKey(event.key)) { - const step = event.shiftKey - ? ELEMENT_SHIFT_TRANSLATE_AMOUNT - : ELEMENT_TRANSLATE_AMOUNT; - elements.forEach(element => { - if (element.isSelected) { - if (event.key === KEYS.ARROW_LEFT) element.x -= step; - else if (event.key === KEYS.ARROW_RIGHT) element.x += step; - else if (event.key === KEYS.ARROW_UP) element.y -= step; - else if (event.key === KEYS.ARROW_DOWN) element.y += step; - } - }); - this.forceUpdate(); - event.preventDefault(); - - // Send backward: Cmd-Shift-Alt-B - } else if ( - event.metaKey && - event.shiftKey && - event.altKey && - event.code === "KeyB" - ) { - this.moveOneLeft(); - event.preventDefault(); - - // Send to back: Cmd-Shift-B - } else if (event.metaKey && event.shiftKey && event.code === "KeyB") { - this.moveAllLeft(); - event.preventDefault(); - - // Bring forward: Cmd-Shift-Alt-F - } else if ( - event.metaKey && - event.shiftKey && - event.altKey && - event.code === "KeyF" - ) { - this.moveOneRight(); - event.preventDefault(); - - // Bring to front: Cmd-Shift-F - } else if (event.metaKey && event.shiftKey && event.code === "KeyF") { - this.moveAllRight(); - event.preventDefault(); - - // Select all: Cmd-A - } else if (event.metaKey && event.code === "KeyA") { - elements.forEach(element => { - element.isSelected = true; - }); - this.forceUpdate(); - event.preventDefault(); - } else if (shapesShortcutKeys.includes(event.key.toLowerCase())) { - this.setState({ elementType: findElementByKey(event.key) }); - } else if (event.metaKey && event.code === "KeyZ") { - const currentEntry = generateHistoryCurrentEntry(); - if (event.shiftKey) { - // Redo action - const entryToRestore = redoStack.pop(); - if (entryToRestore !== undefined) { - restoreHistoryEntry(entryToRestore); - stateHistory.push(currentEntry); - } - } else { - // undo action - let lastEntry = stateHistory.pop(); - // If nothing was changed since last, take the previous one - if (currentEntry === lastEntry) { - lastEntry = stateHistory.pop(); - } - if (lastEntry !== undefined) { - restoreHistoryEntry(lastEntry); - redoStack.push(currentEntry); - } - } - this.forceUpdate(); - event.preventDefault(); - } - }; - - private deleteSelectedElements = () => { - deleteSelectedElements(); - this.forceUpdate(); - }; - - private clearCanvas = () => { - if (window.confirm("This will clear the whole canvas. Are you sure?")) { - elements.splice(0, elements.length); - this.setState({ - viewBackgroundColor: "#ffffff", - scrollX: 0, - scrollY: 0 - }); - this.forceUpdate(); - } - }; - - private moveAllLeft = () => { - moveAllLeft(elements, getSelectedIndices()); - this.forceUpdate(); - }; - - private moveOneLeft = () => { - moveOneLeft(elements, getSelectedIndices()); - this.forceUpdate(); - }; - - private moveAllRight = () => { - moveAllRight(elements, getSelectedIndices()); - this.forceUpdate(); - }; - - private moveOneRight = () => { - moveOneRight(elements, getSelectedIndices()); - this.forceUpdate(); - }; - - private removeWheelEventListener: (() => void) | undefined; - - private updateProjectName(name: string): void { - this.setState({ name }); - } - - private changeProperty = (callback: (element: ExcalidrawElement) => void) => { - elements.forEach(element => { - if (element.isSelected) { - callback(element); - generateDraw(element); - } - }); - - this.forceUpdate(); - }; - - private changeOpacity = (event: React.ChangeEvent) => { - this.changeProperty(element => (element.opacity = +event.target.value)); - }; - - private changeStrokeColor = (color: string) => { - this.changeProperty(element => (element.strokeColor = color)); - this.setState({ currentItemStrokeColor: color }); - }; - - private changeBackgroundColor = (color: string) => { - this.changeProperty(element => (element.backgroundColor = color)); - this.setState({ currentItemBackgroundColor: color }); - }; - - public render() { - const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT; - const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP; - - return ( -
{ - e.clipboardData.setData( - "text/plain", - JSON.stringify(elements.filter(element => element.isSelected)) - ); - deleteSelectedElements(); - this.forceUpdate(); - e.preventDefault(); - }} - onCopy={e => { - e.clipboardData.setData( - "text/plain", - JSON.stringify(elements.filter(element => element.isSelected)) - ); - e.preventDefault(); - }} - onPaste={e => { - const paste = e.clipboardData.getData("text"); - let parsedElements; - try { - parsedElements = JSON.parse(paste); - } catch (e) {} - if ( - Array.isArray(parsedElements) && - parsedElements.length > 0 && - parsedElements[0].type // need to implement a better check here... - ) { - clearSelection(); - parsedElements.forEach(parsedElement => { - parsedElement.x += 10; - parsedElement.y += 10; - parsedElement.seed = randomSeed(); - generateDraw(parsedElement); - elements.push(parsedElement); - }); - this.forceUpdate(); - } - e.preventDefault(); - }} - > -
-

Shapes

-
- {SHAPES.map(({ value, icon }) => ( - - ))} -
- {someElementIsSelected() && ( -
-

Selection

-
- - - - -
-
Stroke Color
- element.strokeColor)} - onChange={color => this.changeStrokeColor(color)} - /> - - {hasBackground() && ( - <> -
Background Color
- element.backgroundColor - )} - onChange={color => this.changeBackgroundColor(color)} - /> -
Fill
- element.fillStyle)} - onChange={value => { - this.changeProperty(element => { - element.fillStyle = value; - }); - }} - /> - - )} - - {hasStroke() && ( - <> -
Stroke Width
- element.strokeWidth)} - onChange={value => { - this.changeProperty(element => { - element.strokeWidth = value; - }); - }} - /> - -
Sloppiness
- element.roughness)} - onChange={value => - this.changeProperty(element => { - element.roughness = value; - }) - } - /> - - )} - -
Opacity
- element.opacity) || - 0 /* Put the opacity at 0 if there are two conflicting ones */ - } - /> - - -
- )} -

Canvas

-
-
Canvas Background Color
- this.setState({ viewBackgroundColor: color })} - /> - -
-

Export

-
-
Name
- {this.state.name && ( - this.updateProjectName(name)} - /> - )} -
Image
- - -
Scene
- - -
-
- { - if (this.removeWheelEventListener) { - this.removeWheelEventListener(); - this.removeWheelEventListener = undefined; - } - if (canvas) { - canvas.addEventListener("wheel", this.handleWheel, { - passive: false - }); - this.removeWheelEventListener = () => - canvas.removeEventListener("wheel", this.handleWheel); - - // Whenever React sets the width/height of the canvas element, - // the context loses the scale transform. We need to re-apply it - if ( - canvasWidth !== lastCanvasWidth || - canvasHeight !== lastCanvasHeight - ) { - lastCanvasWidth = canvasWidth; - lastCanvasHeight = canvasHeight; - canvas - .getContext("2d")! - .scale(window.devicePixelRatio, window.devicePixelRatio); - } - } - }} - onMouseDown={e => { - if (lastMouseUp !== null) { - // Unfortunately, sometimes we don't get a mouseup after a mousedown, - // this can happen when a contextual menu or alert is triggered. In order to avoid - // being in a weird state, we clean up on the next mousedown - lastMouseUp(e); - } - // only handle left mouse button - if (e.button !== 0) return; - // fixes mousemove causing selection of UI texts #32 - e.preventDefault(); - // Preventing the event above disables default behavior - // of defocusing potentially focused input, which is what we want - // when clicking inside the canvas. - if (isInputLike(document.activeElement)) { - document.activeElement.blur(); - } - - // Handle scrollbars dragging - const { - isOverHorizontalScrollBar, - isOverVerticalScrollBar - } = isOverScrollBars( - e.clientX - CANVAS_WINDOW_OFFSET_LEFT, - e.clientY - CANVAS_WINDOW_OFFSET_TOP, - canvasWidth, - canvasHeight, - this.state.scrollX, - this.state.scrollY - ); - - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; - const element = newElement( - this.state.elementType, - x, - y, - this.state.currentItemStrokeColor, - this.state.currentItemBackgroundColor, - "hachure", - 1, - 1, - 100 - ); - let resizeHandle: string | false = false; - let isDraggingElements = false; - let isResizingElements = false; - if (this.state.elementType === "selection") { - const resizeElement = elements.find(element => { - return resizeTest(element, x, y, { - scrollX: this.state.scrollX, - scrollY: this.state.scrollY, - viewBackgroundColor: this.state.viewBackgroundColor - }); - }); - - this.setState({ - resizingElement: resizeElement ? resizeElement : null - }); - - if (resizeElement) { - resizeHandle = resizeTest(resizeElement, x, y, { - scrollX: this.state.scrollX, - scrollY: this.state.scrollY, - viewBackgroundColor: this.state.viewBackgroundColor - }); - document.documentElement.style.cursor = `${resizeHandle}-resize`; - isResizingElements = true; - } else { - const hitElement = getElementAtPosition(x, y); - - // If we click on something - if (hitElement) { - if (hitElement.isSelected) { - // If that element is not already selected, do nothing, - // we're likely going to drag it - } else { - // We unselect every other elements unless shift is pressed - if (!e.shiftKey) { - clearSelection(); - } - // No matter what, we select it - hitElement.isSelected = true; - } - } else { - // If we don't click on anything, let's remove all the selected elements - clearSelection(); - } - - isDraggingElements = someElementIsSelected(); - - if (isDraggingElements) { - document.documentElement.style.cursor = "move"; - } - } - } - - if (isTextElement(element)) { - if (!addTextElement(element)) { - return; - } - } - - generateDraw(element); - elements.push(element); - if (this.state.elementType === "text") { - this.setState({ - draggingElement: null, - elementType: "selection" - }); - element.isSelected = true; - } else { - this.setState({ draggingElement: element }); - } - - let lastX = x; - let lastY = y; - - if (isOverHorizontalScrollBar || isOverVerticalScrollBar) { - lastX = e.clientX - CANVAS_WINDOW_OFFSET_LEFT; - lastY = e.clientY - CANVAS_WINDOW_OFFSET_TOP; - } - - const onMouseMove = (e: MouseEvent) => { - const target = e.target; - if (!(target instanceof HTMLElement)) { - return; - } - - if (isOverHorizontalScrollBar) { - const x = e.clientX - CANVAS_WINDOW_OFFSET_LEFT; - const dx = x - lastX; - this.setState(state => ({ scrollX: state.scrollX - dx })); - lastX = x; - return; - } - - if (isOverVerticalScrollBar) { - const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP; - const dy = y - lastY; - this.setState(state => ({ scrollY: state.scrollY - dy })); - lastY = y; - return; - } - - if (isResizingElements && this.state.resizingElement) { - const el = this.state.resizingElement; - const selectedElements = elements.filter(el => el.isSelected); - if (selectedElements.length === 1) { - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = - e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; - selectedElements.forEach(element => { - switch (resizeHandle) { - case "nw": - element.width += element.x - lastX; - element.height += element.y - lastY; - element.x = lastX; - element.y = lastY; - break; - case "ne": - element.width = lastX - element.x; - element.height += element.y - lastY; - element.y = lastY; - break; - case "sw": - element.width += element.x - lastX; - element.x = lastX; - element.height = lastY - element.y; - break; - case "se": - element.width += x - lastX; - if (e.shiftKey) { - element.height = element.width; - } else { - element.height += y - lastY; - } - break; - case "n": - element.height += element.y - lastY; - element.y = lastY; - break; - case "w": - element.width += element.x - lastX; - element.x = lastX; - break; - case "s": - element.height = lastY - element.y; - break; - case "e": - element.width = lastX - element.x; - break; - } - - el.x = element.x; - el.y = element.y; - generateDraw(el); - }); - lastX = x; - lastY = y; - // We don't want to save history when resizing an element - skipHistory = true; - this.forceUpdate(); - return; - } - } - - if (isDraggingElements) { - const selectedElements = elements.filter(el => el.isSelected); - if (selectedElements.length) { - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = - e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; - selectedElements.forEach(element => { - element.x += x - lastX; - element.y += y - lastY; - }); - lastX = x; - lastY = y; - // We don't want to save history when dragging an element to initially size it - skipHistory = true; - this.forceUpdate(); - return; - } - } - - // It is very important to read this.state within each move event, - // otherwise we would read a stale one! - const draggingElement = this.state.draggingElement; - if (!draggingElement) return; - let width = - e.clientX - - CANVAS_WINDOW_OFFSET_LEFT - - draggingElement.x - - this.state.scrollX; - let height = - e.clientY - - CANVAS_WINDOW_OFFSET_TOP - - draggingElement.y - - this.state.scrollY; - draggingElement.width = width; - // Make a perfect square or circle when shift is enabled - draggingElement.height = e.shiftKey - ? Math.abs(width) * Math.sign(height) - : height; - - generateDraw(draggingElement); - - if (this.state.elementType === "selection") { - setSelection(draggingElement); - } - // We don't want to save history when moving an element - skipHistory = true; - this.forceUpdate(); - }; - - const onMouseUp = (e: MouseEvent) => { - const { draggingElement, elementType } = this.state; - - lastMouseUp = null; - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("mouseup", onMouseUp); - - resetCursor(); - - // if no element is clicked, clear the selection and redraw - if (draggingElement === null) { - clearSelection(); - this.forceUpdate(); - return; - } - - if (elementType === "selection") { - if (isDraggingElements) { - isDraggingElements = false; - } - elements.pop(); - } else { - draggingElement.isSelected = true; - } - - this.setState({ - draggingElement: null, - elementType: "selection" - }); - this.forceUpdate(); - }; - - lastMouseUp = onMouseUp; - - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("mouseup", onMouseUp); - - // We don't want to save history on mouseDown, only on mouseUp when it's fully configured - skipHistory = true; - this.forceUpdate(); - }} - onDoubleClick={e => { - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; - - if (getElementAtPosition(x, y)) { - return; - } - - const element = newElement( - "text", - x, - y, - this.state.currentItemStrokeColor, - this.state.currentItemBackgroundColor, - "hachure", - 1, - 1, - 100 - ); - - if (!addTextElement(element as ExcalidrawTextElement)) { - return; - } - - generateDraw(element); - elements.push(element); - - this.setState({ - draggingElement: null, - elementType: "selection" - }); - element.isSelected = true; - - this.forceUpdate(); - }} - /> -
- ); - } - - private handleWheel = (e: WheelEvent) => { - e.preventDefault(); - const { deltaX, deltaY } = e; - this.setState(state => ({ - scrollX: state.scrollX - deltaX, - scrollY: state.scrollY - deltaY - })); - }; - - componentDidUpdate() { - renderScene(rc, canvas, { - scrollX: this.state.scrollX, - scrollY: this.state.scrollY, - viewBackgroundColor: this.state.viewBackgroundColor - }); - save(this.state); - if (!skipHistory) { - pushHistoryEntry(generateHistoryCurrentEntry()); - redoStack.splice(0, redoStack.length); - } - skipHistory = false; - } -} - -const rootElement = document.getElementById("root"); -ReactDOM.render(, rootElement); -const canvas = document.getElementById("canvas") as HTMLCanvasElement; -const rc = rough.canvas(canvas); -const context = canvas.getContext("2d")!; - -ReactDOM.render(, rootElement); diff --git a/src/math.ts b/src/math.ts new file mode 100644 index 000000000..f699fea34 --- /dev/null +++ b/src/math.ts @@ -0,0 +1,38 @@ +// https://stackoverflow.com/a/6853926/232122 +export function distanceBetweenPointAndSegment( + x: number, + y: number, + x1: number, + y1: number, + x2: number, + y2: number +) { + const A = x - x1; + const B = y - y1; + const C = x2 - x1; + const D = y2 - y1; + + const dot = A * C + B * D; + const lenSquare = C * C + D * D; + let param = -1; + if (lenSquare !== 0) { + // in case of 0 length line + param = dot / lenSquare; + } + + let xx, yy; + if (param < 0) { + xx = x1; + yy = y1; + } else if (param > 1) { + xx = x2; + yy = y2; + } else { + xx = x1 + param * C; + yy = y1 + param * D; + } + + const dx = x - xx; + const dy = y - yy; + return Math.hypot(dx, dy); +} diff --git a/src/random.ts b/src/random.ts new file mode 100644 index 000000000..9147c80c4 --- /dev/null +++ b/src/random.ts @@ -0,0 +1,18 @@ +// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript/47593316#47593316 +export const LCG = (seed: number) => () => + ((2 ** 31 - 1) & (seed = Math.imul(48271, seed))) / 2 ** 31; + +export function randomSeed() { + return Math.floor(Math.random() * 2 ** 31); +} + +// Unfortunately, roughjs doesn't support a seed attribute (https://github.com/pshihn/rough/issues/27). +// We can achieve the same result by overriding the Math.random function with a +// pseudo random generator that supports a random seed and swapping it back after. +export function withCustomMathRandom(seed: number, cb: () => T): T { + const random = Math.random; + Math.random = LCG(seed); + const result = cb(); + Math.random = random; + return result; +}