Bladeren bron

Improves outlines

main
Steve Ruiz 4 jaren geleden
bovenliggende
commit
addf4185f0

+ 13
- 13
components/canvas/shapes/circle.tsx Bestand weergeven

1
-import { useSelector } from "state"
2
 import { CircleShape, ShapeProps } from "types"
1
 import { CircleShape, ShapeProps } from "types"
2
+import { Indicator, HoverIndicator } from "./indicator"
3
 import ShapeGroup from "./shape-g"
3
 import ShapeGroup from "./shape-g"
4
 
4
 
5
 function BaseCircle({
5
 function BaseCircle({
9
   strokeWidth = 0,
9
   strokeWidth = 0,
10
 }: ShapeProps<CircleShape>) {
10
 }: ShapeProps<CircleShape>) {
11
   return (
11
   return (
12
-    <circle
13
-      cx={radius}
14
-      cy={radius}
15
-      r={radius}
16
-      fill={fill}
17
-      stroke={stroke}
18
-      strokeWidth={strokeWidth}
19
-    />
12
+    <>
13
+      <HoverIndicator as="circle" cx={radius} cy={radius} r={radius - 1} />
14
+      <circle
15
+        cx={radius}
16
+        cy={radius}
17
+        r={radius - strokeWidth / 2}
18
+        fill={fill}
19
+        stroke={stroke}
20
+        strokeWidth={strokeWidth}
21
+      />
22
+      <Indicator as="circle" cx={radius} cy={radius} r={radius - 1} />
23
+    </>
20
   )
24
   )
21
 }
25
 }
22
 
26
 
23
 export default function Circle({ id, point, radius }: CircleShape) {
27
 export default function Circle({ id, point, radius }: CircleShape) {
24
-  const isSelected = useSelector((state) => state.values.selectedIds.has(id))
25
   return (
28
   return (
26
     <ShapeGroup id={id} point={point}>
29
     <ShapeGroup id={id} point={point}>
27
       <BaseCircle radius={radius} />
30
       <BaseCircle radius={radius} />
28
-      {isSelected && (
29
-        <BaseCircle radius={radius} fill="none" stroke="blue" strokeWidth={1} />
30
-      )}
31
     </ShapeGroup>
31
     </ShapeGroup>
32
   )
32
   )
33
 }
33
 }

+ 13
- 14
components/canvas/shapes/dot.tsx Bestand weergeven

1
-import { useSelector } from "state"
1
+import { Indicator, HoverIndicator } from "./indicator"
2
 import { DotShape, ShapeProps } from "types"
2
 import { DotShape, ShapeProps } from "types"
3
 import ShapeGroup from "./shape-g"
3
 import ShapeGroup from "./shape-g"
4
 
4
 
5
+const dotRadius = 4
6
+
5
 function BaseDot({
7
 function BaseDot({
6
   fill = "#999",
8
   fill = "#999",
7
   stroke = "none",
9
   stroke = "none",
8
-  strokeWidth = 0,
10
+  strokeWidth = 1,
9
 }: ShapeProps<DotShape>) {
11
 }: ShapeProps<DotShape>) {
10
   return (
12
   return (
11
     <>
13
     <>
12
-      <circle
13
-        cx={strokeWidth}
14
-        cy={strokeWidth}
15
-        r={8}
16
-        fill="transparent"
17
-        stroke="none"
18
-        strokeWidth="0"
14
+      <HoverIndicator
15
+        as="circle"
16
+        cx={dotRadius}
17
+        cy={dotRadius}
18
+        r={dotRadius - 1}
19
       />
19
       />
20
       <circle
20
       <circle
21
-        cx={strokeWidth}
22
-        cy={strokeWidth}
23
-        r={Math.max(1, 4 - strokeWidth)}
21
+        cx={dotRadius}
22
+        cy={dotRadius}
23
+        r={dotRadius - strokeWidth / 2}
24
         fill={fill}
24
         fill={fill}
25
         stroke={stroke}
25
         stroke={stroke}
26
         strokeWidth={strokeWidth}
26
         strokeWidth={strokeWidth}
27
       />
27
       />
28
+      <Indicator as="circle" cx={dotRadius} cy={dotRadius} r={dotRadius - 1} />
28
     </>
29
     </>
29
   )
30
   )
30
 }
31
 }
