mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Save state and elements automatically
This commit is contained in:
commit
c59d3c90f4
4 changed files with 853 additions and 713 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@ logs
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
382
src/index.tsx
382
src/index.tsx
|
@ -14,6 +14,7 @@ type ExcaliburTextElement = ExcaliburElement & {
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "excalibur";
|
const LOCAL_STORAGE_KEY = "excalibur";
|
||||||
|
const LOCAL_STORAGE_KEY_STATE = "excalibur-state";
|
||||||
|
|
||||||
var elements = Array.of<ExcaliburElement>();
|
var elements = Array.of<ExcaliburElement>();
|
||||||
|
|
||||||
|
@ -107,7 +108,15 @@ function hitTest(element: ExcaliburElement, x: number, y: number): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function newElement(type: string, x: number, y: number, width = 0, height = 0) {
|
function newElement(
|
||||||
|
type: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
strokeColor: string,
|
||||||
|
backgroundColor: string,
|
||||||
|
width = 0,
|
||||||
|
height = 0
|
||||||
|
) {
|
||||||
const element = {
|
const element = {
|
||||||
type: type,
|
type: type,
|
||||||
x: x,
|
x: x,
|
||||||
|
@ -115,29 +124,69 @@ function newElement(type: string, x: number, y: number, width = 0, height = 0) {
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
strokeColor: strokeColor,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
draw(rc: RoughCanvas, context: CanvasRenderingContext2D) {}
|
draw(rc: RoughCanvas, context: CanvasRenderingContext2D) {}
|
||||||
};
|
};
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderScene(
|
||||||
|
rc: RoughCanvas,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
// null indicates transparent bg
|
||||||
|
viewBackgroundColor: string | null
|
||||||
|
) {
|
||||||
|
if (!context) return;
|
||||||
|
|
||||||
|
const fillStyle = context.fillStyle;
|
||||||
|
if (typeof viewBackgroundColor === "string") {
|
||||||
|
context.fillStyle = viewBackgroundColor;
|
||||||
|
context.fillRect(-0.5, -0.5, canvas.width, canvas.height);
|
||||||
|
} else {
|
||||||
|
context.clearRect(-0.5, -0.5, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
context.fillStyle = fillStyle;
|
||||||
|
|
||||||
|
elements.forEach(element => {
|
||||||
|
element.draw(rc, context);
|
||||||
|
if (element.isSelected) {
|
||||||
|
const margin = 4;
|
||||||
|
|
||||||
|
const elementX1 = getElementAbsoluteX1(element);
|
||||||
|
const elementX2 = getElementAbsoluteX2(element);
|
||||||
|
const elementY1 = getElementAbsoluteY1(element);
|
||||||
|
const elementY2 = getElementAbsoluteY2(element);
|
||||||
|
const lineDash = context.getLineDash();
|
||||||
|
context.setLineDash([8, 4]);
|
||||||
|
context.strokeRect(
|
||||||
|
elementX1 - margin,
|
||||||
|
elementY1 - margin,
|
||||||
|
elementX2 - elementX1 + margin * 2,
|
||||||
|
elementY2 - elementY1 + margin * 2
|
||||||
|
);
|
||||||
|
context.setLineDash(lineDash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function exportAsPNG({
|
function exportAsPNG({
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportVisibleOnly,
|
exportVisibleOnly,
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
viewBgColor
|
viewBackgroundColor
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportVisibleOnly: boolean;
|
exportVisibleOnly: boolean;
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBgColor: string;
|
viewBackgroundColor: string;
|
||||||
}) {
|
}) {
|
||||||
if (!elements.length) return window.alert("Cannot export empty canvas.");
|
if (!elements.length) return window.alert("Cannot export empty canvas.");
|
||||||
|
|
||||||
// deselect & rerender
|
// deselect & rerender
|
||||||
|
|
||||||
clearSelection();
|
clearSelection();
|
||||||
drawScene();
|
ReactDOM.render(<App />, rootElement, () => {
|
||||||
|
|
||||||
// calculate visible-area coords
|
// calculate visible-area coords
|
||||||
|
|
||||||
let subCanvasX1 = Infinity;
|
let subCanvasX1 = Infinity;
|
||||||
|
@ -165,9 +214,10 @@ function exportAsPNG({
|
||||||
? subCanvasY2 - subCanvasY1 + exportPadding * 2
|
? subCanvasY2 - subCanvasY1 + exportPadding * 2
|
||||||
: canvas.height;
|
: canvas.height;
|
||||||
|
|
||||||
if (exportBackground) {
|
// if we're exporting without bg, we need to rerender the scene without it
|
||||||
tempCanvasCtx.fillStyle = viewBgColor;
|
// (it's reset again, below)
|
||||||
tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
if (!exportBackground) {
|
||||||
|
renderScene(rc, context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy our original canvas onto the temp canvas
|
// copy our original canvas onto the temp canvas
|
||||||
|
@ -191,6 +241,11 @@ function exportAsPNG({
|
||||||
exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
|
exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// reset transparent bg back to original
|
||||||
|
if (!exportBackground) {
|
||||||
|
renderScene(rc, context, viewBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
// 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", "excalibur.png");
|
link.setAttribute("download", "excalibur.png");
|
||||||
|
@ -200,6 +255,7 @@ function exportAsPNG({
|
||||||
// clean up the DOM
|
// clean up the DOM
|
||||||
link.remove();
|
link.remove();
|
||||||
if (tempCanvas !== canvas) tempCanvas.remove();
|
if (tempCanvas !== canvas) tempCanvas.remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
|
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
|
||||||
|
@ -242,11 +298,7 @@ function getArrowPoints(element: ExcaliburElement) {
|
||||||
return [x1, y1, x2, y2, x3, y3, x4, y4];
|
return [x1, y1, x2, y2, x3, y3, x4, y4];
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateDraw(
|
function generateDraw(element: ExcaliburElement) {
|
||||||
element: ExcaliburElement,
|
|
||||||
itemStrokeColor: string,
|
|
||||||
itemBackgroundColorColor: string
|
|
||||||
) {
|
|
||||||
if (element.type === "selection") {
|
if (element.type === "selection") {
|
||||||
element.draw = (rc, context) => {
|
element.draw = (rc, context) => {
|
||||||
const fillStyle = context.fillStyle;
|
const fillStyle = context.fillStyle;
|
||||||
|
@ -256,8 +308,8 @@ function generateDraw(
|
||||||
};
|
};
|
||||||
} else if (element.type === "rectangle") {
|
} else if (element.type === "rectangle") {
|
||||||
const shape = generator.rectangle(0, 0, element.width, element.height, {
|
const shape = generator.rectangle(0, 0, element.width, element.height, {
|
||||||
stroke: itemStrokeColor,
|
stroke: element.strokeColor,
|
||||||
fill: itemBackgroundColorColor
|
fill: element.backgroundColor
|
||||||
});
|
});
|
||||||
element.draw = (rc, context) => {
|
element.draw = (rc, context) => {
|
||||||
context.translate(element.x, element.y);
|
context.translate(element.x, element.y);
|
||||||
|
@ -270,7 +322,7 @@ function generateDraw(
|
||||||
element.height / 2,
|
element.height / 2,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
{ stroke: itemStrokeColor, fill: itemBackgroundColorColor }
|
{ stroke: element.strokeColor, fill: element.backgroundColor }
|
||||||
);
|
);
|
||||||
element.draw = (rc, context) => {
|
element.draw = (rc, context) => {
|
||||||
context.translate(element.x, element.y);
|
context.translate(element.x, element.y);
|
||||||
|
@ -281,11 +333,11 @@ function generateDraw(
|
||||||
const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
||||||
const shapes = [
|
const shapes = [
|
||||||
// \
|
// \
|
||||||
generator.line(x3, y3, x2, y2, { stroke: itemStrokeColor }),
|
generator.line(x3, y3, x2, y2, { stroke: element.strokeColor }),
|
||||||
// -----
|
// -----
|
||||||
generator.line(x1, y1, x2, y2, { stroke: itemStrokeColor }),
|
generator.line(x1, y1, x2, y2, { stroke: element.strokeColor }),
|
||||||
// /
|
// /
|
||||||
generator.line(x4, y4, x2, y2, { stroke: itemStrokeColor })
|
generator.line(x4, y4, x2, y2, { stroke: element.strokeColor })
|
||||||
];
|
];
|
||||||
|
|
||||||
element.draw = (rc, context) => {
|
element.draw = (rc, context) => {
|
||||||
|
@ -360,62 +412,73 @@ function deleteSelectedElements() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save(state: AppState) {
|
||||||
if (elements && elements.length > 0) {
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
|
||||||
const items = [...elements];
|
localStorage.setItem(LOCAL_STORAGE_KEY_STATE, JSON.stringify(state));
|
||||||
for (const item of items) {
|
|
||||||
item.isSelected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(items));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.removeEventListener("beforeunload", onUnload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function restore() {
|
function restore() {
|
||||||
const el = localStorage.getItem(LOCAL_STORAGE_KEY);
|
try {
|
||||||
|
const savedElements = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
const savedState = localStorage.getItem(LOCAL_STORAGE_KEY_STATE);
|
||||||
|
|
||||||
if (el) {
|
if (savedElements) {
|
||||||
const items = JSON.parse(el);
|
elements = JSON.parse(savedElements);
|
||||||
for (let item of items) {
|
elements.forEach((element: ExcaliburElement) => generateDraw(element));
|
||||||
item = generateDraw(item, "#000000", "#ffffff");
|
|
||||||
}
|
}
|
||||||
elements = [...items];
|
|
||||||
|
return savedState ? JSON.parse(savedState) : null;
|
||||||
|
} catch (e) {
|
||||||
|
elements = [];
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnload(event: BeforeUnloadEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const confirmationMessage = "excalibur";
|
|
||||||
event.returnValue = confirmationMessage;
|
|
||||||
return confirmationMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOnBeforeUnload() {
|
|
||||||
window.addEventListener("beforeunload", onUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppState = {
|
type AppState = {
|
||||||
draggingElement: ExcaliburElement | null;
|
draggingElement: ExcaliburElement | null;
|
||||||
elementType: string;
|
elementType: string;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportVisibleOnly: boolean;
|
exportVisibleOnly: boolean;
|
||||||
exportPadding: number;
|
exportPadding: number;
|
||||||
itemStrokeColor: string;
|
currentItemStrokeColor: string;
|
||||||
itemBackgroundColor: string;
|
currentItemBackgroundColor: string;
|
||||||
viewBgColor: string;
|
viewBackgroundColor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const KEYS = {
|
||||||
|
ARROW_LEFT: "ArrowLeft",
|
||||||
|
ARROW_RIGHT: "ArrowRight",
|
||||||
|
ARROW_DOWN: "ArrowDown",
|
||||||
|
ARROW_UP: "ArrowUp",
|
||||||
|
ESCAPE: "Escape",
|
||||||
|
DELETE: "Delete",
|
||||||
|
BACKSPACE: "Backspace"
|
||||||
|
};
|
||||||
|
|
||||||
|
function isArrowKey(keyCode: string) {
|
||||||
|
return (
|
||||||
|
keyCode === KEYS.ARROW_LEFT ||
|
||||||
|
keyCode === KEYS.ARROW_RIGHT ||
|
||||||
|
keyCode === KEYS.ARROW_DOWN ||
|
||||||
|
keyCode === KEYS.ARROW_UP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
||||||
|
const ELEMENT_TRANSLATE_AMOUNT = 1;
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
const savedState = restore();
|
||||||
|
if (savedState) {
|
||||||
|
this.setState(savedState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
document.removeEventListener("keydown", this.onKeyDown, false);
|
document.removeEventListener("keydown", this.onKeyDown, false);
|
||||||
window.removeEventListener("beforeunload", onUnload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public state: AppState = {
|
public state: AppState = {
|
||||||
|
@ -424,9 +487,9 @@ class App extends React.Component<{}, AppState> {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
exportVisibleOnly: true,
|
exportVisibleOnly: true,
|
||||||
exportPadding: 10,
|
exportPadding: 10,
|
||||||
itemStrokeColor: "#000000",
|
currentItemStrokeColor: "#000000",
|
||||||
itemBackgroundColor: "#ffffff",
|
currentItemBackgroundColor: "#ffffff",
|
||||||
viewBgColor: "#ffffff"
|
viewBackgroundColor: "#ffffff"
|
||||||
};
|
};
|
||||||
|
|
||||||
private onKeyDown = (event: KeyboardEvent) => {
|
private onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
@ -434,39 +497,33 @@ class App extends React.Component<{}, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === "Backspace") {
|
} else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
|
||||||
addOnBeforeUnload();
|
|
||||||
deleteSelectedElements();
|
deleteSelectedElements();
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (
|
} else if (isArrowKey(event.key)) {
|
||||||
event.key === "ArrowLeft" ||
|
const step = event.shiftKey
|
||||||
event.key === "ArrowRight" ||
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||||
event.key === "ArrowUp" ||
|
: ELEMENT_TRANSLATE_AMOUNT;
|
||||||
event.key === "ArrowDown"
|
|
||||||
) {
|
|
||||||
addOnBeforeUnload();
|
|
||||||
|
|
||||||
const step = event.shiftKey ? 5 : 1;
|
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
if (element.isSelected) {
|
if (element.isSelected) {
|
||||||
if (event.key === "ArrowLeft") element.x -= step;
|
if (event.key === KEYS.ARROW_LEFT) element.x -= step;
|
||||||
else if (event.key === "ArrowRight") element.x += step;
|
else if (event.key === KEYS.ARROW_RIGHT) element.x += step;
|
||||||
else if (event.key === "ArrowUp") element.y -= step;
|
else if (event.key === KEYS.ARROW_UP) element.y -= step;
|
||||||
else if (event.key === "ArrowDown") element.y += step;
|
else if (event.key === KEYS.ARROW_DOWN) element.y += step;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === "a" && event.metaKey) {
|
} else if (event.key === "a" && event.metaKey) {
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
element.isSelected = true;
|
element.isSelected = true;
|
||||||
});
|
});
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -486,7 +543,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
this.setState({ elementType: type });
|
this.setState({ elementType: type });
|
||||||
clearSelection();
|
clearSelection();
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
|
@ -496,66 +553,6 @@ class App extends React.Component<{}, AppState> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="wrappers">
|
|
||||||
<div className="saveWrapper">
|
|
||||||
<button disabled={elements.length === 0} onClick={save}>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="exportWrapper">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
exportAsPNG({
|
|
||||||
exportBackground: this.state.exportBackground,
|
|
||||||
exportVisibleOnly: this.state.exportVisibleOnly,
|
|
||||||
exportPadding: this.state.exportPadding,
|
|
||||||
viewBgColor: this.state.viewBgColor
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export to png
|
|
||||||
</button>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.exportBackground}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ exportBackground: e.target.checked });
|
|
||||||
}}
|
|
||||||
/>{" "}
|
|
||||||
background
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.exportVisibleOnly}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ exportVisibleOnly: e.target.checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
visible area only
|
|
||||||
</label>
|
|
||||||
(padding:
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={this.state.exportPadding}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ exportPadding: Number(e.target.value) });
|
|
||||||
}}
|
|
||||||
disabled={!this.state.exportVisibleOnly}
|
|
||||||
/>
|
|
||||||
px)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Shapes</legend>
|
|
||||||
{this.renderOption({ type: "rectangle", children: "Rectangle" })}
|
|
||||||
{this.renderOption({ type: "ellipse", children: "Ellipse" })}
|
|
||||||
{this.renderOption({ type: "arrow", children: "Arrow" })}
|
|
||||||
{this.renderOption({ type: "text", children: "Text" })}
|
|
||||||
{this.renderOption({ type: "selection", children: "Selection" })}
|
|
||||||
</fieldset>
|
|
||||||
<div
|
<div
|
||||||
onCut={e => {
|
onCut={e => {
|
||||||
e.clipboardData.setData(
|
e.clipboardData.setData(
|
||||||
|
@ -563,7 +560,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
JSON.stringify(elements.filter(element => element.isSelected))
|
JSON.stringify(elements.filter(element => element.isSelected))
|
||||||
);
|
);
|
||||||
deleteSelectedElements();
|
deleteSelectedElements();
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
onCopy={e => {
|
onCopy={e => {
|
||||||
|
@ -588,19 +585,23 @@ class App extends React.Component<{}, AppState> {
|
||||||
parsedElements.forEach(parsedElement => {
|
parsedElements.forEach(parsedElement => {
|
||||||
parsedElement.x += 10;
|
parsedElement.x += 10;
|
||||||
parsedElement.y += 10;
|
parsedElement.y += 10;
|
||||||
generateDraw(
|
generateDraw(parsedElement);
|
||||||
parsedElement,
|
|
||||||
this.state.itemStrokeColor,
|
|
||||||
this.state.itemBackgroundColor
|
|
||||||
);
|
|
||||||
elements.push(parsedElement);
|
elements.push(parsedElement);
|
||||||
addOnBeforeUnload();
|
|
||||||
});
|
});
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Shapes</legend>
|
||||||
|
{this.renderOption({ type: "rectangle", children: "Rectangle" })}
|
||||||
|
{this.renderOption({ type: "ellipse", children: "Ellipse" })}
|
||||||
|
{this.renderOption({ type: "arrow", children: "Arrow" })}
|
||||||
|
{this.renderOption({ type: "text", children: "Text" })}
|
||||||
|
{this.renderOption({ type: "selection", children: "Selection" })}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
id="canvas"
|
||||||
width={window.innerWidth}
|
width={window.innerWidth}
|
||||||
|
@ -608,7 +609,13 @@ class App extends React.Component<{}, AppState> {
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
const x = e.clientX - (e.target as HTMLElement).offsetLeft;
|
const x = e.clientX - (e.target as HTMLElement).offsetLeft;
|
||||||
const y = e.clientY - (e.target as HTMLElement).offsetTop;
|
const y = e.clientY - (e.target as HTMLElement).offsetTop;
|
||||||
const element = newElement(this.state.elementType, x, y);
|
const element = newElement(
|
||||||
|
this.state.elementType,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
this.state.currentItemStrokeColor,
|
||||||
|
this.state.currentItemBackgroundColor
|
||||||
|
);
|
||||||
let isDraggingElements = false;
|
let isDraggingElements = false;
|
||||||
const cursorStyle = document.documentElement.style.cursor;
|
const cursorStyle = document.documentElement.style.cursor;
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
|
@ -634,15 +641,11 @@ class App extends React.Component<{}, AppState> {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
isDraggingElements = elements.some(
|
isDraggingElements = elements.some(element => element.isSelected);
|
||||||
element => element.isSelected
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDraggingElements) {
|
if (isDraggingElements) {
|
||||||
document.documentElement.style.cursor = "move";
|
document.documentElement.style.cursor = "move";
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
addOnBeforeUnload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
|
@ -661,8 +664,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
} = context.measureText(element.text);
|
} = context.measureText(element.text);
|
||||||
element.actualBoundingBoxAscent = actualBoundingBoxAscent;
|
element.actualBoundingBoxAscent = actualBoundingBoxAscent;
|
||||||
context.font = font;
|
context.font = font;
|
||||||
const height =
|
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
|
||||||
actualBoundingBoxAscent + actualBoundingBoxDescent;
|
|
||||||
// Center the text
|
// Center the text
|
||||||
element.x -= width / 2;
|
element.x -= width / 2;
|
||||||
element.y -= actualBoundingBoxAscent;
|
element.y -= actualBoundingBoxAscent;
|
||||||
|
@ -670,11 +672,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
element.height = height;
|
element.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateDraw(
|
generateDraw(element);
|
||||||
element,
|
|
||||||
this.state.itemStrokeColor,
|
|
||||||
this.state.itemBackgroundColor
|
|
||||||
);
|
|
||||||
elements.push(element);
|
elements.push(element);
|
||||||
if (this.state.elementType === "text") {
|
if (this.state.elementType === "text") {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -706,8 +704,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
});
|
});
|
||||||
lastX = x;
|
lastX = x;
|
||||||
lastY = y;
|
lastY = y;
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
addOnBeforeUnload();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -722,16 +719,12 @@ class App extends React.Component<{}, AppState> {
|
||||||
// Make a perfect square or circle when shift is enabled
|
// Make a perfect square or circle when shift is enabled
|
||||||
draggingElement.height = e.shiftKey ? width : height;
|
draggingElement.height = e.shiftKey ? width : height;
|
||||||
|
|
||||||
generateDraw(
|
generateDraw(draggingElement);
|
||||||
draggingElement,
|
|
||||||
this.state.itemStrokeColor,
|
|
||||||
this.state.itemBackgroundColor
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
setSelection(draggingElement);
|
setSelection(draggingElement);
|
||||||
}
|
}
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = (e: MouseEvent) => {
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
|
@ -745,7 +738,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
// if no element is clicked, clear the selection and redraw
|
// if no element is clicked, clear the selection and redraw
|
||||||
if (draggingElement === null) {
|
if (draggingElement === null) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -762,24 +755,23 @@ class App extends React.Component<{}, AppState> {
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
elementType: "selection"
|
elementType: "selection"
|
||||||
});
|
});
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
|
||||||
drawScene();
|
this.forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Colors</legend>
|
<legend>Colors</legend>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={this.state.viewBgColor}
|
value={this.state.viewBackgroundColor}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
this.setState({ viewBgColor: e.target.value });
|
this.setState({ viewBackgroundColor: e.target.value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Background
|
Background
|
||||||
|
@ -787,9 +779,9 @@ class App extends React.Component<{}, AppState> {
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={this.state.itemStrokeColor}
|
value={this.state.currentItemStrokeColor}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
this.setState({ itemStrokeColor: e.target.value });
|
this.setState({ currentItemStrokeColor: e.target.value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Shape Stroke
|
Shape Stroke
|
||||||
|
@ -797,9 +789,9 @@ class App extends React.Component<{}, AppState> {
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={this.state.itemBackgroundColor}
|
value={this.state.currentItemBackgroundColor}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
this.setState({ itemBackgroundColor: e.target.value });
|
this.setState({ currentItemBackgroundColor: e.target.value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Shape Background
|
Shape Background
|
||||||
|
@ -813,7 +805,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
exportBackground: this.state.exportBackground,
|
exportBackground: this.state.exportBackground,
|
||||||
exportVisibleOnly: this.state.exportVisibleOnly,
|
exportVisibleOnly: this.state.exportVisibleOnly,
|
||||||
exportPadding: this.state.exportPadding,
|
exportPadding: this.state.exportPadding,
|
||||||
viewBgColor: this.state.viewBgColor
|
viewBackgroundColor: this.state.viewBackgroundColor
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -850,35 +842,13 @@ class App extends React.Component<{}, AppState> {
|
||||||
/>
|
/>
|
||||||
px)
|
px)
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const fillStyle = context.fillStyle;
|
renderScene(rc, context, this.state.viewBackgroundColor);
|
||||||
context.fillStyle = this.state.viewBgColor;
|
save(this.state);
|
||||||
context.fillRect(-0.5, -0.5, canvas.width, canvas.height);
|
|
||||||
context.fillStyle = fillStyle;
|
|
||||||
|
|
||||||
elements.forEach(element => {
|
|
||||||
element.draw(rc, context);
|
|
||||||
if (element.isSelected) {
|
|
||||||
const margin = 4;
|
|
||||||
|
|
||||||
const elementX1 = getElementAbsoluteX1(element);
|
|
||||||
const elementX2 = getElementAbsoluteX2(element);
|
|
||||||
const elementY1 = getElementAbsoluteY1(element);
|
|
||||||
const elementY2 = getElementAbsoluteY2(element);
|
|
||||||
const lineDash = context.getLineDash();
|
|
||||||
context.setLineDash([8, 4]);
|
|
||||||
context.strokeRect(
|
|
||||||
elementX1 - margin,
|
|
||||||
elementY1 - margin,
|
|
||||||
elementX2 - elementX1 + margin * 2,
|
|
||||||
elementY2 - elementY1 + margin * 2
|
|
||||||
);
|
|
||||||
context.setLineDash(lineDash);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -892,10 +862,4 @@ const context = canvas.getContext("2d")!;
|
||||||
// https://stackoverflow.com/questions/13879322/drawing-a-1px-thick-line-in-canvas-creates-a-2px-thick-line/13879402#comment90766599_13879402
|
// https://stackoverflow.com/questions/13879322/drawing-a-1px-thick-line-in-canvas-creates-a-2px-thick-line/13879402#comment90766599_13879402
|
||||||
context.translate(0.5, 0.5);
|
context.translate(0.5, 0.5);
|
||||||
|
|
||||||
restore();
|
ReactDOM.render(<App />, rootElement);
|
||||||
|
|
||||||
function drawScene() {
|
|
||||||
ReactDOM.render(<App />, rootElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawScene();
|
|
||||||
|
|
|
@ -4,32 +4,24 @@
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrappers {
|
body {
|
||||||
display: flex;
|
margin: 0;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.exportWrapper {
|
/* Controls - Begin */
|
||||||
display: flex;
|
fieldset {
|
||||||
align-items: center;
|
margin: 5px;
|
||||||
}
|
|
||||||
.exportWrapper label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exportWrapper button {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
/* Controls - End */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue