Add react-color

This commit is contained in:
Jared Palmer 2020-01-05 11:52:45 -08:00
parent 91bc9df37a
commit de114b9f59
4 changed files with 278 additions and 165 deletions

45
package-lock.json generated
View file

@ -1034,6 +1034,11 @@
"@hapi/hoek": "^8.3.0" "@hapi/hoek": "^8.3.0"
} }
}, },
"@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
},
"@jest/console": { "@jest/console": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz",
@ -1511,6 +1516,15 @@
"csstype": "^2.2.0" "csstype": "^2.2.0"
} }
}, },
"@types/react-color": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.1.tgz",
"integrity": "sha512-J6mYm43Sid9y+OjZ7NDfJ2VVkeeuTPNVImNFITgQNXodHteKfl/t/5pAR5Z9buodZ2tCctsZjgiMlQOpfntakw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": { "@types/react-dom": {
"version": "16.9.4", "version": "16.9.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz",
@ -9369,6 +9383,11 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -12094,6 +12113,19 @@
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0"
} }
}, },
"react-color": {
"version": "2.17.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.17.3.tgz",
"integrity": "sha512-1dtO8LqAVotPIChlmo6kLtFS1FP89ll8/OiA8EcFRDR+ntcK+0ukJgByuIQHRtzvigf26dV5HklnxDIvhON9VQ==",
"requires": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.11",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
}
},
"react-dev-utils": { "react-dev-utils": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.0.0.tgz",
@ -12315,6 +12347,14 @@
"workbox-webpack-plugin": "4.3.1" "workbox-webpack-plugin": "4.3.1"
} }
}, },
"reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
"requires": {
"lodash": "^4.0.1"
}
},
"read-pkg": { "read-pkg": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@ -14397,6 +14437,11 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
}, },
"tinycolor2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View file