31
 
32
 
32
 export default function Dot({ id, point }: DotShape) {
33
 export default function Dot({ id, point }: DotShape) {
33
-  const isSelected = useSelector((state) => state.values.selectedIds.has(id))
34
   return (
34
   return (
35
     <ShapeGroup id={id} point={point}>
35
     <ShapeGroup id={id} point={point}>
36
       <BaseDot />
36
       <BaseDot />
37
-      {isSelected && <BaseDot fill="none" stroke="blue" strokeWidth={1} />}
38
     </ShapeGroup>
37
     </ShapeGroup>
39
   )
38
   )
40
 }
39
 }

+ 21
- 0
components/canvas/shapes/indicator.tsx Bestand weergeven

1
+import styled from "styles"
2
+
3
+const Indicator = styled("path", {
4
+  fill: "none",
5
+  stroke: "transparent",
6
+  strokeWidth: "2",
7
+  pointerEvents: "none",
8
+  strokeLineCap: "round",
9
+  strokeLinejoin: "round",
10
+})
11
+
12
+const HoverIndicator = styled("path", {
13
+  fill: "none",
14
+  stroke: "transparent",
15
+  strokeWidth: "8",
16
+  pointerEvents: "all",
17
+  strokeLinecap: "round",
18
+  strokeLinejoin: "round",
19
+})
20
+
21
+export { Indicator, HoverIndicator }

+ 5
- 9
components/canvas/shapes/polyline.tsx Bestand weergeven

1
-import { useSelector } from "state"
2
 import { PolylineShape, ShapeProps } from "types"
1
 import { PolylineShape, ShapeProps } from "types"
2
+import { Indicator, HoverIndicator } from "./indicator"
3
 import ShapeGroup from "./shape-g"
3
 import ShapeGroup from "./shape-g"
4
 
4
 
5
 function BasePolyline({
5
 function BasePolyline({
10
 }: ShapeProps<PolylineShape>) {
10
 }: ShapeProps<PolylineShape>) {
11
   return (
11
   return (
12
     <>
12
     <>
13
-      <polyline
14
-        points={points.toString()}
15
-        fill="none"
16
-        stroke="transparent"
17
-        strokeWidth={12}
18
-      />
13
+      <HoverIndicator as="polyline" points={points.toString()} />
19
       <polyline
14
       <polyline
20
         points={points.toString()}
15
         points={points.toString()}
21
         fill={fill}
16
         fill={fill}
22
         stroke={stroke}
17
         stroke={stroke}
23
         strokeWidth={strokeWidth}
18
         strokeWidth={strokeWidth}
19
+        strokeLinecap="round"
20
+        strokeLinejoin="round"
24
       />
21
       />
22
+      <Indicator as="polyline" points={points.toString()} />
25
     </>
23
     </>
26
   )
24
   )
27
 }
25
 }
28
 
26
 
29
 export default function Polyline({ id, point, points }: PolylineShape) {
27
 export default function Polyline({ id, point, points }: PolylineShape) {
30
-  const isSelected = useSelector((state) => state.values.selectedIds.has(id))
31
   return (
28
   return (
32
     <ShapeGroup id={id} point={point}>
29
     <ShapeGroup id={id} point={point}>
33
       <BasePolyline points={points} />
30
       <BasePolyline points={points} />
34
-      {isSelected && <BasePolyline points={points} fill="none" stroke="blue" />}
35
     </ShapeGroup>
31
     </ShapeGroup>
36
   )
32
   )
37
 }
33
 }

+ 26
- 14
components/canvas/shapes/rectangle.tsx Bestand weergeven

1
-import { useSelector } from "state"
2
 import { RectangleShape, ShapeProps } from "types"
1
 import { RectangleShape, ShapeProps } from "types"
2
+import { HoverIndicator, Indicator } from "./indicator"
3
 import ShapeGroup from "./shape-g"
