Browse Source

Extract element functions into modules (#207)

vanilla_orig
Gasim Gasimzada 5 years ago
parent
commit
01805f734d
No account linked to committer's email address

+ 1
- 0
.gitignore View File

@@ -17,3 +17,4 @@ yarn.lock
17 17
 # Editors
18 18
 .vscode/
19 19
 
20
+.DS_Store

+ 52
- 0
src/element/bounds.ts View File

@@ -0,0 +1,52 @@
1
+import { ExcalidrawElement } from "./types";
2
+import { rotate } from "../math";
3
+
4
+// If the element is created from right to left, the width is going to be negative
5
+// This set of functions retrieves the absolute position of the 4 points.
6
+// We can't just always normalize it since we need to remember the fact that an arrow
7
+// is pointing left or right.
8
+export function getElementAbsoluteX1(element: ExcalidrawElement) {
9
+  return element.width >= 0 ? element.x : element.x + element.width;
10
+}
11
+export function getElementAbsoluteX2(element: ExcalidrawElement) {
12
+  return element.width >= 0 ? element.x + element.width : element.x;
13
+}
14
+export function getElementAbsoluteY1(element: ExcalidrawElement) {
15
+  return element.height >= 0 ? element.y : element.y + element.height;
16
+}
17
+export function getElementAbsoluteY2(element: ExcalidrawElement) {
18
+  return element.height >= 0 ? element.y + element.height : element.y;
19
+}
20
+
21
+export function getDiamondPoints(element: ExcalidrawElement) {
22
+  const topX = Math.floor(element.width / 2) + 1;
23
+  const topY = 0;
24
+  const rightX = element.width;
25
+  const rightY = Math.floor(element.height / 2) + 1;
26
+  const bottomX = topX;
27
+  const bottomY = element.height;
28
+  const leftX = topY;
29
+  const leftY = rightY;
30
+
31
+  return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
32
+}
33
+
34
+export function getArrowPoints(element: ExcalidrawElement) {
35
+  const x1 = 0;
36
+  const y1 = 0;
37
+  const x2 = element.width;
38
+  const y2 = element.height;
39
+
40
+  const size = 30; // pixels
41
+  const distance = Math.hypot(x2 - x1, y2 - y1);
42
+  // Scale down the arrow until we hit a certain size so that it doesn't look weird
43
+  const minSize = Math.min(size, distance / 2);
44
+  const xs = x2 - ((x2 - x1) / distance) * minSize;
45
+  const ys = y2 - ((y2 - y1) / distance) * minSize;
46
+
47
+  const angle = 20; // degrees
48
+  const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
49
+  const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
50
+
51
+  return [x1, y1, x2, y2, x3, y3, x4, y4];
52
+}

+ 124
- 0
src/element/collision.ts View File

@@ -0,0 +1,124 @@
1
+import { distanceBetweenPointAndSegment } from "../math";
2
+
3
+import { ExcalidrawElement } from "./types";
4
+import {
5
+  getElementAbsoluteX1,
6
+  getElementAbsoluteX2,
7
+  getElementAbsoluteY1,
8
+  getElementAbsoluteY2,
9
+  getArrowPoints,
10
+  getDiamondPoints
11
+} from "./bounds";
12
+
13
+export function hitTest(
14
+  element: ExcalidrawElement,
15
+  x: number,
16
+  y: number
17
+): boolean {
18
+  // For shapes that are composed of lines, we only enable point-selection when the distance
19
+  // of the click is less than x pixels of any of the lines that the shape is composed of
20
+  const lineThreshold = 10;
21
+
22
+  if (element.type === "ellipse") {
23
+    // https://stackoverflow.com/a/46007540/232122
24
+    const px = Math.abs(x - element.x - element.width / 2);
25
+    const py = Math.abs(y - element.y - element.height / 2);
26
+
27
+    let tx = 0.707;
28
+    let ty = 0.707;
29
+
30
+    const a = element.width / 2;
31
+    const b = element.height / 2;
32
+
33
+    [0, 1, 2, 3].forEach(x => {
34
+      const xx = a * tx;
35
+      const yy = b * ty;
36
+
37
+      const ex = ((a * a - b * b) * tx ** 3) / a;
38
+      const ey = ((b * b - a * a) * ty ** 3) / b;
39
+
40
+      const rx = xx - ex;
41
+      const ry = yy - ey;
42
+
43
+      const qx = px - ex;
44
+      const qy = py - ey;
45
+
46
+      const r = Math.hypot(ry, rx);
47
+      const q = Math.hypot(qy, qx);
48
+
49
+      tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
50
+      ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
51
+      const t = Math.hypot(ty, tx);
52
+      tx /= t;
53
+      ty /= t;
54
+    });
55
+
56
+    return Math.hypot(a * tx - px, b * ty - py) < lineThreshold;
57
+  } else if (element.type === "rectangle") {
58
+    const x1 = getElementAbsoluteX1(element);
59
+    const x2 = getElementAbsoluteX2(element);
60
+    const y1 = getElementAbsoluteY1(element);
61
+    const y2 = getElementAbsoluteY2(element);
62
+
63
+    // (x1, y1) --A-- (x2, y1)
64
+    //    |D             |B
65
+    // (x1, y2) --C-- (x2, y2)
66
+    return (
67
+      distanceBetweenPointAndSegment(x, y, x1, y1, x2, y1) < lineThreshold || // A
68
+      distanceBetweenPointAndSegment(x, y, x2, y1, x2, y2) < lineThreshold || // B
69
+      distanceBetweenPointAndSegment(x, y, x2, y2, x1, y2) < lineThreshold || // C
70
+      distanceBetweenPointAndSegment(x, y, x1, y2, x1, y1) < lineThreshold // D
71
+    );
72
+  } else if (element.type === "diamond") {
73
+    x -= element.x;
74
+    y -= element.y;
75
+
76
+    const [
77
+      topX,
78
+      topY,
79
+      rightX,
80
+      rightY,
81
+      bottomX,
82
+      bottomY,
83
+      leftX,
84
+      leftY
85
+    ] = getDiamondPoints(element);
86
+
87
+    return (
88
+      distanceBetweenPointAndSegment(x, y, topX, topY, rightX, rightY) <
89
+        lineThreshold ||
90
+      distanceBetweenPointAndSegment(x, y, rightX, rightY, bottomX, bottomY) <
91
+        lineThreshold ||
92
+      distanceBetweenPointAndSegment(x, y, bottomX, bottomY, leftX, leftY) <
93
+        lineThreshold ||
94
+      distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
95
+        lineThreshold
96
+    );
97
+  } else if (element.type === "arrow") {
98
+    let [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
99
+    // The computation is done at the origin, we need to add a translation
100
+    x -= element.x;
101
+    y -= element.y;
102
+
103
+    return (
104
+      //    \
105
+      distanceBetweenPointAndSegment(x, y, x3, y3, x2, y2) < lineThreshold ||
106
+      // -----
107
+      distanceBetweenPointAndSegment(x, y, x1, y1, x2, y2) < lineThreshold ||
108
+      //    /
109
+      distanceBetweenPointAndSegment(x, y, x4, y4, x2, y2) < lineThreshold
110
+    );
111
+  } else if (element.type === "text") {
112
+    const x1 = getElementAbsoluteX1(element);
113
+    const x2 = getElementAbsoluteX2(element);
114
+    const y1 = getElementAbsoluteY1(element);
115
+    const y2 = getElementAbsoluteY2(element);
116
+
117
+    return x >= x1 && x <= x2 && y >= y1 && y <= y2;
118
+  } else if (element.type === "selection") {
119
+    console.warn("This should not happen, we need to investigate why it does.");
120
+    return false;
121
+  } else {
122
+    throw new Error("Unimplemented type " + element.type);
123
+  }
124
+}

+ 145
- 0
src/element/generateDraw.ts View File

@@ -0,0 +1,145 @@
1
+import rough from "roughjs/bin/wrappers/rough";
2
+
3
+import { withCustomMathRandom } from "../random";
4
+
5
+import { ExcalidrawElement } from "./types";
6
+import { isTextElement } from "./typeChecks";
7
+import { getDiamondPoints, getArrowPoints } from "./bounds";
8
+
9
+// Casting second argument (DrawingSurface) to any,
10
+// because it is requred by TS definitions and not required at runtime
11
+const generator = rough.generator(null, null as any);
12
+
13
+export function generateDraw(element: ExcalidrawElement) {
14
+  if (element.type === "selection") {
15
+    element.draw = (rc, context, { scrollX, scrollY }) => {
16
+      const fillStyle = context.fillStyle;
17
+      context.fillStyle = "rgba(0, 0, 255, 0.10)";
18
+      context.fillRect(
19
+        element.x + scrollX,
20
+        element.y + scrollY,
21
+        element.width,
22
+        element.height
23
+      );
24
+      context.fillStyle = fillStyle;
25
+    };
26
+  } else if (element.type === "rectangle") {
27
+    const shape = withCustomMathRandom(element.seed, () => {
28
+      return generator.rectangle(0, 0, element.width, element.height, {
29
+        stroke: element.strokeColor,
30
+        fill: element.backgroundColor,
31
+        fillStyle: element.fillStyle,
32
+        strokeWidth: element.strokeWidth,
33
+        roughness: element.roughness
34
+      });
35
+    });
36
+    element.draw = (rc, context, { scrollX, scrollY }) => {
37
+      context.globalAlpha = element.opacity / 100;
38
+      context.translate(element.x + scrollX, element.y + scrollY);
39
+      rc.draw(shape);
40
+      context.translate(-element.x - scrollX, -element.y - scrollY);
41
+      context.globalAlpha = 1;
42
+    };
43
+  } else if (element.type === "diamond") {
44
+    const shape = withCustomMathRandom(element.seed, () => {
45
+      const [
46
+        topX,
47
+        topY,
48
+        rightX,
49
+        rightY,
50
+        bottomX,
51
+        bottomY,
52
+        leftX,
53
+        leftY
54
+      ] = getDiamondPoints(element);
55
+      return generator.polygon(
56
+        [
57
+          [topX, topY],
58
+          [rightX, rightY],
59
+          [bottomX, bottomY],
60
+          [leftX, leftY]
61
+        ],
62
+        {
63
+          stroke: element.strokeColor,
64
+          fill: element.backgroundColor,
65
+          fillStyle: element.fillStyle,
66
+          strokeWidth: element.strokeWidth,
67
+          roughness: element.roughness
68
+        }
69
+      );
70
+    });
71
+    element.draw = (rc, context, { scrollX, scrollY }) => {
72
+      context.globalAlpha = element.opacity / 100;
73
+      context.translate(element.x + scrollX, element.y + scrollY);
74
+      rc.draw(shape);
75
+      context.translate(-element.x - scrollX, -element.y - scrollY);
76
+      context.globalAlpha = 1;
77
+    };
78
+  } else if (element.type === "ellipse") {
79
+    const shape = withCustomMathRandom(element.seed, () =>
80
+      generator.ellipse(
81
+        element.width / 2,
82
+        element.height / 2,
83
+        element.width,
84
+        element.height,
85
+        {
86
+          stroke: element.strokeColor,
87
+          fill: element.backgroundColor,
88
+          fillStyle: element.fillStyle,
89
+          strokeWidth: element.strokeWidth,
90
+          roughness: element.roughness
91
+        }
92
+      )
93
+    );
94
+    element.draw = (rc, context, { scrollX, scrollY }) => {
95
+      context.globalAlpha = element.opacity / 100;
96
+      context.translate(element.x + scrollX, element.y + scrollY);
97
+      rc.draw(shape);
98
+      context.translate(-element.x - scrollX, -element.y - scrollY);
99
+      context.globalAlpha = 1;
100
+    };
101
+  } else if (element.type === "arrow") {
102
+    const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
103
+    const options = {
104
+      stroke: element.strokeColor,
105
+      strokeWidth: element.strokeWidth,
106
+      roughness: element.roughness
107
+    };
108
+
109
+    const shapes = withCustomMathRandom(element.seed, () => [
110
+      //    \
111
+      generator.line(x3, y3, x2, y2, options),
112
+      // -----
113
+      generator.line(x1, y1, x2, y2, options),
114
+      //    /
115
+      generator.line(x4, y4, x2, y2, options)
116
+    ]);
117
+
118
+    element.draw = (rc, context, { scrollX, scrollY }) => {
119
+      context.globalAlpha = element.opacity / 100;
120
+      context.translate(element.x + scrollX, element.y + scrollY);
121
+      shapes.forEach(shape => rc.draw(shape));
122
+      context.translate(-element.x - scrollX, -element.y - scrollY);
123
+      context.globalAlpha = 1;
124
+    };
125
+    return;
126
+  } else if (isTextElement(element)) {
127
+    element.draw = (rc, context, { scrollX, scrollY }) => {
128
+      context.globalAlpha = element.opacity / 100;
129
+      const font = context.font;
130
+      context.font = element.font;
131
+      const fillStyle = context.fillStyle;
132
+      context.fillStyle = element.strokeColor;
133
+      context.fillText(
134
+        element.text,
135
+        element.x + scrollX,
136
+        element.y + element.actualBoundingBoxAscent + scrollY
137
+      );
138
+      context.fillStyle = fillStyle;
139
+      context.font = font;
140
+      context.globalAlpha = 1;
141
+    };
142
+  } else {
143
+    throw new Error("Unimplemented type " + element.type);
144
+  }
145
+}

+ 85
- 0
src/element/handlerRectangles.ts View File

@@ -0,0 +1,85 @@
1
+import { SceneState } from "../scene/types";
2
+import { ExcalidrawElement } from "./types";
3
+
4
+export function handlerRectangles(
5
+  element: ExcalidrawElement,
6
+  sceneState: SceneState
7
+) {
8
+  const elementX1 = element.x;
9
+  const elementX2 = element.x + element.width;
10
+  const elementY1 = element.y;
11
+  const elementY2 = element.y + element.height;
12
+
13
+  const margin = 4;
14
+  const minimumSize = 40;
15
+  const handlers: { [handler: string]: number[] } = {};
16
+
17
+  const marginX = element.width < 0 ? 8 : -8;
18
+  const marginY = element.height < 0 ? 8 : -8;
19
+
20
+  if (Math.abs(elementX2 - elementX1) > minimumSize) {
21
+    handlers["n"] = [
22
+      elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
23
+      elementY1 - margin + sceneState.scrollY + marginY,
24
+      8,
25
+      8
26
+    ];
27
+
28
+    handlers["s"] = [
29
+      elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
30
+      elementY2 - margin + sceneState.scrollY - marginY,
31
+      8,
32
+      8
33
+    ];
34
+  }
35
+
36
+  if (Math.abs(elementY2 - elementY1) > minimumSize) {
37
+    handlers["w"] = [
38
+      elementX1 - margin + sceneState.scrollX + marginX,
39
+      elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
40
+      8,
41
+      8
42
+    ];
43
+
44
+    handlers["e"] = [
45
+      elementX2 - margin + sceneState.scrollX - marginX,
46
+      elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
47
+      8,
48
+      8
49
+    ];
50
+  }
51
+
52
+  handlers["nw"] = [
53
+    elementX1 - margin + sceneState.scrollX + marginX,
54
+    elementY1 - margin + sceneState.scrollY + marginY,
55
+    8,
56
+    8
57
+  ]; // nw
58
+  handlers["ne"] = [
59
+    elementX2 - margin + sceneState.scrollX - marginX,
60
+    elementY1 - margin + sceneState.scrollY + marginY,
61
+    8,
62
+    8
63
+  ]; // ne
64
+  handlers["sw"] = [
65
+    elementX1 - margin + sceneState.scrollX + marginX,
66
+    elementY2 - margin + sceneState.scrollY - marginY,
67
+    8,
68
+    8
69
+  ]; // sw
70
+  handlers["se"] = [
71
+    elementX2 - margin + sceneState.scrollX - marginX,
72
+    elementY2 - margin + sceneState.scrollY - marginY,
73
+    8,
74
+    8
75
+  ]; // se
76
+
77
+  if (element.type === "arrow") {
78
+    return {
79
+      nw: handlers.nw,
80
+      se: handlers.se
81
+    };
82
+  }
83
+
84
+  return handlers;
85
+}

+ 15
- 0
src/element/index.ts View File

@@ -0,0 +1,15 @@
1
+export { newElement } from "./newElement";
2
+export {
3
+  getElementAbsoluteX1,
4
+  getElementAbsoluteX2,
5
+  getElementAbsoluteY1,
6
+  getElementAbsoluteY2,
7
+  getDiamondPoints,
8
+  getArrowPoints
9
+} from "./bounds";
10
+
11
+export { handlerRectangles } from "./handlerRectangles";
12
+export { hitTest } from "./collision";
13
+export { resizeTest } from "./resizeTest";
14
+export { generateDraw } from "./generateDraw";
15
+export { isTextElement } from "./typeChecks";

+ 40
- 0
src/element/newElement.ts View File

@@ -0,0 +1,40 @@
1
+import { RoughCanvas } from "roughjs/bin/canvas";
2
+
3
+import { SceneState } from "../scene/types";
4
+import { randomSeed } from "../random";
5
+
6
+export function newElement(
7
+  type: string,
8
+  x: number,
9
+  y: number,
10
+  strokeColor: string,
11
+  backgroundColor: string,
12
+  fillStyle: string,
13
+  strokeWidth: number,
14
+  roughness: number,
15
+  opacity: number,
16
+  width = 0,
17
+  height = 0
18
+) {
19
+  const element = {
20
+    type: type,
21
+    x: x,
22
+    y: y,
23
+    width: width,
24
+    height: height,
25
+    isSelected: false,
26
+    strokeColor: strokeColor,
27
+    backgroundColor: backgroundColor,
28
+    fillStyle: fillStyle,
29
+    strokeWidth: strokeWidth,
30
+    roughness: roughness,
31
+    opacity: opacity,
32
+    seed: randomSeed(),
33
+    draw(
34
+      rc: RoughCanvas,
35
+      context: CanvasRenderingContext2D,
36
+      sceneState: SceneState
37
+    ) {}
38
+  };
39
+  return element;
40
+}

+ 32
- 0
src/element/resizeTest.ts View File

@@ -0,0 +1,32 @@
1
+import { ExcalidrawElement } from "./types";
2
+import { SceneState } from "../scene/types";
3
+
4
+import { handlerRectangles } from "./handlerRectangles";
5
+
6
+export function resizeTest(
7
+  element: ExcalidrawElement,
8
+  x: number,
9
+  y: number,
10
+  sceneState: SceneState
11
+): string | false {
12
+  if (element.type === "text") return false;
13
+
14
+  const handlers = handlerRectangles(element, sceneState);
15
+
16
+  const filter = Object.keys(handlers).filter(key => {
17
+    const handler = handlers[key];
18
+
19
+    return (
20
+      x + sceneState.scrollX >= handler[0] &&
21
+      x + sceneState.scrollX <= handler[0] + handler[2] &&
22
+      y + sceneState.scrollY >= handler[1] &&
23
+      y + sceneState.scrollY <= handler[1] + handler[3]
24
+    );
25
+  });
26
+
27
+  if (filter.length > 0) {
28
+    return filter[0];
29
+  }
30
+
31
+  return false;
32
+}

+ 7
- 0
src/element/typeChecks.ts View File

@@ -0,0 +1,7 @@
1
+import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
2
+
3
+export function isTextElement(
4
+  element: ExcalidrawElement
5
+): element is ExcalidrawTextElement {
6
+  return element.type === "text";
7
+}

+ 9
- 0
src/element/types.ts View File

@@ -0,0 +1,9 @@
1
+import { newElement } from "./newElement";
2
+
3
+export type ExcalidrawElement = ReturnType<typeof newElement>;
4
+export type ExcalidrawTextElement = ExcalidrawElement & {
5
+  type: "text";
6
+  font: string;
7
+  text: string;
8
+  actualBoundingBoxAscent: number;
9
+};

+ 15
- 474
src/index.tsx View File

@@ -5,22 +5,27 @@ import { RoughCanvas } from "roughjs/bin/canvas";
5 5
 import { TwitterPicker } from "react-color";
6 6
 
7 7
 import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
8
-import { LCG, randomSeed, withCustomMathRandom } from "./random";
9
-import { distanceBetweenPointAndSegment } from "./math";
8
+import { randomSeed } from "./random";
10 9
 import { roundRect } from "./roundRect";
10
+import {
11
+  newElement,
12
+  resizeTest,
13
+  generateDraw,
14
+  getElementAbsoluteX1,
15
+  getElementAbsoluteX2,
16
+  getElementAbsoluteY1,
17
+  getElementAbsoluteY2,
18
+  handlerRectangles,
19
+  hitTest,
20
+  isTextElement
21
+} from "./element";
22
+import { SceneState } from "./scene/types";
23
+import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
11 24
 
12 25
 import EditableText from "./components/EditableText";
13 26
 
14 27
 import "./styles.scss";
15 28
 
16
-type ExcalidrawElement = ReturnType<typeof newElement>;
17
-type ExcalidrawTextElement = ExcalidrawElement & {
18
-  type: "text";
19
-  font: string;
20
-  text: string;
21
-  actualBoundingBoxAscent: number;
22
-};
23
-
24 29
 const LOCAL_STORAGE_KEY = "excalidraw";
25 30
 const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
26 31
 
@@ -58,186 +63,6 @@ function restoreHistoryEntry(entry: string) {
58 63
   skipHistory = true;
59 64
 }
60 65
 
61
-function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
62
-  // For shapes that are composed of lines, we only enable point-selection when the distance
63
-  // of the click is less than x pixels of any of the lines that the shape is composed of
64
-  const lineThreshold = 10;
65
-
66
-  if (element.type === "ellipse") {
67
-    // https://stackoverflow.com/a/46007540/232122
68
-    const px = Math.abs(x - element.x - element.width / 2);
69
-    const py = Math.abs(y - element.y - element.height / 2);
70
-
71
-    let tx = 0.707;
72
-    let ty = 0.707;
73
-
74
-    const a = element.width / 2;
75
-    const b = element.height / 2;
76
-
77
-    [0, 1, 2, 3].forEach(x => {
78
-      const xx = a * tx;
79
-      const yy = b * ty;
80
-
81
-      const ex = ((a * a - b * b) * tx ** 3) / a;
82
-      const ey = ((b * b - a * a) * ty ** 3) / b;
83
-
84
-      const rx = xx - ex;
85
-      const ry = yy - ey;
86
-
87
-      const qx = px - ex;
88
-      const qy = py - ey;
89
-
90
-      const r = Math.hypot(ry, rx);
91
-      const q = Math.hypot(qy, qx);
92
-
93
-      tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
94
-      ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
95
-      const t = Math.hypot(ty, tx);
96
-      tx /= t;
97
-      ty /= t;
98
-    });
99
-
100
-    return Math.hypot(a * tx - px, b * ty - py) < lineThreshold;
101
-  } else if (element.type === "rectangle") {
102
-    const x1 = getElementAbsoluteX1(element);
103
-    const x2 = getElementAbsoluteX2(element);
104
-    const y1 = getElementAbsoluteY1(element);
105
-    const y2 = getElementAbsoluteY2(element);
106
-
107
-    // (x1, y1) --A-- (x2, y1)
108
-    //    |D             |B
109
-    // (x1, y2) --C-- (x2, y2)
110
-    return (
111
-      distanceBetweenPointAndSegment(x, y, x1, y1, x2, y1) < lineThreshold || // A
112
-      distanceBetweenPointAndSegment(x, y, x2, y1, x2, y2) < lineThreshold || // B
113
-      distanceBetweenPointAndSegment(x, y, x2, y2, x1, y2) < lineThreshold || // C
114
-      distanceBetweenPointAndSegment(x, y, x1, y2, x1, y1) < lineThreshold // D
115
-    );
116
-  } else if (element.type === "diamond") {
117
-    x -= element.x;
118
-    y -= element.y;
119
-
120
-    const [
121
-      topX,
122
-      topY,
123
-      rightX,
124
-      rightY,
125
-      bottomX,
126
-      bottomY,
127
-      leftX,
128
-      leftY
129
-    ] = getDiamondPoints(element);
130
-
131
-    return (
132
-      distanceBetweenPointAndSegment(x, y, topX, topY, rightX, rightY) <
133
-        lineThreshold ||
134
-      distanceBetweenPointAndSegment(x, y, rightX, rightY, bottomX, bottomY) <
135
-        lineThreshold ||
136
-      distanceBetweenPointAndSegment(x, y, bottomX, bottomY, leftX, leftY) <
137
-        lineThreshold ||
138
-      distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
139
-        lineThreshold
140
-    );
141
-  } else if (element.type === "arrow") {
142
-    let [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
143
-    // The computation is done at the origin, we need to add a translation
144
-    x -= element.x;
145
-    y -= element.y;
146
-
147
-    return (
148
-      //    \
149
-      distanceBetweenPointAndSegment(x, y, x3, y3, x2, y2) < lineThreshold ||
150
-      // -----
151
-      distanceBetweenPointAndSegment(x, y, x1, y1, x2, y2) < lineThreshold ||
152
-      //    /
153
-      distanceBetweenPointAndSegment(x, y, x4, y4, x2, y2) < lineThreshold
154
-    );
155
-  } else if (element.type === "text") {
156
-    const x1 = getElementAbsoluteX1(element);
157
-    const x2 = getElementAbsoluteX2(element);
158
-    const y1 = getElementAbsoluteY1(element);
159
-    const y2 = getElementAbsoluteY2(element);
160
-
161
-    return x >= x1 && x <= x2 && y >= y1 && y <= y2;
162
-  } else if (element.type === "selection") {
163
-    console.warn("This should not happen, we need to investigate why it does.");
164
-    return false;
165
-  } else {
166
-    throw new Error("Unimplemented type " + element.type);
167
-  }
168
-}
169
-
170
-function resizeTest(
171
-  element: ExcalidrawElement,
172
-  x: number,
173
-  y: number,
174
-  sceneState: SceneState
175
-): string | false {
176
-  if (element.type === "text") return false;
177
-
178
-  const handlers = handlerRectangles(element, sceneState);
179
-
180
-  const filter = Object.keys(handlers).filter(key => {
181
-    const handler = handlers[key];
182
-
183
-    return (
184
-      x + sceneState.scrollX >= handler[0] &&
185
-      x + sceneState.scrollX <= handler[0] + handler[2] &&
186
-      y + sceneState.scrollY >= handler[1] &&
187
-      y + sceneState.scrollY <= handler[1] + handler[3]
188
-    );
189
-  });
190
-
191
-  if (filter.length > 0) {
192
-    return filter[0];
193
-  }
194
-
195
-  return false;
196
-}
197
-
198
-function newElement(
199
-  type: string,
200
-  x: number,
201
-  y: number,
202
-  strokeColor: string,
203
-  backgroundColor: string,
204
-  fillStyle: string,
205
-  strokeWidth: number,
206
-  roughness: number,
207
-  opacity: number,
208
-  width = 0,
209
-  height = 0
210
-) {
211
-  const element = {
212
-    type: type,
213
-    x: x,
214
-    y: y,
215
-    width: width,
216
-    height: height,
217
-    isSelected: false,
218
-    strokeColor: strokeColor,
219
-    backgroundColor: backgroundColor,
220
-    fillStyle: fillStyle,
221
-    strokeWidth: strokeWidth,
222
-    roughness: roughness,
223
-    opacity: opacity,
224
-    seed: randomSeed(),
225
-    draw(
226
-      rc: RoughCanvas,
227
-      context: CanvasRenderingContext2D,
228
-      sceneState: SceneState
229
-    ) {}
230
-  };
231
-  return element;
232
-}
233
-
234
-type SceneState = {
235
-  scrollX: number;
236
-  scrollY: number;
237
-  // null indicates transparent bg
238
-  viewBackgroundColor: string | null;
239
-};
240
-
241 66
 const SCROLLBAR_WIDTH = 6;
242 67
 const SCROLLBAR_MIN_SIZE = 15;
243 68
 const SCROLLBAR_MARGIN = 4;
@@ -340,86 +165,6 @@ function isOverScrollBars(
340 165
   };
341 166
 }
342 167
 
343
-function handlerRectangles(element: ExcalidrawElement, sceneState: SceneState) {
344
-  const elementX1 = element.x;
345
-  const elementX2 = element.x + element.width;
346
-  const elementY1 = element.y;
347
-  const elementY2 = element.y + element.height;
348
-
349
-  const margin = 4;
350
-  const minimumSize = 40;
351
-  const handlers: { [handler: string]: number[] } = {};
352
-
353
-  const marginX = element.width < 0 ? 8 : -8;
354
-  const marginY = element.height < 0 ? 8 : -8;
355
-
356
-  if (Math.abs(elementX2 - elementX1) > minimumSize) {
357
-    handlers["n"] = [
358
-      elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
359
-      elementY1 - margin + sceneState.scrollY + marginY,
360
-      8,
361
-      8
362
-    ];
363
-
364
-    handlers["s"] = [
365
-      elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
366
-      elementY2 - margin + sceneState.scrollY - marginY,
367
-      8,
368
-      8
369
-    ];
370
-  }
371
-
372
-  if (Math.abs(elementY2 - elementY1) > minimumSize) {
373
-    handlers["w"] = [
374
-      elementX1 - margin + sceneState.scrollX + marginX,
375
-      elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
376
-      8,
377
-      8
378
-    ];
379
-
380
-    handlers["e"] = [
381
-      elementX2 - margin + sceneState.scrollX - marginX,
382
-      elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
383
-      8,
384
-      8
385
-    ];
386
-  }
387
-
388
-  handlers["nw"] = [
389
-    elementX1 - margin + sceneState.scrollX + marginX,
390
-    elementY1 - margin + sceneState.scrollY + marginY,
391
-    8,
392
-    8
393
-  ]; // nw
394
-  handlers["ne"] = [
395
-    elementX2 - margin + sceneState.scrollX - marginX,
396
-    elementY1 - margin + sceneState.scrollY + marginY,
397
-    8,
398
-    8
399
-  ]; // ne
400
-  handlers["sw"] = [
401
-    elementX1 - margin + sceneState.scrollX + marginX,
402
-    elementY2 - margin + sceneState.scrollY - marginY,
403
-    8,
404
-    8
405
-  ]; // sw
406
-  handlers["se"] = [
407
-    elementX2 - margin + sceneState.scrollX - marginX,
408
-    elementY2 - margin + sceneState.scrollY - marginY,
409
-    8,
410
-    8
411
-  ]; // se
412
-
413
-  if (element.type === "arrow") {
414
-    return {
415
-      nw: handlers.nw,
416
-      se: handlers.se
417
-    };
418
-  }
419
-
420
-  return handlers;
421
-}
422
-
423 168
 function renderScene(
424 169
   rc: RoughCanvas,
425 170
   canvas: HTMLCanvasElement,
@@ -624,16 +369,6 @@ function saveFile(name: string, data: string) {
624 369
   link.remove();
625 370
 }
626 371
 
627
-function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
628
-  // 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
629
-  // 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
630
-  // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
631
-  return [
632
-    (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
633
-    (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2
634
-  ];
635
-}
636
-
637 372
 function getDateTime() {
638 373
   const date = new Date();
639 374
   const year = date.getFullYear();
@@ -646,16 +381,6 @@ function getDateTime() {
646 381
   return `${year}${month}${day}${hr}${min}${secs}`;
647 382
 }
648 383
 
649
-// Casting second argument (DrawingSurface) to any,
650
-// because it is requred by TS definitions and not required at runtime
651
-const generator = rough.generator(null, null as any);
652
-
653
-function isTextElement(
654
-  element: ExcalidrawElement
655
-): element is ExcalidrawTextElement {
656
-  return element.type === "text";
657
-}
658
-
659 384
 function isInputLike(
660 385
   target: Element | EventTarget | null
661 386
 ): target is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement {
@@ -666,190 +391,6 @@ function isInputLike(
666 391
   );
667 392
 }
668 393
 
669
-function getArrowPoints(element: ExcalidrawElement) {
670
-  const x1 = 0;
671
-  const y1 = 0;
672
-  const x2 = element.width;
673
-  const y2 = element.height;
674
-
675
-  const size = 30; // pixels
676
-  const distance = Math.hypot(x2 - x1, y2 - y1);
677
-  // Scale down the arrow until we hit a certain size so that it doesn't look weird
678
-  const minSize = Math.min(size, distance / 2);
679
-  const xs = x2 - ((x2 - x1) / distance) * minSize;
680
-  const ys = y2 - ((y2 - y1) / distance) * minSize;
681
-
682
-  const angle = 20; // degrees
683
-  const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
684
-  const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
685
-
686
-  return [x1, y1, x2, y2, x3, y3, x4, y4];
687
-}
688
-
689
-function getDiamondPoints(element: ExcalidrawElement) {
690
-  const topX = Math.floor(element.width / 2) + 1;
691
-  const topY = 0;
692
-  const rightX = element.width;
693
-  const rightY = Math.floor(element.height / 2) + 1;
694
-  const bottomX = topX;
695
-  const bottomY = element.height;
696
-  const leftX = topY;
697
-  const leftY = rightY;
698
-
699
-  return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
700
-}
701
-
702
-function generateDraw(element: ExcalidrawElement) {
703
-  if (element.type === "selection") {
704
-    element.draw = (rc, context, { scrollX, scrollY }) => {
705
-      const fillStyle = context.fillStyle;
706
-      context.fillStyle = "rgba(0, 0, 255, 0.10)";
707
-      context.fillRect(
708
-        element.x + scrollX,
709
-        element.y + scrollY,
710
-        element.width,
711
-        element.height
712
-      );
713
-      context.fillStyle = fillStyle;
714
-    };
715
-  } else if (element.type === "rectangle") {
716
-    const shape = withCustomMathRandom(element.seed, () => {
717
-      return generator.rectangle(0, 0, element.width, element.height, {
718
-        stroke: element.strokeColor,
719
-        fill: element.backgroundColor,
720
-        fillStyle: element.fillStyle,
721
-        strokeWidth: element.strokeWidth,
722
-        roughness: element.roughness
723
-      });
724
-    });
725
-    element.draw = (rc, context, { scrollX, scrollY }) => {
726
-      context.globalAlpha = element.opacity / 100;
727
-      context.translate(element.x + scrollX, element.y + scrollY);
728
-      rc.draw(shape);
729
-      context.translate(-element.x - scrollX, -element.y - scrollY);
730
-      context.globalAlpha = 1;
731
-    };
732
-  } else if (element.type === "diamond") {
733
-    const shape = withCustomMathRandom(element.seed, () => {
734
-      const [
735
-        topX,
736
-        topY,
737
-        rightX,
738
-        rightY,
739
-        bottomX,
740
-        bottomY,
741
-        leftX,
742
-        leftY
743
-      ] = getDiamondPoints(element);
744
-      return generator.polygon(
745
-        [
746
-          [topX, topY],
747
-          [rightX, rightY],
748
-          [bottomX, bottomY],
749
-          [leftX, leftY]
750
-        ],
751
-        {
752
-          stroke: element.strokeColor,
753
-          fill: element.backgroundColor,
754
-          fillStyle: element.fillStyle,
755
-          strokeWidth: element.strokeWidth,
756
-          roughness: element.roughness
757
-        }
758
-      );
759
-    });
760
-    element.draw = (rc, context, { scrollX, scrollY }) => {
761
-      context.globalAlpha = element.opacity / 100;
762
-      context.translate(element.x + scrollX, element.y + scrollY);
763
-      rc.draw(shape);
764
-      context.translate(-element.x - scrollX, -element.y - scrollY);
765
-      context.globalAlpha = 1;
766
-    };
767
-  } else if (element.type === "ellipse") {
768
-    const shape = withCustomMathRandom(element.seed, () =>
769
-      generator.ellipse(
770
-        element.width / 2,
771
-        element.height / 2,
772
-        element.width,
773
-        element.height,
774
-        {
775
-          stroke: element.strokeColor,
776
-          fill: element.backgroundColor,
777
-          fillStyle: element.fillStyle,
778
-          strokeWidth: element.strokeWidth,
779
-          roughness: element.roughness
780
-        }
781
-      )
782
-    );
783
-    element.draw = (rc, context, { scrollX, scrollY }) => {
784
-      context.globalAlpha = element.opacity / 100;
785
-      context.translate(element.x + scrollX, element.y + scrollY);
786
-      rc.draw(shape);
787
-      context.translate(-element.x - scrollX, -element.y - scrollY);
788
-      context.globalAlpha = 1;
789
-    };
790
-  } else if (element.type === "arrow") {
791
-    const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
792
-    const options = {
793
-      stroke: element.strokeColor,
794
-      strokeWidth: element.strokeWidth,
795
-      roughness: element.roughness
796
-    };
797
-
798
-    const shapes = withCustomMathRandom(element.seed, () => [
799
-      //    \
800
-      generator.line(x3, y3, x2, y2, options),
801
-      // -----
802
-      generator.line(x1, y1, x2, y2, options),
803
-      //    /
804
-      generator.line(x4, y4, x2, y2, options)
805
-    ]);
806
-
807
-    element.draw = (rc, context, { scrollX, scrollY }) => {
808
-      context.globalAlpha = element.opacity / 100;
809
-      context.translate(element.x + scrollX, element.y + scrollY);
810
-      shapes.forEach(shape => rc.draw(shape));
811
-      context.translate(-element.x - scrollX, -element.y - scrollY);
812
-      context.globalAlpha = 1;
813
-    };
814
-    return;
815
-  } else if (isTextElement(element)) {
816
-    element.draw = (rc, context, { scrollX, scrollY }) => {
817
-      context.globalAlpha = element.opacity / 100;
818
-      const font = context.font;
819
-      context.font = element.font;
820
-      const fillStyle = context.fillStyle;
821
-      context.fillStyle = element.strokeColor;
822
-      context.fillText(
823
-        element.text,
824
-        element.x + scrollX,
825
-        element.y + element.actualBoundingBoxAscent + scrollY
826
-      );
827
-      context.fillStyle = fillStyle;
828
-      context.font = font;
829
-      context.globalAlpha = 1;
830
-    };
831
-  } else {
832
-    throw new Error("Unimplemented type " + element.type);
833
-  }
834
-}
835
-
836
-// If the element is created from right to left, the width is going to be negative
837
-// This set of functions retrieves the absolute position of the 4 points.
838
-// We can't just always normalize it since we need to remember the fact that an arrow
839
-// is pointing left or right.
840
-function getElementAbsoluteX1(element: ExcalidrawElement) {
841
-  return element.width >= 0 ? element.x : element.x + element.width;
842
-}
843
-function getElementAbsoluteX2(element: ExcalidrawElement) {
844
-  return element.width >= 0 ? element.x + element.width : element.x;
845
-}
846
-function getElementAbsoluteY1(element: ExcalidrawElement) {
847
-  return element.height >= 0 ? element.y : element.y + element.height;
848
-}
849
-function getElementAbsoluteY2(element: ExcalidrawElement) {
850
-  return element.height >= 0 ? element.y + element.height : element.y;
851
-}
852
-
853 394
 function setSelection(selection: ExcalidrawElement) {
854 395
   const selectionX1 = getElementAbsoluteX1(selection);
855 396
   const selectionX2 = getElementAbsoluteX2(selection);

+ 16
- 0
src/math.ts View File

@@ -36,3 +36,19 @@ export function distanceBetweenPointAndSegment(
36 36
   const dy = y - yy;
37 37
   return Math.hypot(dx, dy);
38 38
 }
39
+
40
+export function rotate(
41
+  x1: number,
42
+  y1: number,
43
+  x2: number,
44
+  y2: number,
45
+  angle: number
46
+) {
47
+  // 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
48
+  // 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
49
+  // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
50
+  return [
51
+    (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
52
+    (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2
53
+  ];
54
+}

+ 6
- 0
src/scene/types.ts View File

@@ -0,0 +1,6 @@
1
+export type SceneState = {
2
+  scrollX: number;
3
+  scrollY: number;
4
+  // null indicates transparent bg
5
+  viewBackgroundColor: string | null;
6
+};

Loading…
Cancel
Save