Elements are now inside the react scope

This commit is contained in:
André Bastos 2020-01-02 22:37:57 +00:00
parent 1383758aa7
commit b2b9b56eb2

View file

@ -5,15 +5,15 @@ import { RoughCanvas } from "roughjs/bin/canvas";
import "./styles.css"; import "./styles.css";
type ExcaliburElement = ReturnType<typeof newElement>; export type ExcaliburElement = ReturnType<typeof newElement>;
type ExcaliburTextElement = ExcaliburElement & { export type ExcaliburTextElement = ExcaliburElement & {
type: "text"; type: "text";
font: string; font: string;
text: string; text: string;
measure: TextMetrics; measure: TextMetrics;
}; };
var elements = Array.of<ExcaliburElement>(); // var elements = Array.of<ExcaliburElement>();
function isInsideAnElement(x: number, y: number) { function isInsideAnElement(x: number, y: number) {
return (element: ExcaliburElement) => { return (element: ExcaliburElement) => {
@ -39,86 +39,6 @@ function newElement(type: string, x: number, y: number, width = 0, height = 0) {
return element; return element;
} }
function exportAsPNG({
exportBackground,
exportVisibleOnly,
exportPadding = 10
}: {
exportBackground: boolean;
exportVisibleOnly: boolean;
exportPadding?: number;
}) {
if (!elements.length) return window.alert("Cannot export empty canvas.");
// deselect & rerender
clearSelection();
drawScene();
// calculate visible-area coords
let subCanvasX1 = Infinity;
let subCanvasX2 = 0;
let subCanvasY1 = Infinity;
let subCanvasY2 = 0;
elements.forEach(element => {
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
});
// create temporary canvas from which we'll export
const tempCanvas = document.createElement("canvas");
const tempCanvasCtx = tempCanvas.getContext("2d")!;
tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas);
tempCanvas.width = exportVisibleOnly
? subCanvasX2 - subCanvasX1 + exportPadding * 2
: canvas.width;
tempCanvas.height = exportVisibleOnly
? subCanvasY2 - subCanvasY1 + exportPadding * 2
: canvas.height;
if (exportBackground) {
tempCanvasCtx.fillStyle = "#FFF";
tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
}
// copy our original canvas onto the temp canvas
tempCanvasCtx.drawImage(
canvas, // source
exportVisibleOnly // sx
? subCanvasX1 - exportPadding
: 0,
exportVisibleOnly // sy
? subCanvasY1 - exportPadding
: 0,
exportVisibleOnly // sWidth
? subCanvasX2 - subCanvasX1 + exportPadding * 2
: canvas.width,
exportVisibleOnly // sHeight
? subCanvasY2 - subCanvasY1 + exportPadding * 2
: canvas.height,
0, // dx
0, // dy
exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
);
// create a temporary <a> elem which we'll use to download the image
const link = document.createElement("a");
link.setAttribute("download", "excalibur.png");
link.setAttribute("href", tempCanvas.toDataURL("image/png"));
link.click();
// clean up the DOM
link.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) {
// 𝑎𝑥=(𝑎𝑥𝑐𝑥)cos𝜃(𝑎𝑦𝑐𝑦)sin𝜃+𝑐𝑥 // 𝑎𝑥=(𝑎𝑥𝑐𝑥)cos𝜃(𝑎𝑦𝑐𝑦)sin𝜃+𝑐𝑥
// 𝑎𝑦=(𝑎𝑥𝑐𝑥)sin𝜃+(𝑎𝑦𝑐𝑦)cos𝜃+𝑐𝑦. // 𝑎𝑦=(𝑎𝑥𝑐𝑥)sin𝜃+(𝑎𝑦𝑐𝑦)cos𝜃+𝑐𝑦.
@ -231,33 +151,9 @@ function getElementAbsoluteY2(element: ExcaliburElement) {
return element.height >= 0 ? element.y + element.height : element.y; return element.height >= 0 ? element.y + element.height : element.y;
} }
function setSelection(selection: ExcaliburElement) {
const selectionX1 = getElementAbsoluteX1(selection);
const selectionX2 = getElementAbsoluteX2(selection);
const selectionY1 = getElementAbsoluteY1(selection);
const selectionY2 = getElementAbsoluteY2(selection);
elements.forEach(element => {
const elementX1 = getElementAbsoluteX1(element);
const elementX2 = getElementAbsoluteX2(element);
const elementY1 = getElementAbsoluteY1(element);
const elementY2 = getElementAbsoluteY2(element);
element.isSelected =
element.type !== "selection" &&
selectionX1 <= elementX1 &&
selectionY1 <= elementY1 &&
selectionX2 >= elementX2 &&
selectionY2 >= elementY2;
});
}
function clearSelection() {
elements.forEach(element => {
element.isSelected = false;
});
}
type AppState = { type AppState = {
draggingElement: ExcaliburElement | null; draggingElement: ExcaliburElement | null;
elements: ExcaliburElement[];
elementType: string; elementType: string;
exportBackground: boolean; exportBackground: boolean;
exportVisibleOnly: boolean; exportVisibleOnly: boolean;
@ -275,13 +171,45 @@ class App extends React.Component<{}, AppState> {
public state: AppState = { public state: AppState = {
draggingElement: null, draggingElement: null,
elements: [],
elementType: "selection", elementType: "selection",
exportBackground: false, exportBackground: false,
exportVisibleOnly: true, exportVisibleOnly: true,
exportPadding: 10 exportPadding: 10
}; };
clearSelection = () => {
const newElements = [...this.state.elements];
newElements.map(element => {
element.isSelected = false;
return element;
});
this.setState({ elements: newElements });
};
setSelection = (selection: ExcaliburElement) => {
const selectionX1 = getElementAbsoluteX1(selection);
const selectionX2 = getElementAbsoluteX2(selection);
const selectionY1 = getElementAbsoluteY1(selection);
const selectionY2 = getElementAbsoluteY2(selection);
const newElements = this.state.elements.map(element => {
const elementX1 = getElementAbsoluteX1(element);
const elementX2 = getElementAbsoluteX2(element);
const elementY1 = getElementAbsoluteY1(element);
const elementY2 = getElementAbsoluteY2(element);
element.isSelected =
element.type !== "selection" &&
selectionX1 <= elementX1 &&
selectionY1 <= elementY1 &&
selectionX2 >= elementX2 &&
selectionY2 >= elementY2;
return element;
});
this.setState({ elements: newElements });
};
private onKeyDown = (event: KeyboardEvent) => { private onKeyDown = (event: KeyboardEvent) => {
const { elements } = this.state;
if ( if (
event.key === "Backspace" && event.key === "Backspace" &&
(event.target as HTMLElement)?.nodeName !== "INPUT" (event.target as HTMLElement)?.nodeName !== "INPUT"
@ -291,7 +219,7 @@ class App extends React.Component<{}, AppState> {
elements.splice(i, 1); elements.splice(i, 1);
} }
} }
drawScene(); drawScene(this.state.elements);
event.preventDefault(); event.preventDefault();
} else if ( } else if (
event.key === "ArrowLeft" || event.key === "ArrowLeft" ||
@ -308,7 +236,7 @@ class App extends React.Component<{}, AppState> {
else if (event.key === "ArrowDown") element.y += step; else if (event.key === "ArrowDown") element.y += step;
} }
}); });
drawScene(); drawScene(this.state.elements);
event.preventDefault(); event.preventDefault();
} }
}; };
@ -327,8 +255,8 @@ class App extends React.Component<{}, AppState> {
checked={this.state.elementType === type} checked={this.state.elementType === type}
onChange={() => { onChange={() => {
this.setState({ elementType: type }); this.setState({ elementType: type });
clearSelection(); this.clearSelection();
drawScene(); drawScene(this.state.elements);
}} }}
/> />
{children} {children}
@ -336,13 +264,95 @@ class App extends React.Component<{}, AppState> {
); );
} }
exportAsPNG = ({
exportBackground,
exportVisibleOnly,
exportPadding = 10
}: {
exportBackground: boolean;
exportVisibleOnly: boolean;
exportPadding?: number;
}) => {
const { elements } = this.state;
if (!elements.length) return window.alert("Cannot export empty canvas.");
// deselect & rerender
this.clearSelection();
drawScene(elements);
// calculate visible-area coords
let subCanvasX1 = Infinity;
let subCanvasX2 = 0;
let subCanvasY1 = Infinity;
let subCanvasY2 = 0;
elements.forEach(element => {
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
});
// create temporary canvas from which we'll export
const tempCanvas = document.createElement("canvas");
const tempCanvasCtx = tempCanvas.getContext("2d")!;
tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas);
tempCanvas.width = exportVisibleOnly
? subCanvasX2 - subCanvasX1 + exportPadding * 2
: canvas.width;
tempCanvas.height = exportVisibleOnly
? subCanvasY2 - subCanvasY1 + exportPadding * 2
: canvas.height;
if (exportBackground) {
tempCanvasCtx.fillStyle = "#FFF";
tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
}
// copy our original canvas onto the temp canvas
tempCanvasCtx.drawImage(
canvas, // source
exportVisibleOnly // sx
? subCanvasX1 - exportPadding
: 0,
exportVisibleOnly // sy
? subCanvasY1 - exportPadding
: 0,
exportVisibleOnly // sWidth
? subCanvasX2 - subCanvasX1 + exportPadding * 2
: canvas.width,
exportVisibleOnly // sHeight
? subCanvasY2 - subCanvasY1 + exportPadding * 2
: canvas.height,
0, // dx
0, // dy
exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
);
// create a temporary <a> elem which we'll use to download the image
const link = document.createElement("a");
link.setAttribute("download", "excalibur.png");
link.setAttribute("href", tempCanvas.toDataURL("image/png"));
link.click();
// clean up the DOM
link.remove();
if (tempCanvas !== canvas) tempCanvas.remove();
};
public render() { public render() {
return ( return (
<> <>
<div className="exportWrapper"> <div className="exportWrapper">
<button <button
onClick={() => { onClick={() => {
exportAsPNG({ this.exportAsPNG({
exportBackground: this.state.exportBackground, exportBackground: this.state.exportBackground,
exportVisibleOnly: this.state.exportVisibleOnly, exportVisibleOnly: this.state.exportVisibleOnly,
exportPadding: this.state.exportPadding exportPadding: this.state.exportPadding
@ -399,7 +409,7 @@ class App extends React.Component<{}, AppState> {
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") {
const selectedElement = elements.find(element => { const selectedElement = this.state.elements.find(element => {
const isSelected = isInsideAnElement(x, y)(element); const isSelected = isInsideAnElement(x, y)(element);
if (isSelected) { if (isSelected) {
element.isSelected = true; element.isSelected = true;
@ -410,10 +420,10 @@ class App extends React.Component<{}, AppState> {
if (selectedElement) { if (selectedElement) {
this.setState({ draggingElement: selectedElement }); this.setState({ draggingElement: selectedElement });
} else { } else {
clearSelection(); this.clearSelection();
} }
isDraggingElements = elements.some( isDraggingElements = this.state.elements.some(
element => element.isSelected element => element.isSelected
); );
@ -444,7 +454,12 @@ class App extends React.Component<{}, AppState> {
} }
generateDraw(element); generateDraw(element);
elements.push(element);
// generate new elements array
this.setState({
elements: [...this.state.elements, element]
});
// elements.push(element);
if (this.state.elementType === "text") { if (this.state.elementType === "text") {
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
@ -465,7 +480,9 @@ class App extends React.Component<{}, AppState> {
} }
if (isDraggingElements) { if (isDraggingElements) {
const selectedElements = elements.filter(el => el.isSelected); const selectedElements = this.state.elements.filter(
el => el.isSelected
);
if (selectedElements.length) { if (selectedElements.length) {
const x = e.clientX - target.offsetLeft; const x = e.clientX - target.offsetLeft;
const y = e.clientY - target.offsetTop; const y = e.clientY - target.offsetTop;
@ -475,7 +492,7 @@ class App extends React.Component<{}, AppState> {
}); });
lastX = x; lastX = x;
lastY = y; lastY = y;
drawScene(); drawScene(this.state.elements);
return; return;
} }
} }
@ -493,9 +510,9 @@ class App extends React.Component<{}, AppState> {
generateDraw(draggingElement); generateDraw(draggingElement);
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
setSelection(draggingElement); this.setSelection(draggingElement);
} }
drawScene(); drawScene(this.state.elements);
}; };
const onMouseUp = (e: MouseEvent) => { const onMouseUp = (e: MouseEvent) => {
@ -508,8 +525,8 @@ 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(); this.clearSelection();
drawScene(); drawScene(this.state.elements);
return; return;
} }
@ -517,7 +534,9 @@ class App extends React.Component<{}, AppState> {
if (isDraggingElements) { if (isDraggingElements) {
isDraggingElements = false; isDraggingElements = false;
} }
elements.pop(); const newElements = this.state.elements;
newElements.pop();
this.setState({ elements: newElements });
} else { } else {
draggingElement.isSelected = true; draggingElement.isSelected = true;
} }
@ -526,13 +545,13 @@ class App extends React.Component<{}, AppState> {
draggingElement: null, draggingElement: null,
elementType: "selection" elementType: "selection"
}); });
drawScene(); drawScene(this.state.elements);
}; };
window.addEventListener("mousemove", onMouseMove); window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp); window.addEventListener("mouseup", onMouseUp);
drawScene(); drawScene(this.state.elements);
}} }}
/> />
</div> </div>
@ -551,7 +570,7 @@ 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);
function drawScene() { function drawScene(elements: ExcaliburElement[]) {
ReactDOM.render(<App />, rootElement); ReactDOM.render(<App />, rootElement);
context.clearRect(-0.5, -0.5, canvas.width, canvas.height); context.clearRect(-0.5, -0.5, canvas.width, canvas.height);
@ -578,4 +597,4 @@ function drawScene() {
}); });
} }
drawScene(); drawScene([]);