Parcourir la source

TS, Prettier, Eslint (#39)

* TS, Prettier, Eslint

* Used rough ts definitions
vanilla_orig
Timur Khazamov il y a 5 ans
Parent
révision
1383758aa7
6 fichiers modifiés avec 2300 ajouts et 1465 suppressions
  1. 26
    4
      package.json
  2. 0
    529
      src/index.js
  3. 581
    0
      src/index.tsx
  4. 1
    0
      src/react-app-env.d.ts
  5. 19
    0
      tsconfig.json
  6. 1673
    932
      yarn.lock

+ 26
- 4
package.json Voir le fichier

@@ -7,11 +7,16 @@
7 7
   "dependencies": {
8 8
     "react": "16.12.0",
9 9
     "react-dom": "16.12.0",
10
-    "react-scripts": "3.0.1",
10
+    "react-scripts": "3.3.0",
11 11
     "roughjs": "3.1.0"
12 12
   },
13 13
   "devDependencies": {
14
-    "typescript": "3.3.3"
14
+    "@types/react": "16.9.17",
15
+    "@types/react-dom": "16.9.4",
16
+    "husky": "3.1.0",
17
+    "lint-staged": "9.5.0",
18
+    "prettier": "1.19.1",
19
+    "typescript": "3.7.4"
15 20
   },
16 21
   "scripts": {
17 22
     "start": "react-scripts start",
@@ -24,5 +29,22 @@
24 29
     "not dead",
25 30
     "not ie <= 11",
26 31
     "not op_mini all"
27
-  ]
28
-}
32
+  ],
33
+  "eslintConfig": {
34
+    "extends": "react-app"
35
+  },
36
+  "husky": {
37
+    "hooks": {
38
+      "pre-commit": "lint-staged"
39
+    }
40
+  },
41
+  "lint-staged": {
42
+    "*.{js,css,json,md,ts,tsx}": [
43
+      "prettier --write",
44
+      "git add"
45
+    ],
46
+    "*.{js,ts,tsx}": [
47
+      "eslint"
48
+    ]
49
+  }
50
+}

+ 0
- 529
src/index.js Voir le fichier

