6 changed files with 263 additions and 2 deletions
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
{ |
||||
"name": "react", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"keywords": [], |
||||
"main": "src/index.js", |
||||
"dependencies": { |
||||
"react": "16.12.0", |
||||
"react-dom": "16.12.0", |
||||
"react-scripts": "3.0.1", |
||||
"roughjs": "3.1.0" |
||||
}, |
||||
"devDependencies": { |
||||
"typescript": "3.3.3" |
||||
}, |
||||
"scripts": { |
||||
"start": "react-scripts start", |
||||
"build": "react-scripts build", |
||||
"test": "react-scripts test --env=jsdom", |
||||
"eject": "react-scripts eject" |
||||
}, |
||||
"browserslist": [ |
||||
">0.2%", |
||||
"not dead", |
||||
"not ie <= 11", |
||||
"not op_mini all" |
||||
] |
||||
} |
||||
Binary file not shown.
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
<meta name="theme-color" content="#000000"> |
||||
<!-- |
||||
manifest.json provides metadata used when your web app is added to the |
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ |
||||
--> |
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"> |
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> |
||||
<!-- |
||||
Notice the use of %PUBLIC_URL% in the tags above. |
||||
It will be replaced with the URL of the `public` folder during the build. |
||||
Only files inside the `public` folder can be referenced from the HTML. |
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||
work correctly both with client-side routing and a non-root public URL. |
||||
Learn how to configure a non-root public URL by running `npm run build`. |
||||
--> |
||||
<title>React App</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<noscript> |
||||
You need to enable JavaScript to run this app. |
||||
</noscript> |
||||
<div id="root"></div> |
||||
<!-- |
||||
This HTML file is a template. |
||||
If you open it directly in the browser, you will see an empty page. |
||||
|
||||
You can add webfonts, meta tags, or analytics to this file. |
||||
The build step will place the bundled scripts into the <body> tag. |
||||
|
||||
To begin the development, run `npm start` or `yarn start`. |
||||
To create a production bundle, use `npm run build` or `yarn build`. |
||||
--> |
||||
</body> |
||||
|
||||
</html> |
||||
@ -0,0 +1,187 @@
@@ -0,0 +1,187 @@
|
||||
import React from "react"; |
||||
import ReactDOM from "react-dom"; |
||||
import rough from "roughjs/dist/rough.umd.js"; |
||||
|
||||
import "./styles.css"; |
||||
|
||||
var elements = []; |
||||
|
||||
function newElement(type, x, y) { |
||||
const element = { |
||||
type: type, |
||||
x: x, |
||||
y: y, |
||||
width: 0, |
||||
height: 0 |
||||
}; |
||||
generateShape(element); |
||||
return element; |
||||
} |
||||
|
||||
function rotate(x1, y1, x2, y2, angle) { |
||||
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
||||
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
||||
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||
return [ |
||||
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2, |
||||
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2 |
||||
]; |
||||
} |
||||
|
||||
var generator = rough.generator(); |
||||
|
||||
function generateShape(element) { |
||||
if (element.type === "selection") { |
||||
element.draw = (rc, context) => { |
||||
context.fillStyle = "rgba(0, 0, 255, 0.10)"; |
||||
context.fillRect(element.x, element.y, element.width, element.height); |
||||
}; |
||||
} else if (element.type === "rectangle") { |
||||
const shape = generator.rectangle( |
||||
element.x, |
||||
element.y, |
||||
element.width, |
||||
element.height |
||||
); |
||||
element.draw = (rc, context) => { |
||||
rc.draw(shape); |
||||
}; |
||||
} else if (element.type === "ellipse") { |
||||
const shape = generator.ellipse( |
||||
element.x + element.width / 2, |
||||
element.y + element.height / 2, |
||||
element.width, |
||||
element.height |
||||
); |
||||
element.draw = (rc, context) => { |
||||
rc.draw(shape); |
||||
}; |
||||
} else if (element.type === "arrow") { |
||||
const x1 = element.x; |
||||
const y1 = element.y; |
||||
const x2 = element.x + element.width; |
||||
const y2 = element.y + element.height; |
||||
|
||||
const size = 30; // pixels
|
||||
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); |
||||
// Scale down the arrow until we hit a certain size so that it doesn't look weird
|
||||
const minSize = Math.min(size, distance / 2); |
||||
const xs = x2 - ((x2 - x1) / distance) * minSize; |
||||
const ys = y2 - ((y2 - y1) / distance) * minSize; |
||||
|
||||
const angle = 20; // degrees
|
||||
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180); |
||||
const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180); |
||||
|
||||
const shapes = [ |
||||
generator.line(x1, y1, x2, y2), |
||||
generator.line(x3, y3, x2, y2), |
||||
generator.line(x4, y4, x2, y2) |
||||
]; |
||||
|
||||
element.draw = (rc, context) => { |
||||
shapes.forEach(shape => rc.draw(shape)); |
||||
}; |
||||
return; |
||||
} else if (element.type === "text") { |
||||
if (element.text === undefined) { |
||||
element.text = prompt("What text do you want?"); |
||||
} |
||||
element.draw = (rc, context) => { |
||||
context.font = "20px Virgil"; |
||||
const measure = context.measureText(element.text); |
||||
const height = |
||||
measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent; |
||||
context.fillText( |
||||
element.text, |
||||
element.x - measure.width / 2, |
||||
element.y + measure.actualBoundingBoxAscent - height / 2 |
||||
); |
||||
}; |
||||
} else { |
||||
throw new Error("Unimplemented type " + element.type); |
||||
} |
||||
} |
||||
|
||||
function App() { |
||||
const [draggingElement, setDraggingElement] = React.useState(null); |
||||
const [elementType, setElementType] = React.useState("selection"); |
||||
const [selectedElements, setSelectedElements] = React.useState([]); |
||||
function ElementOption({ type, children }) { |
||||
return ( |
||||
<label> |
||||
<input |
||||
type="radio" |
||||
checked={elementType === type} |
||||
onChange={() => setElementType(type)} |
||||
/> |
||||
{children} |
||||
</label> |
||||
); |
||||
} |
||||
return ( |
||||
<div> |
||||
<ElementOption type="rectangle">Rectangle</ElementOption> |
||||
<ElementOption type="ellipse">Ellipse</ElementOption> |
||||
<ElementOption type="arrow">Arrow</ElementOption> |
||||
<ElementOption type="text">Text</ElementOption> |
||||
<ElementOption type="selection">Selection</ElementOption> |
||||
<canvas |
||||
id="canvas" |
||||
width={window.innerWidth} |
||||
height={window.innerHeight} |
||||
onMouseDown={e => { |
||||
const element = newElement( |
||||
elementType, |
||||
e.clientX - e.target.offsetLeft, |
||||
e.clientY - e.target.offsetTop |
||||
); |
||||
elements.push(element); |
||||
setDraggingElement(element); |
||||
drawScene(); |
||||
}} |
||||
onMouseUp={e => { |
||||
setDraggingElement(null); |
||||
drawScene(); |
||||
}} |
||||
onMouseMove={e => { |
||||
if (!draggingElement) return; |
||||
let width = e.clientX - e.target.offsetLeft - draggingElement.x; |
||||
let height = e.clientY - e.target.offsetTop - draggingElement.y; |
||||
draggingElement.width = width; |
||||
// Make a perfect square or circle when shift is enabled
|
||||
draggingElement.height = e.shiftKey ? width : height; |
||||
generateShape(draggingElement); |
||||
drawScene(); |
||||
}} |
||||
/> |
||||
</div> |
||||
); |
||||
} |
||||
const rootElement = document.getElementById("root"); |
||||
|
||||
function drawScene() { |
||||
ReactDOM.render(<App />, rootElement); |
||||
|
||||
const canvas = document.getElementById("canvas"); |
||||
const rc = rough.canvas(canvas); |
||||
const context = canvas.getContext("2d"); |
||||
context.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
||||
elements.forEach(element => { |
||||
element.draw(rc, context); |
||||
if (true || element.isSelected) { |
||||
const margin = 4; |
||||
context.setLineDash([8, 4]); |
||||
context.strokeRect( |
||||
element.x - margin, |
||||
element.y - margin, |
||||
element.width + margin * 2, |
||||
element.height + margin * 2 |
||||
); |
||||
context.setLineDash([]); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
drawScene(); |
||||
Loading…
Reference in new issue