3
 import ShapeGroup from "./shape-g"
4
 
4
 
5
 function BaseRectangle({
5
 function BaseRectangle({
9
   strokeWidth = 0,
9
   strokeWidth = 0,
10
 }: ShapeProps<RectangleShape>) {
10
 }: ShapeProps<RectangleShape>) {
11
   return (
11
   return (
12
-    <rect
13
-      x={strokeWidth}
14
-      y={strokeWidth}
15
-      width={size[0] - strokeWidth * 2}
16
-      height={size[1] - strokeWidth * 2}
17
-      fill={fill}
18
-      stroke={stroke}
19
-      strokeWidth={strokeWidth}
20
-    />
12
+    <>
13
+      <HoverIndicator
14
+        as="rect"
15
+        x={1}
16
+        y={1}
17
+        width={size[0] - 2}
18
+        height={size[1] - 2}
19
+      />
20
+      <rect
21
+        x={strokeWidth / 2}
22
+        y={strokeWidth / 2}
23
+        width={size[0] - strokeWidth}
24
+        height={size[1] - strokeWidth}
25
+        fill={fill}
26
+        stroke={stroke}
27
+        strokeWidth={strokeWidth}
28
+      />
29
+      <Indicator
30
+        as="rect"
31
+        x={1}
32
+        y={1}
33
+        width={size[0] - 2}
34
+        height={size[1] - 2}
35
+      />
36
+    </>
21
   )
37
   )
22
 }
38
 }
23
 
39
 
24
 export default function Rectangle({ id, point, size }: RectangleShape) {
40
 export default function Rectangle({ id, point, size }: RectangleShape) {
25
-  const isSelected = useSelector((state) => state.values.selectedIds.has(id))
26
   return (
41
   return (
27
     <ShapeGroup id={id} point={point}>
42
     <ShapeGroup id={id} point={point}>
28
       <BaseRectangle size={size} />
43
       <BaseRectangle size={size} />
29
-      {isSelected && (
30
-        <BaseRectangle size={size} fill="none" stroke="blue" strokeWidth={1} />
31
-      )}
32
     </ShapeGroup>
44
     </ShapeGroup>
33
   )
45
   )
34
 }
46
 }

+ 32
- 3
components/canvas/shapes/shape-g.tsx Bestand weergeven

1
-import state from "state"
1
+import state, { useSelector } from "state"
2
 import React, { useCallback, useRef } from "react"
2
 import React, { useCallback, useRef } from "react"
3
 import { getPointerEventInfo } from "utils/utils"
3
 import { getPointerEventInfo } from "utils/utils"
4
+import { Indicator, HoverIndicator } from "./indicator"
5
+import styled from "styles"
4
 
6
 