@@ -1,529 +0,0 @@
1
-import React from "react";
2
-import ReactDOM from "react-dom";
3
-import rough from "roughjs/dist/rough.umd.js";
4
-
5
-import "./styles.css";
6
-
7
-var elements = [];
8
-
9
-function isInsideAnElement(x, y) {
10
-  return (element) => {
11
-    const x1 = getElementAbsoluteX1(element)
12
-    const x2 = getElementAbsoluteX2(element)
13
-    const y1 = getElementAbsoluteY1(element)
14
-    const y2 = getElementAbsoluteY2(element)
15
-
16
-    return (x >= x1 && x <= x2) && (y >= y1 && y <= y2)
17
-  }
18
-}
19
-
20
-function newElement(type, x, y, width = 0, height = 0) {
21
-  const element = {
22
-    type: type,
23
-    x: x,
24
-    y: y,
25
-    width: width,
26
-    height: height,
27
-    isSelected: false
28
-  };
29
-  return element;
30
-}
31
-
32
-function exportAsPNG({
33
-  exportBackground,
34
-  exportVisibleOnly,
35
-  exportPadding = 10
36
-}) {
37
-  if ( !elements.length ) return window.alert("Cannot export empty canvas.");
38
-
39
-  // deselect & rerender
40
-
41
-  clearSelection();
42
-  drawScene();
43
-
44
-  // calculate visible-area coords
45
-
46
-  let subCanvasX1 = Infinity;
47
-  let subCanvasX2 = 0;
48
-  let subCanvasY1 = Infinity;
49
-  let subCanvasY2 = 0;
50
-
51
-  elements.forEach(element => {
52
-    subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
53
-    subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
54
-    subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
55
-    subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
56
-  });
57
-
58
-  // create temporary canvas from which we'll export
59
-
60
-  const tempCanvas = document.createElement("canvas");
61
-  const tempCanvasCtx = tempCanvas.getContext("2d");
62
-  tempCanvas.style.display = "none";
63
-  document.body.appendChild(tempCanvas);
64
-  tempCanvas.width = exportVisibleOnly
65
-    ? subCanvasX2 - subCanvasX1 + exportPadding * 2
66
-    : canvas.width;
67
-  tempCanvas.height = exportVisibleOnly
68
-    ? subCanvasY2 - subCanvasY1 + exportPadding * 2
69
-    : canvas.height;
70
-
71
-  if (exportBackground) {
72
-    tempCanvasCtx.fillStyle = "#FFF";
73
-    tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
74
-  }
75
-
76
-  // copy our original canvas onto the temp canvas
77
-  tempCanvasCtx.drawImage(
78
-    canvas, // source
79
-    exportVisibleOnly // sx
80
-      ? subCanvasX1 - exportPadding
81
-      : 0,
82
-    exportVisibleOnly // sy
83
-      ? subCanvasY1 - exportPadding
84
-      : 0,
85
-    exportVisibleOnly // sWidth
86
-      ? subCanvasX2 - subCanvasX1 + exportPadding * 2
87
-      : canvas.width,
88
-    exportVisibleOnly // sHeight
89
-      ? subCanvasY2 - subCanvasY1 + exportPadding * 2
90
-      : canvas.height,
91
-    0, // dx
92
-    0, // dy
93
-    exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
94
-    exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
95
-  );
96
-
97
-  // create a temporary <a> elem which we'll use to download the image
98
-  const link = document.createElement("a");
99
-  link.setAttribute("download", "excalibur.png");
100
-  link.setAttribute("href", tempCanvas.toDataURL("image/png"));
101
-  link.click();
102
-
103
-  // clean up the DOM
104
-  link.remove();
105
-  if (tempCanvas !== canvas) tempCanvas.remove();
106
-}
107
-
108
-function rotate(x1, y1, x2, y2, angle) {
109
-  // 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
110
-  // 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
111
-  // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
112
-  return [
113
-    (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
114
-    (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2
115
-  ];
116
-}
117
-
118
-var generator = rough.generator();
119
-
120
-function generateDraw(element) {
121
-  if (element.type === "selection") {
122
-    element.draw = (rc, context) => {
123
-      const fillStyle = context.fillStyle;
124
-      context.fillStyle = "rgba(0, 0, 255, 0.10)";
125
-      context.fillRect(element.x, element.y, element.width, element.height);
126
-      context.fillStyle = fillStyle;
127
-    };
128
-  } else if (element.type === "rectangle") {
129
-    const shape = generator.rectangle(0, 0, element.width, element.height);
130
-    element.draw = (rc, context) => {
131
-      context.translate(element.x, element.y);
132
-      rc.draw(shape);
133
-      context.translate(-element.x, -element.y);
134
-    };
135
-  } else if (element.type === "ellipse") {
136
-    const shape = generator.ellipse(
137
-      element.width / 2,
138
-      element.height / 2,
139
-      element.width,
140
-      element.height
141
-    );
142
-    element.draw = (rc, context) => {
143
-      context.translate(element.x, element.y);
144
-      rc.draw(shape);
145
-      context.translate(-element.x, -element.y);
146
-    };
147
-  } else if (element.type === "arrow") {
148
-    const x1 = 0;
149
-    const y1 = 0;
150
-    const x2 = element.width;
151
-    const y2 = element.height;
152
-
153
-    const size = 30; // pixels
154
-    const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
155
-    // Scale down the arrow until we hit a certain size so that it doesn't look weird
156
-    const minSize = Math.min(size, distance / 2);
157
-    const xs = x2 - ((x2 - x1) / distance) * minSize;
158
-    const ys = y2 - ((y2 - y1) / distance) * minSize;
159
-
160
-    const angle = 20; // degrees
161
-    const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
162
-    const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
163
-
164
-    const shapes = [
165
-      //    \
166
-      generator.line(x3, y3, x2, y2),
167
-      // -----
168
-      generator.line(x1, y1, x2, y2),
169
-      //    /
170
-      generator.line(x4, y4, x2, y2)
171
-    ];
172
-
173
-    element.draw = (rc, context) => {
174
-      context.translate(element.x, element.y);
175
-      shapes.forEach(shape => rc.draw(shape));
176
-      context.translate(-element.x, -element.y);
177
-    };
178
-    return;
179
-  } else if (element.type === "text") {
180
-    element.draw = (rc, context) => {
181
-      const font = context.font;
182
-      context.font = element.font;
183
-      context.fillText(
184
-        element.text,
185
-        element.x,
186
-        element.y + element.measure.actualBoundingBoxAscent
187
-      );
188
-      context.font = font;
189
-    };
190
-  } else {
191
-    throw new Error("Unimplemented type " + element.type);
192
-  }
193
-}
194
-
195
-// If the element is created from right to left, the width is going to be negative
196
-// This set of functions retrieves the absolute position of the 4 points.
197
-// We can't just always normalize it since we need to remember the fact that an arrow
198
-// is pointing left or right.
199
-function getElementAbsoluteX1(element) {
200
-  return element.width >= 0 ? element.x : element.x + element.width;
201
-}
202
-function getElementAbsoluteX2(element) {
203
-  return element.width >= 0 ? element.x + element.width : element.x;
204
-}
205
-function getElementAbsoluteY1(element) {
206
-  return element.height >= 0 ? element.y : element.y + element.height;
207
-}
208
-function getElementAbsoluteY2(element) {
209
-  return element.height >= 0 ? element.y + element.height : element.y;
210
-}
211
-
212
-function setSelection(selection) {
213
-  const selectionX1 = getElementAbsoluteX1(selection);
214
-  const selectionX2 = getElementAbsoluteX2(selection);
215
-  const selectionY1 = getElementAbsoluteY1(selection);
216
-  const selectionY2 = getElementAbsoluteY2(selection);
217
-  elements.forEach(element => {
218
-    const elementX1 = getElementAbsoluteX1(element);
219
-    const elementX2 = getElementAbsoluteX2(element);
220
-    const elementY1 = getElementAbsoluteY1(element);
221
-    const elementY2 = getElementAbsoluteY2(element);
222
-    element.isSelected =
223
-      element.type !== "selection" &&
224
-      selectionX1 <= elementX1 &&
225
-      selectionY1 <= elementY1 &&
226
-      selectionX2 >= elementX2 &&
227
-      selectionY2 >= elementY2;
228
-  });
229
-}
230
-
231
-function clearSelection() {
232
-  elements.forEach(element => {
233
-    element.isSelected = false;
234
-  });
235
-}
236
-
237
-class App extends React.Component {
238
-  componentDidMount() {
239
-    this.onKeyDown = event => {
240
-      if (event.key === "Backspace" && event.target.nodeName !== "INPUT") {
241
-        for (var i = elements.length - 1; i >= 0; --i) {
242
-          if (elements[i].isSelected) {
243
-            elements.splice(i, 1);
244
-          }
245
-        }
246
-        drawScene();
247
-        event.preventDefault();
248
-      } else if (
249
-        event.key === "ArrowLeft" ||
250
-        event.key === "ArrowRight" ||
251
-        event.key === "ArrowUp" ||
252
-        event.key === "ArrowDown"
253
-      ) {
254
-        const step = event.shiftKey ? 5 : 1;
255
-        elements.forEach(element => {
256
-          if (element.isSelected) {
257
-            if (event.key === "ArrowLeft") element.x -= step;
258
-            else if (event.key === "ArrowRight") element.x += step;
259
-            else if (event.key === "ArrowUp") element.y -= step;
260
-            else if (event.key === "ArrowDown") element.y += step;
261
-          }
262
-        });
263
-        drawScene();
264
-        event.preventDefault();
265
-      }
266
-    };
267
-    document.addEventListener("keydown", this.onKeyDown, false);
268
-  }
269
-
270
-  componentWillUnmount() {
271
-    document.removeEventListener("keydown", this.onKeyDown, false);
272
-  }
273
-
274
-  constructor() {
275
-    super();
276
-    this.state = {
277
-      draggingElement: null,
278
-      elementType: "selection",
279
-      exportBackground: false,
280
-      exportVisibleOnly: true,
281
-      exportPadding: 10
282
-    };
283
-  }
284
-
285
-  render() {
286
-    const ElementOption = ({ type, children }) => {
287
-      return (
288
-        <label>
289
-          <input
290
-            type="radio"
291
-            checked={this.state.elementType === type}
292
-            onChange={() => {
293
-              this.setState({ elementType: type });
294
-              clearSelection();
295
-              drawScene();
296
-            }}
297
-          />
298
-          {children}
299
-        </label>
300
-      );
301
-    };
302
-
303
-    return <>
304
-      <div className="exportWrapper">
305
-        <button onClick={() => {
306
-          exportAsPNG({
307
-            exportBackground: this.state.exportBackground,
308
-            exportVisibleOnly: this.state.exportVisibleOnly,
309
-            exportPadding: this.state.exportPadding
310
-          })
311
-        }}>Export to png</button>
312
-        <label>
313
-          <input type="checkbox"
314
-            checked={this.state.exportBackground}
315
-            onChange={e => {
316
-              this.setState({ exportBackground: e.target.checked })
317
-            }}
318
-          /> background
319
-        </label>
320
-        <label>
321
-          <input type="checkbox"
322
-            checked={this.state.exportVisibleOnly}
323
-            onChange={e => {
324
-              this.setState({ exportVisibleOnly: e.target.checked })
325
-            }}
326
-          />
327
-          visible area only
328
-        </label>
329
-        (padding:
330
-          <input type="number" value={this.state.exportPadding}
331
-            onChange={e => {
332
-              this.setState({ exportPadding: e.target.value });
333
-            }}
334
-            disabled={!this.state.exportVisibleOnly}/>
335
-        px)
336
-      </div>
337
-      <div>
338
-        {/* Can't use the <ElementOption> form because ElementOption is re-defined
339
-          on every render, which would blow up and re-create the entire DOM tree,
340
-          which in addition to being inneficient, messes up with browser text
341
-          selection */}
342
-        {ElementOption({ type: "rectangle", children: "Rectangle" })}
343
-        {ElementOption({ type: "ellipse", children: "Ellipse" })}
344
-        {ElementOption({ type: "arrow", children: "Arrow" })}
345
-        {ElementOption({ type: "text", children: "Text" })}
346
-        {ElementOption({ type: "selection", children: "Selection" })}
347
-        <canvas
348
-          id="canvas"
349
-          width={window.innerWidth}
350
-          height={window.innerHeight}
351
-          onMouseDown={e => {
352
-            const x = e.clientX - e.target.offsetLeft;
353
-            const y = e.clientY - e.target.offsetTop;
354
-            const element = newElement(this.state.elementType, x, y);
355
-            let isDraggingElements = false;
356
-            const cursorStyle = document.documentElement.style.cursor;
357
-            if (this.state.elementType === "selection") {
358
-              const selectedElement = elements.find(element => {
359
-                const isSelected = isInsideAnElement(x, y)(element)
360
-                if (isSelected) {
361
-                  element.isSelected = true
362
-                }
363
-                return isSelected
364
-              })
365
-
366
-              if (selectedElement) {
367
-                this.setState({ draggingElement: selectedElement });
368
-              } else {
369
-                clearSelection()
370
-              }
371
-
372
-              isDraggingElements = elements.some(element => element.isSelected);
373
-
374
-              if (isDraggingElements) {
375
-                document.documentElement.style.cursor = "move";
376
-              }
377
-            }
378
-
379
-            if (this.state.elementType === "text") {
380
-              const text = prompt("What text do you want?");
381
-              if (text === null) {
382
-                return;
383
-              }
384
-              element.text = text;
385
-              element.font = "20px Virgil";
386
-              const font = context.font;
387
-              context.font = element.font;
388
-              element.measure = context.measureText(element.text);
389
-              context.font = font;
390
-              const height =
391
-                element.measure.actualBoundingBoxAscent +
392
-                element.measure.actualBoundingBoxDescent;
393
-              // Center the text
394
-              element.x -= element.measure.width / 2;
395
-              element.y -= element.measure.actualBoundingBoxAscent;
396
-              element.width = element.measure.width;
397
-              element.height = height;
398
-            }
399
-
400
-            generateDraw(element);
401
-            elements.push(element);
402
-            if (this.state.elementType === "text") {
403
-              this.setState({
404
-                draggingElement: null,
405
-                elementType: "selection"
406
-              });
407
-              element.isSelected = true;
408
-            } else {
409
-              this.setState({ draggingElement: element });
410
-            }
411
-
412
-            let lastX = x;
413
-            let lastY = y;
414
-
415
-            const onMouseMove = e => {
416
-              if (isDraggingElements) {
417
-                const selectedElements = elements.filter(el => el.isSelected);
418
-                if (selectedElements.length) {
419
-                  const x = e.clientX - e.target.offsetLeft;
420
-                  const y = e.clientY - e.target.offsetTop;
421
-                  selectedElements.forEach(element => {
422
-                    element.x += x - lastX;
423
-                    element.y += y - lastY;
424
-                  });
425
-                  lastX = x;
426
-                  lastY = y;
427
-                  drawScene();
428
-                  return;
429
-                }
430
-              }
431
-
432
-              // It is very important to read this.state within each move event,
433
-              // otherwise we would read a stale one!
434
-              const draggingElement = this.state.draggingElement;
435
-              if (!draggingElement) return;
436
-              let width = e.clientX - e.target.offsetLeft - draggingElement.x;
437
-              let height = e.clientY - e.target.offsetTop - draggingElement.y;
438
-              draggingElement.width = width;
439
-              // Make a perfect square or circle when shift is enabled
440
-              draggingElement.height = e.shiftKey ? width : height;
441
-
442
-              generateDraw(draggingElement);
443
-
444
-              if (this.state.elementType === "selection") {
445
-                setSelection(draggingElement);
446
-              }
447
-              drawScene();
448
-            };
449
-
450
-            const onMouseUp = e => {
451
-              const { draggingElement, elementType } = this.state
452
-
453
-              window.removeEventListener("mousemove", onMouseMove);
454
-              window.removeEventListener("mouseup", onMouseUp);
455
-
456
-              document.documentElement.style.cursor = cursorStyle;
457
-
458
-              // if no element is clicked, clear the selection and redraw
459
-              if (draggingElement === null ) {
460
-                clearSelection()
461
-                drawScene();
462
-                return
463
-              }
464
-
465
-              if (elementType === "selection") {
466
-                if (isDraggingElements) {
467
-                  isDraggingElements = false;
468
-                }
469
-                elements.pop()
470
-              } else {
471
-                draggingElement.isSelected = true;
472
-              }
473
-
474
-              this.setState({
475
-                draggingElement: null,
476
-                elementType: "selection"
477
-              });
478
-              drawScene();
479
-            };
480
-
481
-            window.addEventListener("mousemove", onMouseMove);
482
-            window.addEventListener("mouseup", onMouseUp);
483
-
484
-            drawScene();
485
-          }}
486
-        />
487
-      </div>
488
-    </>;
489
-  }
490
-}
491
-
492
-const rootElement = document.getElementById("root");
493
-ReactDOM.render(<App />, rootElement);
494
-const canvas = document.getElementById("canvas");
495
-const rc = rough.canvas(canvas);
496
-const context = canvas.getContext("2d");
497
-
498
-// Big hack to ensure that all the 1px lines are drawn at 1px instead of 2px
499
-// https://stackoverflow.com/questions/13879322/drawing-a-1px-thick-line-in-canvas-creates-a-2px-thick-line/13879402#comment90766599_13879402
500
-context.translate(0.5, 0.5);
501
-
502
-function drawScene() {
503
-  ReactDOM.render(<App />, rootElement);
504
-
505
-  context.clearRect(-0.5, -0.5, canvas.width, canvas.height);
506
-
507
-  elements.forEach(element => {
508
-    element.draw(rc, context);
509
-    if (element.isSelected) {
510
-      const margin = 4;
511
-
512
-      const elementX1 = getElementAbsoluteX1(element);
513
-      const elementX2 = getElementAbsoluteX2(element);
514
-      const elementY1 = getElementAbsoluteY1(element);
515
-      const elementY2 = getElementAbsoluteY2(element);
516
-      const lineDash = context.getLineDash();
517
-      context.setLineDash([8, 4]);
518
-      context.strokeRect(
519
-        elementX1 - margin,
520
-        elementY1 - margin,
521
-        elementX2 - elementX1 + margin * 2,
522
-        elementY2 - elementY1 + margin * 2
523
-      );
524
-      context.setLineDash(lineDash);
525
-    }
526
-  });
527
-}
528
-
529
-drawScene();

+ 581
- 0
src/index.tsx Voir le fichier

@@ -0,0 +1,581 @@
1
+import React from "react";
2
+import ReactDOM from "react-dom";
3
+import rough from "roughjs/bin/wrappers/rough";
4
+import { RoughCanvas } from "roughjs/bin/canvas";
5
+
6
+import "./styles.css";
7
+
8
+type ExcaliburElement = ReturnType<typeof newElement>;
9
+type ExcaliburTextElement = ExcaliburElement & {
10
+  type: "text";
11
+  font: string;
12
+  text: string;
13
+  measure: TextMetrics;
14
+};
15
+
16
+var elements = Array.of<ExcaliburElement>();
17
+
18
+function isInsideAnElement(x: number, y: number) {
19
+  return (element: ExcaliburElement) => {
20
+    const x1 = getElementAbsoluteX1(element);
21
+    const x2 = getElementAbsoluteX2(element);
22
+    const y1 = getElementAbsoluteY1(element);
23
+    const y2 = getElementAbsoluteY2(element);
24
+
25
+    return x >= x1 && x <= x2 && y >= y1 && y <= y2;
26
+  };
27
+}
28
+
29
+function newElement(type: string, x: number, y: number, width = 0, height = 0) {
30
+  const element = {
31
+    type: type,
32
+    x: x,
33
+    y: y,
34
+    width: width,
35
+    height: height,
36
+    isSelected: false,
37
+    draw(rc: RoughCanvas, context: CanvasRenderingContext2D) {}
38
+  };
39
+  return element;
40
+}
41
+
42
+function exportAsPNG({
43
+  exportBackground,
44
+  exportVisibleOnly,
45
+  exportPadding = 10
46
+}: {
47
+  exportBackground: boolean;
48
+  exportVisibleOnly: boolean;
49
+  exportPadding?: number;
50
+}) {
51
+  if (!elements.length) return window.alert("Cannot export empty canvas.");
52
+
53
+  // deselect & rerender
54
+
55
+  clearSelection();
56
+  drawScene();
57
+
58
+  // calculate visible-area coords
59
+
60
+  let subCanvasX1 = Infinity;
61
+  let subCanvasX2 = 0;
62
+  let subCanvasY1 = Infinity;
63
+  let subCanvasY2 = 0;
64
+
65
+  elements.forEach(element => {
66
+    subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
67
+    subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
68
+    subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
69
+    subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
70
+  });
71
+
72
+  // create temporary canvas from which we'll export
73
+
74
+  const tempCanvas = document.createElement("canvas");
75
+  const tempCanvasCtx = tempCanvas.getContext("2d")!;
76
+  tempCanvas.style.display = "none";
77
+  document.body.appendChild(tempCanvas);
78
+  tempCanvas.width = exportVisibleOnly
79
+    ? subCanvasX2 - subCanvasX1 + exportPadding * 2
80
+    : canvas.width;
81
+  tempCanvas.height = exportVisibleOnly
82
+    ? subCanvasY2 - subCanvasY1 + exportPadding * 2
83
+    : canvas.height;
84
+
85
+  if (exportBackground) {
86
+    tempCanvasCtx.fillStyle = "#FFF";
87
+    tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
88
+  }
89
+
90
+  // copy our original canvas onto the temp canvas
91
+  tempCanvasCtx.drawImage(
92
+    canvas, // source
93
+    exportVisibleOnly // sx
94
+      ? subCanvasX1 - exportPadding
95
+      : 0,
96
+    exportVisibleOnly // sy
97
+      ? subCanvasY1 - exportPadding
98
+      : 0,
99
+    exportVisibleOnly // sWidth
100
+      ? subCanvasX2 - subCanvasX1 + exportPadding * 2
101
+      : canvas.width,
102
+    exportVisibleOnly // sHeight
103
+      ? subCanvasY2 - subCanvasY1 + exportPadding * 2
104
+      : canvas.height,
105
+    0, // dx
106
+    0, // dy
107
+    exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
108
+    exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
109
+  );
110
+
111
+  // create a temporary <a> elem which we'll use to download the image
112
+  const link = document.createElement("a");
113
+  link.setAttribute("download", "excalibur.png");
114
+  link.setAttribute("href", tempCanvas.toDataURL("image/png"));
115
+  link.click();
116
+
117
+  // clean up the DOM
118
+  link.remove();
119
+  if (tempCanvas !== canvas) tempCanvas.remove();
120
+}
121
+
122
+function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
123
+  // 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
124
+  // 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
125
+  // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
126
+  return [
127
+    (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
128
+    (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2
129
+  ];
130
+}
131
+
132
+// Casting second argument (DrawingSurface) to any,
133
+// because it is requred by TS definitions and not required at runtime
134
+var generator = rough.generator(null, null as any);
135
+
136
+function isTextElement(
137
+  element: ExcaliburElement
138
+): element is ExcaliburTextElement {
139
+  return element.type === "text";
140
+}
141
+
142
+function generateDraw(element: ExcaliburElement) {
143
+  if (element.type === "selection") {
144
+    element.draw = (rc, context) => {
145
+      const fillStyle = context.fillStyle;
146
+      context.fillStyle = "rgba(0, 0, 255, 0.10)";
147
+      context.fillRect(element.x, element.y, element.width, element.height);
148
+      context.fillStyle = fillStyle;
149
+    };
150
+  } else if (element.type === "rectangle") {
151
+    const shape = generator.rectangle(0, 0, element.width, element.height);
152
+    element.draw = (rc, context) => {
153
+      context.translate(element.x, element.y);
154
+      rc.draw(shape);
155
+      context.translate(-element.x, -element.y);
156
+    };
157
+  } else if (element.type === "ellipse") {
158
+    const shape = generator.ellipse(
159
+      element.width / 2,
160
+      element.height / 2,
161
+      element.width,
162
+      element.height
163
+    );
164
+    element.draw = (rc, context) => {
165
+      context.translate(element.x, element.y);
166
+      rc.draw(shape);
167
+      context.translate(-element.x, -element.y);
168
+    };
169
+  } else if (element.type === "arrow") {
170
+    const x1 = 0;
171
+    const y1 = 0;
172
+    const x2 = element.width;
173
+    const y2 = element.height;
174
+
175
+    const size = 30; // pixels
176
+    const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
177
+    // Scale down the arrow until we hit a certain size so that it doesn't look weird
178
+    const minSize = Math.min(size, distance / 2);
179
+    const xs = x2 - ((x2 - x1) / distance) * minSize;
180
+    const ys = y2 - ((y2 - y1) / distance) * minSize;
181
+
182
+    const angle = 20; // degrees
183
+    const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
184
+    const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
185
+
186
+    const shapes = [
187
+      //    \
188
+      generator.line(x3, y3, x2, y2),
189
+      // -----
190
+      generator.line(x1, y1, x2, y2),
191
+      //    /
192
+      generator.line(x4, y4, x2, y2)
193
+    ];
194
+
195
+    element.draw = (rc, context) => {
196
+      context.translate(element.x, element.y);
197
+      shapes.forEach(shape => rc.draw(shape));
198
+      context.translate(-element.x, -element.y);
199
+    };
200
+    return;
201
+  } else if (isTextElement(element)) {
202
+    element.draw = (rc, context) => {
203
+      const font = context.font;
204
+      context.font = element.font;
205
+      context.fillText(
206
+        element.text,
207
+        element.x,
208
+        element.y + element.measure.actualBoundingBoxAscent
209
+      );
210
+      context.font = font;
211
+    };
212
+  } else {
213
+    throw new Error("Unimplemented type " + element.type);
214
+  }
215
+}
216
+
217
+// If the element is created from right to left, the width is going to be negative
218
+// This set of functions retrieves the absolute position of the 4 points.
219
+// We can't just always normalize it since we need to remember the fact that an arrow
220
+// is pointing left or right.
221
+function getElementAbsoluteX1(element: ExcaliburElement) {
222
+  return element.width >= 0 ? element.x : element.x + element.width;
223
+}
224
+function getElementAbsoluteX2(element: ExcaliburElement) {
225
+  return element.width >= 0 ? element.x + element.width : element.x;
226
+}
227
+function getElementAbsoluteY1(element: ExcaliburElement) {
228
+  return element.height >= 0 ? element.y : element.y + element.height;
229
+}
230
+function getElementAbsoluteY2(element: ExcaliburElement) {
231
+  return element.height >= 0 ? element.y + element.height : element.y;
232
+}
233
+
234
+function setSelection(selection: ExcaliburElement) {
235
+  const selectionX1 = getElementAbsoluteX1(selection);
236
+  const selectionX2 = getElementAbsoluteX2(selection);
237
+  const selectionY1 = getElementAbsoluteY1(selection);
238
+  const selectionY2 = getElementAbsoluteY2(selection);
239
+  elements.forEach(element => {
240
+    const elementX1 = getElementAbsoluteX1(element);
241
+    const elementX2 = getElementAbsoluteX2(element);
242
+    const elementY1 = getElementAbsoluteY1(element);
243
+    const elementY2 = getElementAbsoluteY2(element);
244
+    element.isSelected =
245
+      element.type !== "selection" &&
246
+      selectionX1 <= elementX1 &&
247
+      selectionY1 <= elementY1 &&
248
+      selectionX2 >= elementX2 &&
249
+      selectionY2 >= elementY2;
250
+  });
251
+}
252
+
253
+function clearSelection() {
254
+  elements.forEach(element => {
255
+    element.isSelected = false;
256
+  });
257
+}
258
+
259
+type AppState = {
260
+  draggingElement: ExcaliburElement | null;
261
+  elementType: string;
262
+  exportBackground: boolean;
263
+  exportVisibleOnly: boolean;
264
+  exportPadding: number;
265
+};
266
+
267
+class App extends React.Component<{}, AppState> {
268
+  public componentDidMount() {
269
+    document.addEventListener("keydown", this.onKeyDown, false);
270
+  }
271
+
272
+  public componentWillUnmount() {
273
+    document.removeEventListener("keydown", this.onKeyDown, false);
274
+  }
275
+
276
+  public state: AppState = {
277
+    draggingElement: null,
278
+    elementType: "selection",
279
+    exportBackground: false,
280
+    exportVisibleOnly: true,
281
+    exportPadding: 10
282
+  };
283
+
284
+  private onKeyDown = (event: KeyboardEvent) => {
285
+    if (
286
+      event.key === "Backspace" &&
287
+      (event.target as HTMLElement)?.nodeName !== "INPUT"
288
+    ) {
289
+      for (var i = elements.length - 1; i >= 0; --i) {
290
+        if (elements[i].isSelected) {
291
+          elements.splice(i, 1);
292
+        }
293
+      }
294
+      drawScene();
295
+      event.preventDefault();
296
+    } else if (
297
+      event.key === "ArrowLeft" ||
298
+      event.key === "ArrowRight" ||
299
+      event.key === "ArrowUp" ||
300
+      event.key === "ArrowDown"
301
+    ) {
302
+      const step = event.shiftKey ? 5 : 1;
303
+      elements.forEach(element => {
304
+        if (element.isSelected) {
305
+          if (event.key === "ArrowLeft") element.x -= step;
306
+          else if (event.key === "ArrowRight") element.x += step;
307
+          else if (event.key === "ArrowUp") element.y -= step;
308
+          else if (event.key === "ArrowDown") element.y += step;
309
+        }
310
+      });
311
+      drawScene();
312
+      event.preventDefault();
313
+    }
314
+  };
315
+
316
+  private renderOption({
317
+    type,
318
+    children
319
+  }: {
320
+    type: string;
321
+    children: React.ReactNode;
322
+  }) {
323
+    return (
324
+      <label>
325
+        <input
326
+          type="radio"
327
+          checked={this.state.elementType === type}
328
+          onChange={() => {
329
+            this.setState({ elementType: type });
330
+            clearSelection();
331
+            drawScene();
332
+          }}
333
+        />
334
+        {children}
335
+      </label>
336
+    );
337
+  }
338
+
339
+  public render() {
340
+    return (
341
+      <>
342
+        <div className="exportWrapper">
343
+          <button
344
+            onClick={() => {
345
+              exportAsPNG({
346
+                exportBackground: this.state.exportBackground,
347
+                exportVisibleOnly: this.state.exportVisibleOnly,
348
+                exportPadding: this.state.exportPadding
349
+              });
350
+            }}
351
+          >
352
+            Export to png
353
+          </button>
354
+          <label>
355
+            <input
356
+              type="checkbox"
357
+              checked={this.state.exportBackground}
358
+              onChange={e => {
359
+                this.setState({ exportBackground: e.target.checked });
360
+              }}
361
+            />{" "}
362
+            background
363
+          </label>
364
+          <label>
365
+            <input
366
+              type="checkbox"
367
+              checked={this.state.exportVisibleOnly}
368
+              onChange={e => {
369
+                this.setState({ exportVisibleOnly: e.target.checked });
370
+              }}
371
+            />
372
+            visible area only
373
+          </label>
374
+          (padding:
375
+          <input
376
+            type="number"
377
+            value={this.state.exportPadding}
378
+            onChange={e => {
379
+              this.setState({ exportPadding: Number(e.target.value) });
380
+            }}
381
+            disabled={!this.state.exportVisibleOnly}
382
+          />
383
+          px)
384
+        </div>
385
+        <div>
386
+          {this.renderOption({ type: "rectangle", children: "Rectangle" })}
387
+          {this.renderOption({ type: "ellipse", children: "Ellipse" })}
388
+          {this.renderOption({ type: "arrow", children: "Arrow" })}
389
+          {this.renderOption({ type: "text", children: "Text" })}
390
+          {this.renderOption({ type: "selection", children: "Selection" })}
391
+          <canvas
392
+            id="canvas"
393
+            width={window.innerWidth}
394
+            height={window.innerHeight}
395
+            onMouseDown={e => {
396
+              const x = e.clientX - (e.target as HTMLElement).offsetLeft;
397
+              const y = e.clientY - (e.target as HTMLElement).offsetTop;
398
+              const element = newElement(this.state.elementType, x, y);
399
+              let isDraggingElements = false;
400
+              const cursorStyle = document.documentElement.style.cursor;
401
+              if (this.state.elementType === "selection") {
402
+                const selectedElement = elements.find(element => {
403
+                  const isSelected = isInsideAnElement(x, y)(element);
404
+                  if (isSelected) {
405
+                    element.isSelected = true;
406
+                  }
407
+                  return isSelected;
408
+                });
409
+
410
+                if (selectedElement) {
411
+                  this.setState({ draggingElement: selectedElement });
412
+                } else {
413
+                  clearSelection();
414
+                }
415
+
416
+                isDraggingElements = elements.some(
417
+                  element => element.isSelected
418
+                );
419
+
420
+                if (isDraggingElements) {
421
+                  document.documentElement.style.cursor = "move";
422
+                }
423
+              }
424
+
425
+              if (isTextElement(element)) {
426
+                const text = prompt("What text do you want?");
427
+                if (text === null) {
428
+                  return;
429
+                }
430
+                element.text = text;
431
+                element.font = "20px Virgil";
432
+                const font = context.font;
433
+                context.font = element.font;
434
+                element.measure = context.measureText(element.text);
435
+                context.font = font;
436
+                const height =
437
+                  element.measure.actualBoundingBoxAscent +
438
+                  element.measure.actualBoundingBoxDescent;
439
+                // Center the text
440
+                element.x -= element.measure.width / 2;
441
+                element.y -= element.measure.actualBoundingBoxAscent;
442
+                element.width = element.measure.width;
443
+                element.height = height;
444
+              }
445
+
446
+              generateDraw(element);
447
+              elements.push(element);
448
+              if (this.state.elementType === "text") {
449
+                this.setState({
450
+                  draggingElement: null,
451
+                  elementType: "selection"
452
+                });
453
+                element.isSelected = true;
454
+              } else {
455
+                this.setState({ draggingElement: element });
456
+              }
457
+
458
+              let lastX = x;
459
+              let lastY = y;
460
+
461
+              const onMouseMove = (e: MouseEvent) => {
462
+                const target = e.target;
463
+                if (!(target instanceof HTMLElement)) {
464
+                  return;
465
+                }
466
+
467
+                if (isDraggingElements) {
468
+                  const selectedElements = elements.filter(el => el.isSelected);
469
+                  if (selectedElements.length) {
470
+                    const x = e.clientX - target.offsetLeft;
471
+                    const y = e.clientY - target.offsetTop;
472
+                    selectedElements.forEach(element => {
473
+                      element.x += x - lastX;
474
+                      element.y += y - lastY;
475
+                    });
476
+                    lastX = x;
477
+                    lastY = y;
478
+                    drawScene();
479
+                    return;
480
+                  }
481
+                }
482
+
483
+                // It is very important to read this.state within each move event,
484
+                // otherwise we would read a stale one!
485
+                const draggingElement = this.state.draggingElement;
486
+                if (!draggingElement) return;
487
+                let width = e.clientX - target.offsetLeft - draggingElement.x;
488
+                let height = e.clientY - target.offsetTop - draggingElement.y;
489
+                draggingElement.width = width;
490
+                // Make a perfect square or circle when shift is enabled
491
+                draggingElement.height = e.shiftKey ? width : height;
492
+
493
+                generateDraw(draggingElement);
494
+
495
+                if (this.state.elementType === "selection") {
496
+                  setSelection(draggingElement);
497
+                }
498
+                drawScene();
499
+              };
500
+
501
+              const onMouseUp = (e: MouseEvent) => {
502
+                const { draggingElement, elementType } = this.state;
503
+
504
+                window.removeEventListener("mousemove", onMouseMove);
505
+                window.removeEventListener("mouseup", onMouseUp);
506
+
507
+                document.documentElement.style.cursor = cursorStyle;
508
+
509
+                // if no element is clicked, clear the selection and redraw
510
+                if (draggingElement === null) {
511
+                  clearSelection();
512
+                  drawScene();
513
+                  return;
514
+                }
515
+
516
+                if (elementType === "selection") {
517
+                  if (isDraggingElements) {
518
+                    isDraggingElements = false;
519
+                  }
520
+                  elements.pop();
521
+                } else {
522
+                  draggingElement.isSelected = true;
523
+                }
524
+
525
+                this.setState({
526
+                  draggingElement: null,
527
+                  elementType: "selection"
528
+                });
529
+                drawScene();
530
+              };
531
+
532
+              window.addEventListener("mousemove", onMouseMove);
533
+              window.addEventListener("mouseup", onMouseUp);
534
+
535
+              drawScene();
536
+            }}
537
+          />
538
+        </div>
539
+      </>
540
+    );
541
+  }
542
+}
543
+
544
+const rootElement = document.getElementById("root");
545
+ReactDOM.render(<App />, rootElement);
546
+const canvas = document.getElementById("canvas") as HTMLCanvasElement;
547
+const rc = rough.canvas(canvas);
548
+const context = canvas.getContext("2d")!;
549
+
550
+// Big hack to ensure that all the 1px lines are drawn at 1px instead of 2px
551
+// https://stackoverflow.com/questions/13879322/drawing-a-1px-thick-line-in-canvas-creates-a-2px-thick-line/13879402#comment90766599_13879402
552
+context.translate(0.5, 0.5);
553
+
554
+function drawScene() {
555
+  ReactDOM.render(<App />, rootElement);
556
+
557
+  context.clearRect(-0.5, -0.5, canvas.width, canvas.height);
558
+
559
+  elements.forEach(element => {
560
+    element.draw(rc, context);
561
+    if (element.isSelected) {
562
+      const margin = 4;
563
+
564
+      const elementX1 = getElementAbsoluteX1(element);
565
+      const elementX2 = getElementAbsoluteX2(element);
566
+      const elementY1 = getElementAbsoluteY1(element);
567
+      const elementY2 = getElementAbsoluteY2(element);
568
+      const lineDash = context.getLineDash();
569
+      context.setLineDash([8, 4]);
570
+      context.strokeRect(
571
+        elementX1 - margin,
572
+        elementY1 - margin,
573
+        elementX2 - elementX1 + margin * 2,
574
+        elementY2 - elementY1 + margin * 2
575
+      );
576
+      context.setLineDash(lineDash);
577
+    }
578
+  });
579
+}
580
+
581
+drawScene();

+ 1
- 0
src/react-app-env.d.ts Voir le fichier

@@ -0,0 +1 @@
1
+/// <reference types="react-scripts" />

+ 19
- 0
tsconfig.json Voir le fichier

@@ -0,0 +1,19 @@
1
+{
2
+  "compilerOptions": {
3
+    "target": "es5",
4
+    "lib": ["dom", "dom.iterable", "esnext"],
5
+    "allowJs": true,
6
+    "skipLibCheck": true,
7
+    "esModuleInterop": true,
8
+    "allowSyntheticDefaultImports": true,
9
+    "strict": true,
10
+    "forceConsistentCasingInFileNames": true,
11
+    "module": "esnext",
12
+    "moduleResolution": "node",
13
+    "resolveJsonModule": true,
14
+    "isolatedModules": true,
15
+    "noEmit": true,
16
+    "jsx": "react"
17
+  },
18
+  "include": ["src"]
19
+}

+ 1673
- 932
yarn.lock
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


Chargement…
Annuler
Enregistrer