@ -7,12 +7,14 @@
"main": "src/index.js", "main": "src/index.js",
"dependencies": { "dependencies": {
"react": "16.12.0", "react": "16.12.0",
"react-color": "^2.17.3",
"react-dom": "16.12.0", "react-dom": "16.12.0",
"react-scripts": "3.3.0", "react-scripts": "3.3.0",
"roughjs": "3.1.0" "roughjs": "3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "16.9.17", "@types/react": "16.9.17",
"@types/react-color": "^3.0.1",
"@types/react-dom": "16.9.4", "@types/react-dom": "16.9.4",
"husky": "3.1.0", "husky": "3.1.0",
"lint-staged": "9.5.0", "lint-staged": "9.5.0",

View file

@ -1,22 +1,23 @@
import React from "react"; import React from 'react';
import ReactDOM from "react-dom"; import ReactDOM from 'react-dom';
import rough from "roughjs/bin/wrappers/rough"; import rough from 'roughjs/bin/wrappers/rough';
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from 'roughjs/bin/canvas';
import { SketchPicker } from 'react-color';
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex"; import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from './zindex';
import "./styles.scss"; import './styles.scss';
type ExcalidrawElement = ReturnType<typeof newElement>; type ExcalidrawElement = ReturnType<typeof newElement>;
type ExcalidrawTextElement = ExcalidrawElement & { type ExcalidrawTextElement = ExcalidrawElement & {
type: "text"; type: 'text';
font: string; font: string;
text: string; text: string;
actualBoundingBoxAscent: number; actualBoundingBoxAscent: number;
}; };
const LOCAL_STORAGE_KEY = "excalidraw"; const LOCAL_STORAGE_KEY = 'excalidraw';
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state"; const LOCAL_STORAGE_KEY_STATE = 'excalidraw-state';
const elements = Array.of<ExcalidrawElement>(); const elements = Array.of<ExcalidrawElement>();
@ -111,7 +112,7 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
// of the click is less than x pixels of any of the lines that the shape is composed of // of the click is less than x pixels of any of the lines that the shape is composed of
const lineThreshold = 10; const lineThreshold = 10;
if (element.type === "ellipse") { if (element.type === 'ellipse') {
// https://stackoverflow.com/a/46007540/232122 // https://stackoverflow.com/a/46007540/232122
const px = Math.abs(x - element.x - element.width / 2); const px = Math.abs(x - element.x - element.width / 2);
const py = Math.abs(y - element.y - element.height / 2); const py = Math.abs(y - element.y - element.height / 2);
@ -146,7 +147,7 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
}); });
return Math.hypot(a * tx - px, b * ty - py) < lineThreshold; return Math.hypot(a * tx - px, b * ty - py) < lineThreshold;
} else if (element.type === "rectangle") { } else if (element.type === 'rectangle') {
const x1 = getElementAbsoluteX1(element); const x1 = getElementAbsoluteX1(element);
const x2 = getElementAbsoluteX2(element); const x2 = getElementAbsoluteX2(element);
const y1 = getElementAbsoluteY1(element); const y1 = getElementAbsoluteY1(element);
@ -161,7 +162,7 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
distanceBetweenPointAndSegment(x, y, x2, y2, x1, y2) < lineThreshold || // C distanceBetweenPointAndSegment(x, y, x2, y2, x1, y2) < lineThreshold || // C
distanceBetweenPointAndSegment(x, y, x1, y2, x1, y1) < lineThreshold // D distanceBetweenPointAndSegment(x, y, x1, y2, x1, y1) < lineThreshold // D
); );
} else if (element.type === "arrow") { } else if (element.type === 'arrow') {
let [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element); let [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
// The computation is done at the origin, we need to add a translation // The computation is done at the origin, we need to add a translation
x -= element.x; x -= element.x;
@ -175,18 +176,18 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
// / // /
distanceBetweenPointAndSegment(x, y, x4, y4, x2, y2) < lineThreshold distanceBetweenPointAndSegment(x, y, x4, y4, x2, y2) < lineThreshold
); );
} else if (element.type === "text") { } else if (element.type === 'text') {
const x1 = getElementAbsoluteX1(element); const x1 = getElementAbsoluteX1(element);
const x2 = getElementAbsoluteX2(element); const x2 = getElementAbsoluteX2(element);
const y1 = getElementAbsoluteY1(element); const y1 = getElementAbsoluteY1(element);
const y2 = getElementAbsoluteY2(element); const y2 = getElementAbsoluteY2(element);
return x >= x1 && x <= x2 && y >= y1 && y <= y2; return x >= x1 && x <= x2 && y >= y1 && y <= y2;
} else if (element.type === "selection") { } else if (element.type === 'selection') {
console.warn("This should not happen, we need to investigate why it does."); console.warn('This should not happen, we need to investigate why it does.');
return false; return false;
} else { } else {
throw new Error("Unimplemented type " + element.type); throw new Error('Unimplemented type ' + element.type);
} }
} }
@ -196,7 +197,7 @@ function resizeTest(
y: number, y: number,
sceneState: SceneState sceneState: SceneState
): string | false { ): string | false {
if (element.type === "text" || element.type === "arrow") return false; if (element.type === 'text' || element.type === 'arrow') return false;
const handlers = handlerRectangles(element, sceneState); const handlers = handlerRectangles(element, sceneState);
@ -241,7 +242,7 @@ function newElement(
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
sceneState: SceneState sceneState: SceneState
) {} ) {},
}; };
return element; return element;
} }
@ -255,7 +256,7 @@ type SceneState = {
const SCROLLBAR_WIDTH = 6; const SCROLLBAR_WIDTH = 6;
const SCROLLBAR_MARGIN = 4; const SCROLLBAR_MARGIN = 4;
const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)"; const SCROLLBAR_COLOR = 'rgba(0,0,0,0.3)';
const CANVAS_WINDOW_OFFSET_LEFT = 250; const CANVAS_WINDOW_OFFSET_LEFT = 250;
const CANVAS_WINDOW_OFFSET_TOP = 0; const CANVAS_WINDOW_OFFSET_TOP = 0;
@ -273,7 +274,7 @@ function getScrollbars(
x: scrollBarX + SCROLLBAR_MARGIN, x: scrollBarX + SCROLLBAR_MARGIN,
y: canvasHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN, y: canvasHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
width: scrollBarWidth - SCROLLBAR_MARGIN * 2, width: scrollBarWidth - SCROLLBAR_MARGIN * 2,
height: SCROLLBAR_WIDTH height: SCROLLBAR_WIDTH,
}; };
// vertical scrollbar // vertical scrollbar
@ -284,12 +285,12 @@ function getScrollbars(
x: canvasWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN, x: canvasWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
y: scrollBarY + SCROLLBAR_MARGIN, y: scrollBarY + SCROLLBAR_MARGIN,
width: SCROLLBAR_WIDTH, width: SCROLLBAR_WIDTH,
height: scrollBarHeight - SCROLLBAR_WIDTH * 2 height: scrollBarHeight - SCROLLBAR_WIDTH * 2,
}; };
return { return {
horizontal: horizontalScrollBar, horizontal: horizontalScrollBar,
vertical: verticalScrollBar vertical: verticalScrollBar,
}; };
} }
@ -305,7 +306,7 @@ function isOverScrollBars(
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [ const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal, scrollBars.horizontal,
scrollBars.vertical scrollBars.vertical,
].map( ].map(
scrollBar => scrollBar =>
scrollBar.x <= x && scrollBar.x <= x &&
@ -316,7 +317,7 @@ function isOverScrollBars(
return { return {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar isOverVerticalScrollBar,
}; };
} }
@ -334,60 +335,60 @@ function handlerRectangles(element: ExcalidrawElement, sceneState: SceneState) {
const marginY = element.height < 0 ? 8 : -8; const marginY = element.height < 0 ? 8 : -8;
if (Math.abs(elementX2 - elementX1) > minimumSize) { if (Math.abs(elementX2 - elementX1) > minimumSize) {
handlers["n"] = [ handlers['n'] = [
elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4, elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
elementY1 - margin + sceneState.scrollY + marginY, elementY1 - margin + sceneState.scrollY + marginY,
8, 8,
8 8,
]; ];
handlers["s"] = [ handlers['s'] = [
elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4, elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
elementY2 - margin + sceneState.scrollY - marginY, elementY2 - margin + sceneState.scrollY - marginY,
8, 8,
8 8,
]; ];
} }
if (Math.abs(elementY2 - elementY1) > minimumSize) { if (Math.abs(elementY2 - elementY1) > minimumSize) {
handlers["w"] = [ handlers['w'] = [
elementX1 - margin + sceneState.scrollX + marginX, elementX1 - margin + sceneState.scrollX + marginX,
elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4, elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
8, 8,
8 8,
]; ];
handlers["e"] = [ handlers['e'] = [
elementX2 - margin + sceneState.scrollX - marginX, elementX2 - margin + sceneState.scrollX - marginX,
elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4, elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
8, 8,
8 8,
]; ];
} }
handlers["nw"] = [ handlers['nw'] = [
elementX1 - margin + sceneState.scrollX + marginX, elementX1 - margin + sceneState.scrollX + marginX,
elementY1 - margin + sceneState.scrollY + marginY, elementY1 - margin + sceneState.scrollY + marginY,
8, 8,
8 8,
]; // nw ]; // nw
handlers["ne"] = [ handlers['ne'] = [
elementX2 - margin + sceneState.scrollX - marginX, elementX2 - margin + sceneState.scrollX - marginX,
elementY1 - margin + sceneState.scrollY + marginY, elementY1 - margin + sceneState.scrollY + marginY,
8, 8,
8 8,
]; // ne ]; // ne
handlers["sw"] = [ handlers['sw'] = [
elementX1 - margin + sceneState.scrollX + marginX, elementX1 - margin + sceneState.scrollX + marginX,
elementY2 - margin + sceneState.scrollY - marginY, elementY2 - margin + sceneState.scrollY - marginY,
8, 8,
8 8,
]; // sw ]; // sw
handlers["se"] = [ handlers['se'] = [
elementX2 - margin + sceneState.scrollX - marginX, elementX2 - margin + sceneState.scrollX - marginX,
elementY2 - margin + sceneState.scrollY - marginY, elementY2 - margin + sceneState.scrollY - marginY,
8, 8,
8 8,
]; // se ]; // se
return handlers; return handlers;
@ -402,7 +403,7 @@ function renderScene(
offsetX, offsetX,
offsetY, offsetY,
renderScrollbars = true, renderScrollbars = true,
renderSelection = true renderSelection = true,
}: { }: {
offsetX?: number; offsetX?: number;
offsetY?: number; offsetY?: number;
@ -411,10 +412,10 @@ function renderScene(
} = {} } = {}
) { ) {
if (!canvas) return; if (!canvas) return;
const context = canvas.getContext("2d")!; const context = canvas.getContext('2d')!;
const fillStyle = context.fillStyle; const fillStyle = context.fillStyle;
if (typeof sceneState.viewBackgroundColor === "string") { if (typeof sceneState.viewBackgroundColor === 'string') {
context.fillStyle = sceneState.viewBackgroundColor; context.fillStyle = sceneState.viewBackgroundColor;
context.fillRect(0, 0, canvas.width, canvas.height); context.fillRect(0, 0, canvas.width, canvas.height);
} else { } else {
@ -426,8 +427,8 @@ function renderScene(
sceneState = { sceneState = {
...sceneState, ...sceneState,
scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX, scrollX: typeof offsetX === 'number' ? offsetX : sceneState.scrollX,
scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY scrollY: typeof offsetY === 'number' ? offsetY : sceneState.scrollY,
}; };
elements.forEach(element => { elements.forEach(element => {
@ -450,8 +451,8 @@ function renderScene(
context.setLineDash(lineDash); context.setLineDash(lineDash);
if ( if (
element.type !== "text" && element.type !== 'text' &&
element.type !== "arrow" && element.type !== 'arrow' &&
selectedIndices.length === 1 selectedIndices.length === 1
) { ) {
const handlers = handlerRectangles(element, sceneState); const handlers = handlerRectangles(element, sceneState);
@ -491,28 +492,28 @@ function saveAsJSON() {
const serialized = JSON.stringify({ const serialized = JSON.stringify({
version: 1, version: 1,
source: window.location.origin, source: window.location.origin,
elements elements,
}); });
saveFile( saveFile(
"excalidraw.json", 'excalidraw.json',
"data:text/plain;charset=utf-8," + encodeURIComponent(serialized) 'data:text/plain;charset=utf-8,' + encodeURIComponent(serialized)
); );
} }
function loadFromJSON() { function loadFromJSON() {
const input = document.createElement("input"); const input = document.createElement('input');
const reader = new FileReader(); const reader = new FileReader();
input.type = "file"; input.type = 'file';
input.accept = ".json"; input.accept = '.json';
input.onchange = () => { input.onchange = () => {
if (!input.files!.length) { if (!input.files!.length) {
alert("A file was not selected."); alert('A file was not selected.');
return; return;
} }
reader.readAsText(input.files![0], "utf8"); reader.readAsText(input.files![0], 'utf8');
}; };
input.click(); input.click();
@ -531,7 +532,7 @@ function loadFromJSON() {
function exportAsPNG({ function exportAsPNG({
exportBackground, exportBackground,
exportPadding = 10, exportPadding = 10,
viewBackgroundColor viewBackgroundColor,
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
@ -539,7 +540,7 @@ function exportAsPNG({
scrollX: number; scrollX: number;
scrollY: number; scrollY: number;
}) { }) {
if (!elements.length) return window.alert("Cannot export empty canvas."); if (!elements.length) return window.alert('Cannot export empty canvas.');
// calculate smallest area to fit the contents in // calculate smallest area to fit the contents in
@ -559,8 +560,8 @@ function exportAsPNG({
return Math.abs(x > y ? x - y : y - x); return Math.abs(x > y ? x - y : y - x);
} }
const tempCanvas = document.createElement("canvas"); const tempCanvas = document.createElement('canvas');
tempCanvas.style.display = "none"; tempCanvas.style.display = 'none';
document.body.appendChild(tempCanvas); document.body.appendChild(tempCanvas);
tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2; tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2; tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
@ -571,17 +572,17 @@ function exportAsPNG({
{ {
viewBackgroundColor: exportBackground ? viewBackgroundColor : null, viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: 0, scrollX: 0,
scrollY: 0 scrollY: 0,
}, },
{ {
offsetX: -subCanvasX1 + exportPadding, offsetX: -subCanvasX1 + exportPadding,
offsetY: -subCanvasY1 + exportPadding, offsetY: -subCanvasY1 + exportPadding,
renderScrollbars: false, renderScrollbars: false,
renderSelection: false renderSelection: false,
} }
); );
saveFile("excalidraw.png", tempCanvas.toDataURL("image/png")); saveFile('excalidraw.png', tempCanvas.toDataURL('image/png'));
// clean up the DOM // clean up the DOM
if (tempCanvas !== canvas) tempCanvas.remove(); if (tempCanvas !== canvas) tempCanvas.remove();
@ -589,9 +590,9 @@ function exportAsPNG({
function saveFile(name: string, data: string) { function saveFile(name: string, data: string) {
// create a temporary <a> elem which we'll use to download the image // create a temporary <a> elem which we'll use to download the image
const link = document.createElement("a"); const link = document.createElement('a');
link.setAttribute("download", name); link.setAttribute('download', name);
link.setAttribute("href", data); link.setAttribute('href', data);
link.click(); link.click();
// clean up // clean up
@ -604,7 +605,7 @@ function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
return [ return [
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2, (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2 (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
]; ];
} }
@ -615,7 +616,7 @@ const generator = rough.generator(null, null as any);
function isTextElement( function isTextElement(
element: ExcalidrawElement element: ExcalidrawElement
): element is ExcalidrawTextElement { ): element is ExcalidrawTextElement {
return element.type === "text"; return element.type === 'text';
} }
function isInputLike( function isInputLike(
@ -649,10 +650,10 @@ function getArrowPoints(element: ExcalidrawElement) {
} }
function generateDraw(element: ExcalidrawElement) { function generateDraw(element: ExcalidrawElement) {
if (element.type === "selection") { if (element.type === 'selection') {
element.draw = (rc, context, { scrollX, scrollY }) => { element.draw = (rc, context, { scrollX, scrollY }) => {
const fillStyle = context.fillStyle; const fillStyle = context.fillStyle;
context.fillStyle = "rgba(0, 0, 255, 0.10)"; context.fillStyle = 'rgba(0, 0, 255, 0.10)';
context.fillRect( context.fillRect(
element.x + scrollX, element.x + scrollX,
element.y + scrollY, element.y + scrollY,
@ -661,11 +662,11 @@ function generateDraw(element: ExcalidrawElement) {
); );
context.fillStyle = fillStyle; context.fillStyle = fillStyle;
}; };
} else if (element.type === "rectangle") { } else if (element.type === 'rectangle') {
const shape = withCustomMathRandom(element.seed, () => { const shape = withCustomMathRandom(element.seed, () => {
return generator.rectangle(0, 0, element.width, element.height, { return generator.rectangle(0, 0, element.width, element.height, {
stroke: element.strokeColor, stroke: element.strokeColor,
fill: element.backgroundColor fill: element.backgroundColor,
}); });
}); });
element.draw = (rc, context, { scrollX, scrollY }) => { element.draw = (rc, context, { scrollX, scrollY }) => {
@ -673,7 +674,7 @@ function generateDraw(element: ExcalidrawElement) {
rc.draw(shape); rc.draw(shape);
context.translate(-element.x - scrollX, -element.y - scrollY); context.translate(-element.x - scrollX, -element.y - scrollY);
}; };
} else if (element.type === "ellipse") { } else if (element.type === 'ellipse') {
const shape = withCustomMathRandom(element.seed, () => const shape = withCustomMathRandom(element.seed, () =>
generator.ellipse( generator.ellipse(
element.width / 2, element.width / 2,
@ -688,7 +689,7 @@ function generateDraw(element: ExcalidrawElement) {
rc.draw(shape); rc.draw(shape);
context.translate(-element.x - scrollX, -element.y - scrollY); context.translate(-element.x - scrollX, -element.y - scrollY);
}; };
} else if (element.type === "arrow") { } else if (element.type === 'arrow') {
const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element); const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
const shapes = withCustomMathRandom(element.seed, () => [ const shapes = withCustomMathRandom(element.seed, () => [
// \ // \
@ -696,7 +697,7 @@ function generateDraw(element: ExcalidrawElement) {
// ----- // -----
generator.line(x1, y1, x2, y2, { stroke: element.strokeColor }), generator.line(x1, y1, x2, y2, { stroke: element.strokeColor }),
// / // /
generator.line(x4, y4, x2, y2, { stroke: element.strokeColor }) generator.line(x4, y4, x2, y2, { stroke: element.strokeColor }),
]); ]);
element.draw = (rc, context, { scrollX, scrollY }) => { element.draw = (rc, context, { scrollX, scrollY }) => {
@ -720,7 +721,7 @@ function generateDraw(element: ExcalidrawElement) {
context.font = font; context.font = font;
}; };
} else { } else {
throw new Error("Unimplemented type " + element.type); throw new Error('Unimplemented type ' + element.type);
} }
} }
@ -752,7 +753,7 @@ function setSelection(selection: ExcalidrawElement) {
const elementY1 = getElementAbsoluteY1(element); const elementY1 = getElementAbsoluteY1(element);
const elementY2 = getElementAbsoluteY2(element); const elementY2 = getElementAbsoluteY2(element);
element.isSelected = element.isSelected =
element.type !== "selection" && element.type !== 'selection' &&
selectionX1 <= elementX1 && selectionX1 <= elementX1 &&
selectionY1 <= elementY1 && selectionY1 <= elementY1 &&
selectionX2 >= elementX2 && selectionX2 >= elementX2 &&
@ -767,7 +768,7 @@ function clearSelection() {
} }
function resetCursor() { function resetCursor() {
document.documentElement.style.cursor = ""; document.documentElement.style.cursor = '';
} }
function deleteSelectedElements() { function deleteSelectedElements() {
@ -799,7 +800,7 @@ function restore(
elements.splice( elements.splice(
0, 0,
elements.length, elements.length,
...(typeof savedElements === "string" ...(typeof savedElements === 'string'
? JSON.parse(savedElements) ? JSON.parse(savedElements)
: savedElements) : savedElements)
); );
@ -816,6 +817,7 @@ function restore(
type AppState = { type AppState = {
draggingElement: ExcalidrawElement | null; draggingElement: ExcalidrawElement | null;
resizingElement: ExcalidrawElement | null; resizingElement: ExcalidrawElement | null;
currentColorPicker: 'Background' | null;
elementType: string; elementType: string;
exportBackground: boolean; exportBackground: boolean;
currentItemStrokeColor: string; currentItemStrokeColor: string;
@ -826,13 +828,13 @@ type AppState = {
}; };
const KEYS = { const KEYS = {
ARROW_LEFT: "ArrowLeft", ARROW_LEFT: 'ArrowLeft',
ARROW_RIGHT: "ArrowRight", ARROW_RIGHT: 'ArrowRight',
ARROW_DOWN: "ArrowDown", ARROW_DOWN: 'ArrowDown',
ARROW_UP: "ArrowUp", ARROW_UP: 'ArrowUp',
ESCAPE: "Escape", ESCAPE: 'Escape',
DELETE: "Delete", DELETE: 'Delete',
BACKSPACE: "Backspace" BACKSPACE: 'Backspace',
}; };
// We inline font-awesome icons in order to save on js size rather than including the font awesome react library // We inline font-awesome icons in order to save on js size rather than including the font awesome react library
@ -844,7 +846,7 @@ const SHAPES = [
<path d="M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z" /> <path d="M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z" />
</svg> </svg>
), ),
value: "selection" value: 'selection',
}, },
{ {
icon: ( icon: (
@ -853,7 +855,7 @@ const SHAPES = [
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z" /> <path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z" />
</svg> </svg>
), ),
value: "rectangle" value: 'rectangle',
}, },
{ {
icon: ( icon: (
@ -862,7 +864,7 @@ const SHAPES = [
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" /> <path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" />
</svg> </svg>
), ),
value: "ellipse" value: 'ellipse',
}, },
{ {
icon: ( icon: (
@ -871,7 +873,7 @@ const SHAPES = [
<path d="M313.941 216H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h301.941v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.569 0-33.941l-86.059-86.059c-15.119-15.119-40.971-4.411-40.971 16.971V216z" /> <path d="M313.941 216H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h301.941v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.569 0-33.941l-86.059-86.059c-15.119-15.119-40.971-4.411-40.971 16.971V216z" />
</svg> </svg>
), ),
value: "arrow" value: 'arrow',
}, },
{ {
icon: ( icon: (
@ -880,19 +882,18 @@ const SHAPES = [
<path d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z" /> <path d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z" />
</svg> </svg>
), ),
value: "text" value: 'text',
} },
]; ];
const shapesShortcutKeys = SHAPES.map(shape => shape.value[0]); const shapesShortcutKeys = SHAPES.map(shape => shape.value[0]);
function capitalize(str: string) { function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
} }
function findElementByKey(key: string) { function findElementByKey(key: string) {
const defaultElement = "selection"; const defaultElement = 'selection';
return SHAPES.reduce((element, shape) => { return SHAPES.reduce((element, shape) => {
if (shape.value[0] !== key) return element; if (shape.value[0] !== key) return element;
@ -932,8 +933,8 @@ let lastMouseUp: ((e: any) => void) | null = null;
class App extends React.Component<{}, AppState> { class App extends React.Component<{}, AppState> {
public componentDidMount() { public componentDidMount() {
document.addEventListener("keydown", this.onKeyDown, false); document.addEventListener('keydown', this.onKeyDown, false);
window.addEventListener("resize", this.onResize, false); window.addEventListener('resize', this.onResize, false);
const savedState = restoreFromLocalStorage(); const savedState = restoreFromLocalStorage();
if (savedState) { if (savedState) {
@ -942,20 +943,21 @@ class App extends React.Component<{}, AppState> {
} }
public componentWillUnmount() { public componentWillUnmount() {
document.removeEventListener("keydown", this.onKeyDown, false); document.removeEventListener('keydown', this.onKeyDown, false);
window.removeEventListener("resize", this.onResize, false); window.removeEventListener('resize', this.onResize, false);
} }
public state: AppState = { public state: AppState = {
draggingElement: null, draggingElement: null,
resizingElement: null, resizingElement: null,
elementType: "selection", elementType: 'selection',
currentColorPicker: null,
exportBackground: true, exportBackground: true,
currentItemStrokeColor: "#000000", currentItemStrokeColor: '#000000',
currentItemBackgroundColor: "#ffffff", currentItemBackgroundColor: '#ffffff',
viewBackgroundColor: "#ffffff", viewBackgroundColor: '#ffffff',
scrollX: 0, scrollX: 0,
scrollY: 0 scrollY: 0,
}; };
private onResize = () => { private onResize = () => {
@ -993,13 +995,13 @@ class App extends React.Component<{}, AppState> {
event.metaKey && event.metaKey &&
event.shiftKey && event.shiftKey &&
event.altKey && event.altKey &&
event.code === "KeyB" event.code === 'KeyB'
) { ) {
this.moveOneLeft(); this.moveOneLeft();
event.preventDefault(); event.preventDefault();
// Send to back: Cmd-Shift-B // Send to back: Cmd-Shift-B
} else if (event.metaKey && event.shiftKey && event.code === "KeyB") { } else if (event.metaKey && event.shiftKey && event.code === 'KeyB') {
this.moveAllLeft(); this.moveAllLeft();
event.preventDefault(); event.preventDefault();
@ -1008,18 +1010,18 @@ class App extends React.Component<{}, AppState> {
event.metaKey && event.metaKey &&
event.shiftKey && event.shiftKey &&
event.altKey && event.altKey &&
event.code === "KeyF" event.code === 'KeyF'
) { ) {
this.moveOneRight(); this.moveOneRight();
event.preventDefault(); event.preventDefault();
// Bring to front: Cmd-Shift-F // Bring to front: Cmd-Shift-F
} else if (event.metaKey && event.shiftKey && event.code === "KeyF") { } else if (event.metaKey && event.shiftKey && event.code === 'KeyF') {
this.moveAllRight(); this.moveAllRight();
event.preventDefault(); event.preventDefault();
// Select all: Cmd-A // Select all: Cmd-A
} else if (event.metaKey && event.code === "KeyA") { } else if (event.metaKey && event.code === 'KeyA') {
elements.forEach(element => { elements.forEach(element => {
element.isSelected = true; element.isSelected = true;
}); });
@ -1027,7 +1029,7 @@ class App extends React.Component<{}, AppState> {
event.preventDefault(); event.preventDefault();
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) { } else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
this.setState({ elementType: findElementByKey(event.key) }); this.setState({ elementType: findElementByKey(event.key) });
} else if (event.metaKey && event.code === "KeyZ") { } else if (event.metaKey && event.code === 'KeyZ') {
let lastEntry = stateHistory.pop(); let lastEntry = stateHistory.pop();
// If nothing was changed since last, take the previous one // If nothing was changed since last, take the previous one
if (generateHistoryCurrentEntry() === lastEntry) { if (generateHistoryCurrentEntry() === lastEntry) {
@ -1047,12 +1049,12 @@ class App extends React.Component<{}, AppState> {
}; };
private clearCanvas = () => { private clearCanvas = () => {
if (window.confirm("This will clear the whole canvas. Are you sure?")) { if (window.confirm('This will clear the whole canvas. Are you sure?')) {
elements.splice(0, elements.length); elements.splice(0, elements.length);
this.setState({ this.setState({
viewBackgroundColor: "#ffffff", viewBackgroundColor: '#ffffff',
scrollX: 0, scrollX: 0,
scrollY: 0 scrollY: 0,
}); });
this.forceUpdate(); this.forceUpdate();
} }
@ -1089,7 +1091,7 @@ class App extends React.Component<{}, AppState> {
className="container" className="container"
onCut={e => { onCut={e => {
e.clipboardData.setData( e.clipboardData.setData(
"text/plain", 'text/plain',
JSON.stringify(elements.filter(element => element.isSelected)) JSON.stringify(elements.filter(element => element.isSelected))
); );
deleteSelectedElements(); deleteSelectedElements();
@ -1098,13 +1100,13 @@ class App extends React.Component<{}, AppState> {
}} }}
onCopy={e => { onCopy={e => {
e.clipboardData.setData( e.clipboardData.setData(
"text/plain", 'text/plain',
JSON.stringify(elements.filter(element => element.isSelected)) JSON.stringify(elements.filter(element => element.isSelected))
); );
e.preventDefault(); e.preventDefault();
}} }}
onPaste={e => { onPaste={e => {
const paste = e.clipboardData.getData("text"); const paste = e.clipboardData.getData('text');
let parsedElements; let parsedElements;
try { try {
parsedElements = JSON.parse(paste); parsedElements = JSON.parse(paste);
@ -1131,7 +1133,11 @@ class App extends React.Component<{}, AppState> {
<h4>Shapes</h4> <h4>Shapes</h4>
<div className="panelTools"> <div className="panelTools">
{SHAPES.map(({ value, icon }) => ( {SHAPES.map(({ value, icon }) => (
<label key={value} className="tool" title={`${capitalize(value)} - ${capitalize(value)[0]}`}> <label
key={value}
className="tool"
title={`${capitalize(value)} - ${capitalize(value)[0]}`}
>
<input <input
type="radio" type="radio"
checked={this.state.elementType === value} checked={this.state.elementType === value}
@ -1139,7 +1145,7 @@ class App extends React.Component<{}, AppState> {
this.setState({ elementType: value }); this.setState({ elementType: value });
clearSelection(); clearSelection();
document.documentElement.style.cursor = document.documentElement.style.cursor =
value === "text" ? "text" : "crosshair"; value === 'text' ? 'text' : 'crosshair';
this.forceUpdate(); this.forceUpdate();
}} }}
/> />
@ -1149,16 +1155,46 @@ class App extends React.Component<{}, AppState> {
</div> </div>
<h4>Colors</h4> <h4>Colors</h4>
<div className="panelColumn"> <div className="panelColumn">
<label> <div>
<input <h5>Background</h5>
type="color"
value={this.state.viewBackgroundColor} <button
onChange={e => { className="swatch"
this.setState({ viewBackgroundColor: e.target.value }); style={{
backgroundColor: this.state.viewBackgroundColor,
}}
onClick={() =>
this.setState(s => ({
currentColorPicker:
s.currentColorPicker === 'Background'
? null
: 'Background',
}))
}
></button>
{this.state.currentColorPicker === 'Background' ? (
<div className="popover">
<div
className="cover"
onClick={() => this.setState({ currentColorPicker: null })}
></div>
<SketchPicker
color={this.state.viewBackgroundColor}
onChange={color => {
this.setState({ viewBackgroundColor: color.hex });
}} }}
/> />
Background </div>
</label> ) : null}
<input
type="text"
className="swatch-input"
value={this.state.viewBackgroundColor}
onChange={e =>
this.setState({ viewBackgroundColor: e.target.value })
}
/>
</div>
<label> <label>
<input <input
type="color" type="color"
@ -1243,7 +1279,7 @@ class App extends React.Component<{}, AppState> {
id="canvas" id="canvas"
style={{ style={{
width: canvasWidth, width: canvasWidth,
height: canvasHeight height: canvasHeight,
}} }}
width={canvasWidth * window.devicePixelRatio} width={canvasWidth * window.devicePixelRatio}
height={canvasHeight * window.devicePixelRatio} height={canvasHeight * window.devicePixelRatio}
@ -1253,11 +1289,11 @@ class App extends React.Component<{}, AppState> {
this.removeWheelEventListener = undefined; this.removeWheelEventListener = undefined;
} }
if (canvas) { if (canvas) {
canvas.addEventListener("wheel", this.handleWheel, { canvas.addEventListener('wheel', this.handleWheel, {
passive: false passive: false,
}); });
this.removeWheelEventListener = () => this.removeWheelEventListener = () =>
canvas.removeEventListener("wheel", this.handleWheel); canvas.removeEventListener('wheel', this.handleWheel);
// Whenever React sets the width/height of the canvas element, // Whenever React sets the width/height of the canvas element,
// the context loses the scale transform. We need to re-apply it // the context loses the scale transform. We need to re-apply it
@ -1268,7 +1304,7 @@ class App extends React.Component<{}, AppState> {
lastCanvasWidth = canvasWidth; lastCanvasWidth = canvasWidth;
lastCanvasHeight = canvasHeight; lastCanvasHeight = canvasHeight;
canvas canvas
.getContext("2d")! .getContext('2d')!
.scale(window.devicePixelRatio, window.devicePixelRatio); .scale(window.devicePixelRatio, window.devicePixelRatio);
} }
} }
@ -1294,7 +1330,7 @@ class App extends React.Component<{}, AppState> {
// Handle scrollbars dragging // Handle scrollbars dragging
const { const {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar isOverVerticalScrollBar,
} = isOverScrollBars( } = isOverScrollBars(
e.clientX - CANVAS_WINDOW_OFFSET_LEFT, e.clientX - CANVAS_WINDOW_OFFSET_LEFT,
e.clientY - CANVAS_WINDOW_OFFSET_TOP, e.clientY - CANVAS_WINDOW_OFFSET_TOP,
@ -1317,24 +1353,24 @@ class App extends React.Component<{}, AppState> {
let resizeHandle: string | false = false; let resizeHandle: string | false = false;
let isDraggingElements = false; let isDraggingElements = false;
let isResizingElements = false; let isResizingElements = false;
if (this.state.elementType === "selection") { if (this.state.elementType === 'selection') {
const resizeElement = elements.find(element => { const resizeElement = elements.find(element => {
return resizeTest(element, x, y, { return resizeTest(element, x, y, {
scrollX: this.state.scrollX, scrollX: this.state.scrollX,
scrollY: this.state.scrollY, scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor viewBackgroundColor: this.state.viewBackgroundColor,
}); });
}); });
this.setState({ this.setState({
resizingElement: resizeElement ? resizeElement : null resizingElement: resizeElement ? resizeElement : null,
}); });
if (resizeElement) { if (resizeElement) {
resizeHandle = resizeTest(resizeElement, x, y, { resizeHandle = resizeTest(resizeElement, x, y, {
scrollX: this.state.scrollX, scrollX: this.state.scrollX,
scrollY: this.state.scrollY, scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor viewBackgroundColor: this.state.viewBackgroundColor,
}); });
document.documentElement.style.cursor = `${resizeHandle}-resize`; document.documentElement.style.cursor = `${resizeHandle}-resize`;
isResizingElements = true; isResizingElements = true;
@ -1369,14 +1405,14 @@ class App extends React.Component<{}, AppState> {
isDraggingElements = someElementIsSelected(); isDraggingElements = someElementIsSelected();
if (isDraggingElements) { if (isDraggingElements) {
document.documentElement.style.cursor = "move"; document.documentElement.style.cursor = 'move';
} }
} }
} }
if (isTextElement(element)) { if (isTextElement(element)) {
resetCursor(); resetCursor();
const text = prompt("What text do you want?"); const text = prompt('What text do you want?');
if (text === null) { if (text === null) {
return; return;
} }
@ -1403,10 +1439,10 @@ class App extends React.Component<{}, AppState> {
generateDraw(element); generateDraw(element);
elements.push(element); elements.push(element);
if (this.state.elementType === "text") { if (this.state.elementType === 'text') {
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
elementType: "selection" elementType: 'selection',
}); });
element.isSelected = true; element.isSelected = true;
} else { } else {
@ -1453,23 +1489,23 @@ class App extends React.Component<{}, AppState> {
e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY;
selectedElements.forEach(element => { selectedElements.forEach(element => {
switch (resizeHandle) { switch (resizeHandle) {
case "nw": case 'nw':
element.width += element.x - lastX; element.width += element.x - lastX;
element.height += element.y - lastY; element.height += element.y - lastY;
element.x = lastX; element.x = lastX;
element.y = lastY; element.y = lastY;
break; break;
case "ne": case 'ne':
element.width = lastX - element.x; element.width = lastX - element.x;
element.height += element.y - lastY; element.height += element.y - lastY;
element.y = lastY; element.y = lastY;
break; break;
case "sw": case 'sw':
element.width += element.x - lastX; element.width += element.x - lastX;
element.x = lastX; element.x = lastX;
element.height = lastY - element.y; element.height = lastY - element.y;
break; break;
case "se": case 'se':
element.width += x - lastX; element.width += x - lastX;
if (e.shiftKey) { if (e.shiftKey) {
element.height = element.width; element.height = element.width;
@ -1477,18 +1513,18 @@ class App extends React.Component<{}, AppState> {
element.height += y - lastY; element.height += y - lastY;
} }
break; break;
case "n": case 'n':
element.height += element.y - lastY; element.height += element.y - lastY;
element.y = lastY; element.y = lastY;
break; break;
case "w": case 'w':
element.width += element.x - lastX; element.width += element.x - lastX;
element.x = lastX; element.x = lastX;
break; break;
case "s": case 's':
element.height = lastY - element.y; element.height = lastY - element.y;
break; break;
case "e": case 'e':
element.width = lastX - element.x; element.width = lastX - element.x;
break; break;
} }
@ -1546,7 +1582,7 @@ class App extends React.Component<{}, AppState> {
generateDraw(draggingElement); generateDraw(draggingElement);
if (this.state.elementType === "selection") { if (this.state.elementType === 'selection') {
setSelection(draggingElement); setSelection(draggingElement);
} }
// We don't want to save history when moving an element // We don't want to save history when moving an element
@ -1558,8 +1594,8 @@ class App extends React.Component<{}, AppState> {
const { draggingElement, elementType } = this.state; const { draggingElement, elementType } = this.state;
lastMouseUp = null; lastMouseUp = null;
window.removeEventListener("mousemove", onMouseMove); window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener("mouseup", onMouseUp); window.removeEventListener('mouseup', onMouseUp);
resetCursor(); resetCursor();
@ -1570,7 +1606,7 @@ class App extends React.Component<{}, AppState> {
return; return;
} }
if (elementType === "selection") { if (elementType === 'selection') {
if (isDraggingElements) { if (isDraggingElements) {
isDraggingElements = false; isDraggingElements = false;
} }
@ -1581,15 +1617,15 @@ class App extends React.Component<{}, AppState> {
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
elementType: "selection" elementType: 'selection',
}); });
this.forceUpdate(); this.forceUpdate();
}; };
lastMouseUp = onMouseUp; lastMouseUp = onMouseUp;
window.addEventListener("mousemove", onMouseMove); window.addEventListener('mousemove', onMouseMove);
window.addEventListener("mouseup", onMouseUp); window.addEventListener('mouseup', onMouseUp);
// We don't want to save history on mouseDown, only on mouseUp when it's fully configured // We don't want to save history on mouseDown, only on mouseUp when it's fully configured
skipHistory = true; skipHistory = true;
@ -1605,7 +1641,7 @@ class App extends React.Component<{}, AppState> {
const { deltaX, deltaY } = e; const { deltaX, deltaY } = e;
this.setState(state => ({ this.setState(state => ({
scrollX: state.scrollX - deltaX, scrollX: state.scrollX - deltaX,
scrollY: state.scrollY - deltaY scrollY: state.scrollY - deltaY,
})); }));
}; };
@ -1613,7 +1649,7 @@ class App extends React.Component<{}, AppState> {
renderScene(rc, canvas, { renderScene(rc, canvas, {
scrollX: this.state.scrollX, scrollX: this.state.scrollX,
scrollY: this.state.scrollY, scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor viewBackgroundColor: this.state.viewBackgroundColor,
}); });
save(this.state); save(this.state);
if (!skipHistory) { if (!skipHistory) {
@ -1623,10 +1659,10 @@ class App extends React.Component<{}, AppState> {
} }
} }
const rootElement = document.getElementById("root"); const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement); ReactDOM.render(<App />, rootElement);
const canvas = document.getElementById("canvas") as HTMLCanvasElement; const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const rc = rough.canvas(canvas); const rc = rough.canvas(canvas);
const context = canvas.getContext("2d")!; const context = canvas.getContext('2d')!;
ReactDOM.render(<App />, rootElement); ReactDOM.render(<App />, rootElement);

View file

@ -1,7 +1,7 @@
/* http://www.eaglefonts.com/fg-virgil-ttf-131249.htm */ /* http://www.eaglefonts.com/fg-virgil-ttf-131249.htm */
@font-face { @font-face {
font-family: "Virgil"; font-family: 'Virgil';
src: url("https://uploads.codesandbox.io/uploads/user/ed077012-e728-4a42-8395-cbd299149d62/AflB-FG_Virgil.ttf"); src: url('https://uploads.codesandbox.io/uploads/user/ed077012-e728-4a42-8395-cbd299149d62/AflB-FG_Virgil.ttf');
font-display: swap; font-display: swap;
} }
@ -22,7 +22,6 @@ body {
.sidePanel { .sidePanel {
width: 230px; width: 230px;
background-color: #eee; background-color: #eee;
padding: 10px; padding: 10px;
overflow-y: auto; overflow-y: auto;
@ -42,20 +41,25 @@ body {
.panelColumn { .panelColumn {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
h5 {
margin-top: 0px;
margin-bottom: 8px;
}
} }
} }
.tool { .tool {
position: relative; position: relative;
input[type="radio"] { input[type='radio'] {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
width: 0; width: 0;
height: 0; height: 0;
} }
input[type="radio"] { input[type='radio'] {
& + .toolIcon { & + .toolIcon {
background-color: #ddd; background-color: #ddd;
@ -91,11 +95,11 @@ label {
} }
} }
input[type="number"] { input[type='number'] {
width: 30px; width: 30px;
} }
input[type="color"] { input[type='color'] {
margin: 2px; margin: 2px;
} }
@ -134,3 +138,29 @@ button {
cursor: not-allowed; cursor: not-allowed;
} }
} }
.popover {
position: absolute;
z-index: 2;
.cover {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.swatch {
height: 24px;
width: 24px;
margin-right: 4px;
}
.swatch-input {
font-size: 16px;
border-radius: 2px;
padding: 2px;
border: 1px solid #ddd;
}