5
 export default function ShapeGroup({
7
 export default function ShapeGroup({
6
   id,
8
   id,
12
   point: number[]
14
   point: number[]
13
 }) {
15
 }) {
14
   const rGroup = useRef<SVGGElement>(null)
16
   const rGroup = useRef<SVGGElement>(null)
17
+  const isSelected = useSelector((state) => state.values.selectedIds.has(id))
15
 
18
 
16
   const handlePointerDown = useCallback(
19
   const handlePointerDown = useCallback(
17
     (e: React.PointerEvent) => {
20
     (e: React.PointerEvent) => {
44
   )
47
   )
45
 
48
 
46
   return (
49
   return (
47
-    <g
50
+    <StyledGroup
48
       ref={rGroup}
51
       ref={rGroup}
52
+      isSelected={isSelected}
49
       transform={`translate(${point})`}
53
       transform={`translate(${point})`}
50
       onPointerDown={handlePointerDown}
54
       onPointerDown={handlePointerDown}
51
       onPointerUp={handlePointerUp}
55
       onPointerUp={handlePointerUp}
53
       onPointerLeave={handlePointerLeave}
57
       onPointerLeave={handlePointerLeave}
54
     >
58
     >
55
       {children}
59
       {children}
56
-    </g>
60
+    </StyledGroup>
57
   )
61
   )
58
 }
62
 }
63
+
64
+const StyledGroup = styled("g", {
65
+  [`& ${HoverIndicator}`]: {
66
+    opacity: "0",
67
+  },
68
+  variants: {
69
+    isSelected: {
70
+      true: {
71
+        [`& ${Indicator}`]: {
72
+          stroke: "$selected",
73
+        },
74
+        [`&:hover ${HoverIndicator}`]: {
75
+          opacity: "1",
76
+          stroke: "$hint",
77
+        },
78
+      },
79
+      false: {
80
+        [`&:hover ${HoverIndicator}`]: {
81
+          opacity: "1",
82
+          stroke: "$hint",
83
+        },
84
+      },
85
+    },
86
+  },
87
+})

+ 3
- 0
components/editor.tsx Bestand weergeven

1
+import useKeyboardEvents from "hooks/useKeyboardEvents"
1
 import Canvas from "./canvas/canvas"
2
 import Canvas from "./canvas/canvas"
2
 import StatusBar from "./status-bar"
3
 import StatusBar from "./status-bar"
3
 
4
 
4
 export default function Editor() {
5
 export default function Editor() {
6
+  useKeyboardEvents()
7
+
5
   return (
8
   return (
6
     <>
9
     <>
7
       <Canvas />
10
       <Canvas />

+ 30
- 0
hooks/useKeyboardEvents.ts Bestand weergeven

1
+import { useEffect } from "react"
2
+import state from "state"
3
+import { getKeyboardEventInfo } from "utils/utils"
4
+
5
+export default function useKeyboardEvents() {
6
+  useEffect(() => {
7
+    function handleKeyDown(e: KeyboardEvent) {
8
+      if (e.key === "Escape") {
9
+        state.send("CANCELLED")
10
+      }
11
+
12
+      state.send("PRESSED_KEY", getKeyboardEventInfo(e))
13
+    }
14
+
15
+    function handleKeyUp(e: KeyboardEvent) {
16
+      if (e.key === "Escape") {
17
+        state.send("CANCELLED")
18
+      }
19
+
20
+      state.send("RELEASED_KEY", getKeyboardEventInfo(e))
21
+    }
22
+
23
+    document.body.addEventListener("keydown", handleKeyDown)
24
+    document.body.addEventListener("keyup", handleKeyUp)
25
+    return () => {
26
+      document.body.removeEventListener("keydown", handleKeyDown)
27
+      document.body.removeEventListener("keyup", handleKeyUp)
28
+    }
29
+  }, [])
30
+}

+ 25
- 18
state/sessions/brush-session.ts Bestand weergeven

10
 } from "utils/intersections"
10
 } from "utils/intersections"
11
 
11
 
12
 interface BrushSnapshot {
12
 interface BrushSnapshot {
13
-  selectedIds: string[]
14
-  shapes: { shape: Shape; test: (bounds: Bounds) => boolean }[]
13
+  selectedIds: Set<string>
14
+  shapes: { id: string; test: (bounds: Bounds) => boolean }[]
15
 }
15
 }
16
 
16
 
17
 export default class BrushSession extends BaseSession {
17
 export default class BrushSession extends BaseSession {
31
 
31
 
32
     const brushBounds = getBoundsFromPoints(origin, point)
32
     const brushBounds = getBoundsFromPoints(origin, point)
33
 
33
 
34
-    data.selectedIds = [
35
-      ...snapshot.selectedIds,
36
-      ...snapshot.shapes
37
-        .filter(({ test }) => test(brushBounds))
38
-        .map(({ shape }) => shape.id),
39
-    ]
34
+    for (let { test, id } of snapshot.shapes) {
35
+      if (test(brushBounds)) {
36
+        data.selectedIds.add(id)
37
+      } else if (data.selectedIds.has(id)) {
38
+        data.selectedIds.delete(id)
39
+      }
40
+    }
40
 
41
 
41
     data.brush = brushBounds
42
     data.brush = brushBounds
42
   }
43
   }
43
 
44
 
44
   cancel = (data: Data) => {
45
   cancel = (data: Data) => {
45
     data.brush = undefined
46
     data.brush = undefined
46
-    data.selectedIds = this.snapshot.selectedIds
47
+    data.selectedIds = new Set(this.snapshot.selectedIds)
47
   }
48
   }
48
 
49
 
49
   complete = (data: Data) => {
50
   complete = (data: Data) => {
50
     data.brush = undefined
51
     data.brush = undefined
51
   }
52
   }
52
 
53
 
54
+  /**
55
+   * Get a snapshot of the current selected ids, for each shape that is
56
+   * not already selected, the shape's id and a test to see whether the
57
+   * brush will intersect that shape. For tests, start broad -> fine.
58
+   * @param data
59
+   * @returns
60
+   */
53
   static getSnapshot(data: Data): BrushSnapshot {
61
   static getSnapshot(data: Data): BrushSnapshot {
54
     const {
62
     const {
55
       selectedIds,
63
       selectedIds,
57
       currentPageId,
65
       currentPageId,
58
     } = current(data)
66
     } = current(data)
59
 
67
 
60
-    const currentlySelected = new Set(selectedIds)
61
-
62
     return {
68
     return {
63
-      selectedIds: [...data.selectedIds],
69
+      selectedIds: new Set(data.selectedIds),
64
       shapes: Object.values(pages[currentPageId].shapes)
70
       shapes: Object.values(pages[currentPageId].shapes)
65
-        .filter((shape) => !currentlySelected.has(shape.id))
71
+        .filter((shape) => !selectedIds.has(shape.id))
66
         .map((shape) => {
72
         .map((shape) => {
67
           switch (shape.type) {
73
           switch (shape.type) {
68
             case ShapeType.Dot: {
74
             case ShapeType.Dot: {
69
               const bounds = shapeUtils[shape.type].getBounds(shape)
75
               const bounds = shapeUtils[shape.type].getBounds(shape)
70
 
76
 
71
               return {
77
               return {
72
-                shape,
78
+                id: shape.id,
73
                 test: (brushBounds: Bounds) =>
79
                 test: (brushBounds: Bounds) =>
74
                   boundsContained(bounds, brushBounds) ||
80
                   boundsContained(bounds, brushBounds) ||
75
                   intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
81
                   intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
79
               const bounds = shapeUtils[shape.type].getBounds(shape)
85
               const bounds = shapeUtils[shape.type].getBounds(shape)
80
 
86
 
81
               return {
87
               return {
82
-                shape,
88
+                id: shape.id,
83
                 test: (brushBounds: Bounds) =>
89
                 test: (brushBounds: Bounds) =>
84
                   boundsContained(bounds, brushBounds) ||
90
                   boundsContained(bounds, brushBounds) ||
85
                   intersectCircleBounds(
91
                   intersectCircleBounds(
93
               const bounds = shapeUtils[shape.type].getBounds(shape)
99
               const bounds = shapeUtils[shape.type].getBounds(shape)
94
 
100
 
95
               return {
101
               return {
96
-                shape,
102
+                id: shape.id,
97
                 test: (brushBounds: Bounds) =>
103
                 test: (brushBounds: Bounds) =>
98
                   boundsContained(bounds, brushBounds) ||
104
                   boundsContained(bounds, brushBounds) ||
99
                   boundsCollide(bounds, brushBounds),
105
                   boundsCollide(bounds, brushBounds),
106
               )
112
               )
107
 
113
 
108
               return {
114
               return {
109
-                shape,
115
+                id: shape.id,
110
                 test: (brushBounds: Bounds) =>
116
                 test: (brushBounds: Bounds) =>
111
                   boundsContained(bounds, brushBounds) ||
117
                   boundsContained(bounds, brushBounds) ||
112
-                  intersectPolylineBounds(points, brushBounds).length > 0,
118
+                  (boundsCollide(bounds, brushBounds) &&
119
+                    intersectPolylineBounds(points, brushBounds).length > 0),
113
               }
120
               }
114
             }
121
             }
115
             default: {
122
             default: {

+ 5
- 5
state/state.ts Bestand weergeven

12
   },
12
   },
13
   brush: undefined,
13
   brush: undefined,
14
   pointedId: null,
14
   pointedId: null,
15
-  selectedIds: [],
15
+  selectedIds: new Set([]),
16
   currentPageId: "page0",
16
   currentPageId: "page0",
17
   document: defaultDocument,
17
   document: defaultDocument,
18
 }
18
 }
61
   },
61
   },
62
   conditions: {
62
   conditions: {
63
     isPointedShapeSelected(data) {
63
     isPointedShapeSelected(data) {
64
-      return data.selectedIds.includes(data.pointedId)
64
+      return data.selectedIds.has(data.pointedId)
65
     },
65
     },
66
     isPressingShiftKey(data, payload: { shiftKey: boolean }) {
66
     isPressingShiftKey(data, payload: { shiftKey: boolean }) {
67
       return payload.shiftKey
67
       return payload.shiftKey
93
       data.pointedId = undefined
93
       data.pointedId = undefined
94
     },
94
     },
95
     clearSelectedIds(data) {
95
     clearSelectedIds(data) {
96
-      data.selectedIds = []
96
+      data.selectedIds.clear()
97
     },
97
     },
98
     pullPointedIdFromSelectedIds(data) {
98
     pullPointedIdFromSelectedIds(data) {
99
       const { selectedIds, pointedId } = data
99
       const { selectedIds, pointedId } = data
100
-      selectedIds.splice(selectedIds.indexOf(pointedId, 1))
100
+      selectedIds.delete(pointedId)
101
     },
101
     },
102
     pushPointedIdToSelectedIds(data) {
102
     pushPointedIdToSelectedIds(data) {
103
-      data.selectedIds.push(data.pointedId)
103
+      data.selectedIds.add(data.pointedId)
104
     },
104
     },
105
     // Camera
105
     // Camera
106
     zoomCamera(data, payload: { delta: number; point: number[] }) {
106
     zoomCamera(data, payload: { delta: number; point: number[] }) {

+ 2
- 0
styles/stitches.config.ts Bestand weergeven

8
     colors: {
8
     colors: {
9
       brushFill: "rgba(0,0,0,.1)",
9
       brushFill: "rgba(0,0,0,.1)",
10
       brushStroke: "rgba(0,0,0,.5)",
10
       brushStroke: "rgba(0,0,0,.5)",
11
+      hint: "rgba(66, 133, 244, 0.200)",
12
+      selected: "rgba(66, 133, 244, 1.000)",
11
     },
13
     },
12
     space: {},
14
     space: {},
13
     fontSizes: {
15
     fontSizes: {

+ 4
- 2
types.ts Bestand weergeven

5
   }
5
   }
6
   brush?: Bounds
6
   brush?: Bounds
7
   currentPageId: string
7
   currentPageId: string
8
-  selectedIds: string[]
8
+  selectedIds: Set<string>
9
   pointedId?: string
9
   pointedId?: string
10
   document: {
10
   document: {
11
     pages: Record<string, Page>
11
     pages: Record<string, Page>
121
 >
121
 >
122
 
122
 
123
 export type ShapeProps<T extends Shape> = Partial<BaseShapeStyles> &
123
 export type ShapeProps<T extends Shape> = Partial<BaseShapeStyles> &
124
-  ShapeSpecificProps<T>
124
+  ShapeSpecificProps<T> & { id?: Shape["id"] }
125
+
126
+export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>

+ 5
- 0
utils/utils.ts Bestand weergeven

848
   const { shiftKey, ctrlKey, metaKey, altKey } = e
848
   const { shiftKey, ctrlKey, metaKey, altKey } = e
849
   return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
849
   return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
850
 }
850
 }
851
+
852
+export function getKeyboardEventInfo(e: React.KeyboardEvent | KeyboardEvent) {
853
+  const { shiftKey, ctrlKey, metaKey, altKey } = e
854
+  return { key: e.key, shiftKey, ctrlKey, metaKey, altKey }
855
+}

Laden…
Annuleren
Opslaan