You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import React from "react";
  2. import ReactDOM from "react-dom";
  3. import rough from "roughjs/dist/rough.umd.js";
  4. import "./styles.css";
  5. var elements = [];
  6. function newElement(type, x, y) {
  7. const element = {
  8. type: type,
  9. x: x,
  10. y: y,
  11. width: 0,
  12. height: 0,
  13. isSelected: false
  14. };
  15. return element;
  16. }
  17. function rotate(x1, y1, x2, y2, angle) {
  18. // 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
  19. // 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
  20. // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
  21. return [
  22. (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
  23. (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2
  24. ];
  25. }
  26. var generator = rough.generator();
  27. function generateShape(element) {
  28. if (element.type === "selection") {
  29. element.draw = (rc, context) => {
  30. const fillStyle = context.fillStyle;
  31. context.fillStyle = "rgba(0, 0, 255, 0.10)";
  32. context.fillRect(element.x, element.y, element.width, element.height);
  33. context.fillStyle = fillStyle;
  34. };
  35. } else if (element.type === "rectangle") {
  36. const shape = generator.rectangle(
  37. element.x,
  38. element.y,
  39. element.width,
  40. element.height
  41. );
  42. element.draw = (rc, context) => {
  43. rc.draw(shape);
  44. };
  45. } else if (element.type === "ellipse") {
  46. const shape = generator.ellipse(
  47. element.x + element.width / 2,
  48. element.y + element.height / 2,
  49. element.width,
  50. element.height
  51. );
  52. element.draw = (rc, context) => {
  53. rc.draw(shape);
  54. };
  55. } else if (element.type === "arrow") {
  56. const x1 = element.x;
  57. const y1 = element.y;
  58. const x2 = element.x + element.width;
  59. const y2 = element.y + element.height;
  60. const size = 30; // pixels
  61. const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  62. // Scale down the arrow until we hit a certain size so that it doesn't look weird
  63. const minSize = Math.min(size, distance / 2);
  64. const xs = x2 - ((x2 - x1) / distance) * minSize;
  65. const ys = y2 - ((y2 - y1) / distance) * minSize;
  66. const angle = 20; // degrees
  67. const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
  68. const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
  69. const shapes = [
  70. generator.line(x1, y1, x2, y2),
  71. generator.line(x3, y3, x2, y2),
  72. generator.line(x4, y4, x2, y2)
  73. ];
  74. element.draw = (rc, context) => {
  75. shapes.forEach(shape => rc.draw(shape));
  76. };
  77. return;
  78. } else if (element.type === "text") {
  79. element.draw = (rc, context) => {
  80. const font = context.font;
  81. context.font = element.font;
  82. const height =
  83. element.measure.actualBoundingBoxAscent +
  84. element.measure.actualBoundingBoxDescent;
  85. context.fillText(
  86. element.text,
  87. element.x,
  88. element.y + 2 * element.measure.actualBoundingBoxAscent - height / 2
  89. );
  90. context.font = font;
  91. };
  92. } else {
  93. throw new Error("Unimplemented type " + element.type);
  94. }
  95. }
  96. function setSelection(selection) {
  97. elements.forEach(element => {
  98. element.isSelected =
  99. selection.x < element.x &&
  100. selection.y < element.y &&
  101. selection.x + selection.width > element.x + element.width &&
  102. selection.y + selection.height > element.y + element.height;
  103. });
  104. }
  105. function App() {
  106. const [draggingElement, setDraggingElement] = React.useState(null);
  107. const [elementType, setElementType] = React.useState("selection");
  108. const onKeyDown = React.useCallback(event => {
  109. if (event.key === "Backspace") {
  110. for (var i = elements.length - 1; i >= 0; --i) {
  111. if (elements[i].isSelected) {
  112. elements.splice(i, 1);
  113. }
  114. }
  115. drawScene();
  116. }
  117. }, []);
  118. React.useEffect(() => {
  119. document.addEventListener("keydown", onKeyDown, false);
  120. return () => {
  121. document.removeEventListener("keydown", onKeyDown, false);
  122. };
  123. }, [onKeyDown]);
  124. function ElementOption({ type, children }) {
  125. return (
  126. <label>
  127. <input
  128. type="radio"
  129. checked={elementType === type}
  130. onChange={() => setElementType(type)}
  131. />
  132. {children}
  133. </label>
  134. );
  135. }
  136. return (
  137. <div>
  138. {/* If using a component, dragging on the canvas also selects the label text which is annoying.
  139. Not sure why that's happening */}
  140. {ElementOption({ type: "rectangle", children: "Rectangle" })}
  141. {ElementOption({ type: "ellipse", children: "Ellipse" })}
  142. {ElementOption({ type: "arrow", children: "Arrow" })}
  143. {ElementOption({ type: "text", children: "Text" })}
  144. {ElementOption({ type: "selection", children: "Selection" })}
  145. <canvas
  146. id="canvas"
  147. width={window.innerWidth}
  148. height={window.innerHeight}
  149. onMouseDown={e => {
  150. const x = e.clientX - e.target.offsetLeft;
  151. const y = e.clientY - e.target.offsetTop;
  152. const element = newElement(elementType, x, y);
  153. if (elementType === "text") {
  154. element.text = prompt("What text do you want?");
  155. element.font = "20px Virgil";
  156. const font = context.font;
  157. context.font = element.font;
  158. element.measure = context.measureText(element.text);
  159. context.font = font;
  160. const height =
  161. element.measure.actualBoundingBoxAscent +
  162. element.measure.actualBoundingBoxDescent;
  163. // Center the text
  164. element.x -= element.measure.width / 2;
  165. element.y -= element.measure.actualBoundingBoxAscent;
  166. element.width = element.measure.width;
  167. element.height = height;
  168. }
  169. generateShape(element);
  170. elements.push(element);
  171. if (elementType === "text") {
  172. setDraggingElement(null);
  173. } else {
  174. setDraggingElement(element);
  175. }
  176. drawScene();
  177. }}
  178. onMouseUp={e => {
  179. setDraggingElement(null);
  180. if (elementType === "selection") {
  181. // Remove actual selection element
  182. elements.pop();
  183. setSelection(draggingElement);
  184. }
  185. drawScene();
  186. }}
  187. onMouseMove={e => {
  188. if (!draggingElement) return;
  189. let width = e.clientX - e.target.offsetLeft - draggingElement.x;
  190. let height = e.clientY - e.target.offsetTop - draggingElement.y;
  191. draggingElement.width = width;
  192. // Make a perfect square or circle when shift is enabled
  193. draggingElement.height = e.shiftKey ? width : height;
  194. generateShape(draggingElement);
  195. if (elementType === "selection") {
  196. setSelection(draggingElement);
  197. }
  198. drawScene();
  199. }}
  200. />
  201. </div>
  202. );
  203. }
  204. const rootElement = document.getElementById("root");
  205. ReactDOM.render(<App />, rootElement);
  206. const canvas = document.getElementById("canvas");
  207. const rc = rough.canvas(canvas);
  208. const context = canvas.getContext("2d");
  209. function drawScene() {
  210. ReactDOM.render(<App />, rootElement);
  211. context.clearRect(0, 0, canvas.width, canvas.height);
  212. elements.forEach(element => {
  213. element.draw(rc, context);
  214. if (element.isSelected) {
  215. const margin = 4;
  216. const lineDash = context.getLineDash();
  217. context.setLineDash([8, 4]);
  218. context.strokeRect(
  219. element.x - margin,
  220. element.y - margin,
  221. element.width + margin * 2,
  222. element.height + margin * 2
  223. );
  224. context.setLineDash(lineDash);
  225. }
  226. });
  227. }
  228. drawScene();