瀏覽代碼

implements distribution

main
Steve Ruiz 4 年之前
父節點
當前提交
21927845a8

+ 5
- 1
components/editor.tsx 查看文件

13
   useKeyboardEvents()
13
   useKeyboardEvents()
14
   useLoadOnMount()
14
   useLoadOnMount()
15
 
15
 
16
+  const hasControls = useSelector(
17
+    (s) => Object.keys(s.data.codeControls).length > 0
18
+  )
19
+
16
   return (
20
   return (
17
     <Layout>
21
     <Layout>
18
       <Canvas />
22
       <Canvas />
20
       <Toolbar />
24
       <Toolbar />
21
       <LeftPanels>
25
       <LeftPanels>
22
         <CodePanel />
26
         <CodePanel />
23
-        <ControlsPanel />
27
+        {hasControls && <ControlsPanel />}
24
       </LeftPanels>
28
       </LeftPanels>
25
       <RightPanels>
29
       <RightPanels>
26
         <StylePanel />
30
         <StylePanel />

+ 27
- 21
components/style-panel/align-distribute.tsx 查看文件

55
   state.send("DISTRIBUTED", { type: DistributeType.Horizontal })
55
   state.send("DISTRIBUTED", { type: DistributeType.Horizontal })
56
 }
56
 }
57
 
57
 
58
-export default function AlignDistribute() {
58
+export default function AlignDistribute({
59
+  hasTwoOrMore,
60
+  hasThreeOrMore,
61
+}: {
62
+  hasTwoOrMore: boolean
63
+  hasThreeOrMore: boolean
64
+}) {
59
   return (
65
   return (
60
     <Container>
66
     <Container>
61
-      <IconButton onClick={alignTop}>
62
-        <AlignTopIcon />
63
-      </IconButton>
64
-      <IconButton onClick={alignCenterVertical}>
65
-        <AlignCenterVerticallyIcon />
66
-      </IconButton>
67
-      <IconButton onClick={alignBottom}>
68
-        <AlignBottomIcon />
69
-      </IconButton>
70
-      <IconButton onClick={stretchVertically}>
71
-        <StretchVerticallyIcon />
72
-      </IconButton>
73
-      <IconButton onClick={distributeVertically}>
74
-        <SpaceEvenlyVerticallyIcon />
75
-      </IconButton>
76
-      <IconButton onClick={alignLeft}>
67
+      <IconButton disabled={!hasTwoOrMore} onClick={alignLeft}>
77
         <AlignLeftIcon />
68
         <AlignLeftIcon />
78
       </IconButton>
69
       </IconButton>
79
-      <IconButton onClick={alignCenterHorizontal}>
70
+      <IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
80
         <AlignCenterHorizontallyIcon />
71
         <AlignCenterHorizontallyIcon />
81
       </IconButton>
72
       </IconButton>
82
-      <IconButton onClick={alignRight}>
73
+      <IconButton disabled={!hasTwoOrMore} onClick={alignRight}>
83
         <AlignRightIcon />
74
         <AlignRightIcon />
84
       </IconButton>
75
       </IconButton>
85
-      <IconButton onClick={stretchHorizontally}>
76
+      <IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
86
         <StretchHorizontallyIcon />
77
         <StretchHorizontallyIcon />
87
       </IconButton>
78
       </IconButton>
88
-      <IconButton onClick={distributeHorizontally}>
79
+      <IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
89
         <SpaceEvenlyHorizontallyIcon />
80
         <SpaceEvenlyHorizontallyIcon />
90
       </IconButton>
81
       </IconButton>
82
+      <IconButton disabled={!hasTwoOrMore} onClick={alignTop}>
83
+        <AlignTopIcon />
84
+      </IconButton>
85
+      <IconButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
86
+        <AlignCenterVerticallyIcon />
87
+      </IconButton>
88
+      <IconButton disabled={!hasTwoOrMore} onClick={alignBottom}>
89
+        <AlignBottomIcon />
90
+      </IconButton>
91
+      <IconButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
92
+        <StretchVerticallyIcon />
93
+      </IconButton>
94
+      <IconButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
95
+        <SpaceEvenlyVerticallyIcon />
96
+      </IconButton>
91
     </Container>
97
     </Container>
92
   )
98
   )
93
 }
99
 }

+ 2
- 2
components/style-panel/color-picker.tsx 查看文件

1
 import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
1
 import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
2
 import { Square } from "react-feather"
2
 import { Square } from "react-feather"
3
-import { colors } from "state/data"
4
 import styled from "styles"
3
 import styled from "styles"
5
 
4
 
6
 interface Props {
5
 interface Props {
7
   label: string
6
   label: string
8
   color: string
7
   color: string
8
+  colors: Record<string, string>
9
   onChange: (color: string) => void
9
   onChange: (color: string) => void
10
 }
10
 }
11
 
11
 
12
-export default function ColorPicker({ label, color, onChange }: Props) {
12
+export default function ColorPicker({ label, color, colors, onChange }: Props) {
13
   return (
13
   return (
14
     <DropdownMenu.Root>
14
     <DropdownMenu.Root>
15
       <CurrentColor>
15
       <CurrentColor>

+ 18
- 5
components/style-panel/style-panel.tsx 查看文件

3
 import * as Panel from "components/panel"
3
 import * as Panel from "components/panel"
4
 import { useRef } from "react"
4
 import { useRef } from "react"
5
 import { IconButton } from "components/shared"
5
 import { IconButton } from "components/shared"
6
-import { Circle, Square, Trash, X } from "react-feather"
6
+import { Circle, Trash, X } from "react-feather"
7
 import { deepCompare, deepCompareArrays, getSelectedShapes } from "utils/utils"
7
 import { deepCompare, deepCompareArrays, getSelectedShapes } from "utils/utils"
8
-import { colors } from "state/data"
8
+import { shades, fills, strokes } from "state/data"
9
 
9
 
10
 import ColorPicker from "./color-picker"
10
 import ColorPicker from "./color-picker"
11
 import AlignDistribute from "./align-distribute"
11
 import AlignDistribute from "./align-distribute"
12
-import { ShapeByType, ShapeStyles } from "types"
12
+import { ShapeStyles } from "types"
13
+
14
+const fillColors = { ...shades, ...fills }
15
+const strokeColors = { ...shades, ...strokes }
13
 
16
 
14
 export default function StylePanel() {
17
 export default function StylePanel() {
15
   const rContainer = useRef<HTMLDivElement>(null)
18
   const rContainer = useRef<HTMLDivElement>(null)
65
     return style
68
     return style
66
   }, deepCompare)
69
   }, deepCompare)
67
 
70
 
71
+  const hasSelection = selectedIds.length > 0
72
+
68
   return (
73
   return (
69
     <Panel.Layout>
74
     <Panel.Layout>
70
       <Panel.Header>
75
       <Panel.Header>
73
         </IconButton>
78
         </IconButton>
74
         <h3>Style</h3>
79
         <h3>Style</h3>
75
         <Panel.ButtonsGroup>
80
         <Panel.ButtonsGroup>
76
-          <IconButton onClick={() => state.send("DELETED")}>
81
+          <IconButton
82
+            disabled={!hasSelection}
83
+            onClick={() => state.send("DELETED")}
84
+          >
77
             <Trash />
85
             <Trash />
78
           </IconButton>
86
           </IconButton>
79
         </Panel.ButtonsGroup>
87
         </Panel.ButtonsGroup>
82
         <ColorPicker
90
         <ColorPicker
83
           label="Fill"
91
           label="Fill"
84
           color={shapesStyle.fill}
92
           color={shapesStyle.fill}
93
+          colors={fillColors}
85
           onChange={(color) => state.send("CHANGED_STYLE", { fill: color })}
94
           onChange={(color) => state.send("CHANGED_STYLE", { fill: color })}
86
         />
95
         />
87
         <ColorPicker
96
         <ColorPicker
88
           label="Stroke"
97
           label="Stroke"
89
           color={shapesStyle.stroke}
98
           color={shapesStyle.stroke}
99
+          colors={strokeColors}
90
           onChange={(color) => state.send("CHANGED_STYLE", { stroke: color })}
100
           onChange={(color) => state.send("CHANGED_STYLE", { stroke: color })}
91
         />
101
         />
92
-        <AlignDistribute />
102
+        <AlignDistribute
103
+          hasTwoOrMore={selectedIds.length > 1}
104
+          hasThreeOrMore={selectedIds.length > 2}
105
+        />
93
       </Content>
106
       </Content>
94
     </Panel.Layout>
107
     </Panel.Layout>
95
   )
108
   )

+ 8
- 3
components/toolbar.tsx 查看文件

72
         </Button>
72
         </Button>
73
         <Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
73
         <Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
74
       </Section>
74
       </Section>
75
+      <Section>
76
+        <Button onClick={() => state.send("UNDO")}>Undo</Button>
77
+        <Button onClick={() => state.send("REDO")}>Redo</Button>
78
+      </Section>
75
     </ToolbarContainer>
79
     </ToolbarContainer>
76
   )
80
   )
77
 }
81
 }
80
   gridArea: "toolbar",
84
   gridArea: "toolbar",
81
   userSelect: "none",
85
   userSelect: "none",
82
   borderBottom: "1px solid black",
86
   borderBottom: "1px solid black",
83
-  display: "grid",
84
-  gridTemplateColumns: "auto 1fr auto",
87
+  display: "flex",
85
   alignItems: "center",
88
   alignItems: "center",
86
-  backgroundColor: "white",
89
+  justifyContent: "space-between",
90
+  backgroundColor: "$panel",
87
   gap: 8,
91
   gap: 8,
88
   fontSize: "$1",
92
   fontSize: "$1",
89
   zIndex: 200,
93
   zIndex: 200,
102
   font: "$ui",
106
   font: "$ui",
103
   fontSize: "$ui",
107
   fontSize: "$ui",
104
   height: "40px",
108
   height: "40px",
109
+  outline: "none",
105
   borderRadius: 0,
110
   borderRadius: 0,
106
   border: "none",
111
   border: "none",
107
   padding: "0 12px",
112
   padding: "0 12px",

+ 124
- 7
state/commands/distribute.ts 查看文件

1
 import Command from "./command"
1
 import Command from "./command"
2
 import history from "../history"
2
 import history from "../history"
3
 import { AlignType, Data, DistributeType } from "types"
3
 import { AlignType, Data, DistributeType } from "types"
4
-import { getPage } from "utils/utils"
4
+import * as vec from "utils/vec"
5
+import {
6
+  getBoundsCenter,
7
+  getBoundsFromPoints,
8
+  getCommonBounds,
9
+  getPage,
10
+  getSelectedShapes,
11
+} from "utils/utils"
5
 import { getShapeUtils } from "lib/shape-utils"
12
 import { getShapeUtils } from "lib/shape-utils"
6
 
13
 
7
 export default function distributeCommand(data: Data, type: DistributeType) {
14
 export default function distributeCommand(data: Data, type: DistributeType) {
8
   const { currentPageId } = data
15
   const { currentPageId } = data
9
 
16
 
10
-  const initialPoints = Object.fromEntries(
11
-    Object.entries(getPage(data).shapes).map(([id, shape]) => [
12
-      id,
13
-      [...shape.point],
17
+  const selectedShapes = getSelectedShapes(data)
18
+
19
+  const entries = selectedShapes.map(
20
+    (shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
21
+  )
22
+  const boundsForShapes = Object.fromEntries(entries)
23
+
24
+  const commonBounds = getCommonBounds(...entries.map((entry) => entry[1]))
25
+
26
+  const innerBounds = getBoundsFromPoints(
27
+    entries.map((entry) => getBoundsCenter(entry[1]))
28
+  )
29
+
30
+  const midX = commonBounds.minX + commonBounds.width / 2
31
+  const midY = commonBounds.minY + commonBounds.height / 2
32
+
33
+  const centers = Object.fromEntries(
34
+    selectedShapes.map((shape) => [
35
+      shape.id,
36
+      getBoundsCenter(boundsForShapes[shape.id]),
14
     ])
37
     ])
15
   )
38
   )
16
 
39
 
21
       category: "canvas",
44
       category: "canvas",
22
       do(data) {
45
       do(data) {
23
         const { shapes } = getPage(data, currentPageId)
46
         const { shapes } = getPage(data, currentPageId)
47
+        const len = entries.length
24
 
48
 
25
         switch (type) {
49
         switch (type) {
26
           case DistributeType.Horizontal: {
50
           case DistributeType.Horizontal: {
51
+            const sortedByCenter = entries.sort(
52
+              ([a], [b]) => centers[a][0] - centers[b][0]
53
+            )
54
+
55
+            const span = sortedByCenter.reduce((a, c) => a + c[1].width, 0)
56
+
57
+            if (span > commonBounds.width) {
58
+              const left = sortedByCenter.sort(
59
+                (a, b) => a[1].minX - b[1].minX
60
+              )[0]
61
+
62
+              const right = sortedByCenter.sort(
63
+                (a, b) => b[1].maxX - a[1].maxX
64
+              )[0]
65
+
66
+              const entriesToMove = sortedByCenter
67
+                .filter((a) => a !== left && a !== right)
68
+                .sort((a, b) => centers[a[0]][0] - centers[b[0]][0])
69
+
70
+              const step =
71
+                (centers[right[0]][0] - centers[left[0]][0]) / (len - 1)
72
+
73
+              const x = centers[left[0]][0] + step
74
+
75
+              for (let i = 0; i < entriesToMove.length; i++) {
76
+                const [id, bounds] = entriesToMove[i]
77
+                const shape = shapes[id]
78
+                getShapeUtils(shape).translateTo(shape, [
79
+                  x + step * i - bounds.width / 2,
80
+                  bounds.minY,
81
+                ])
82
+              }
83
+            } else {
84
+              const step = (commonBounds.width - span) / (len - 1)
85
+              let x = commonBounds.minX
86
+
87
+              for (let i = 0; i < sortedByCenter.length - 1; i++) {
88
+                const [id, bounds] = sortedByCenter[i]
89
+                const shape = shapes[id]
90
+                getShapeUtils(shape).translateTo(shape, [x, bounds.minY])
91
+                x += bounds.width + step
92
+              }
93
+            }
94
+            break
27
           }
95
           }
28
           case DistributeType.Vertical: {
96
           case DistributeType.Vertical: {
97
+            const sortedByCenter = entries.sort(
98
+              ([a], [b]) => centers[a][1] - centers[b][1]
99
+            )
100
+
101
+            const span = sortedByCenter.reduce((a, c) => a + c[1].height, 0)
102
+
103
+            if (span > commonBounds.height) {
104
+              const top = sortedByCenter.sort(
105
+                (a, b) => a[1].minY - b[1].minY
106
+              )[0]
107
+
108
+              const bottom = sortedByCenter.sort(
109
+                (a, b) => b[1].maxY - a[1].maxY
110
+              )[0]
111
+
112
+              const entriesToMove = sortedByCenter
113
+                .filter((a) => a !== top && a !== bottom)
114
+                .sort((a, b) => centers[a[0]][1] - centers[b[0]][1])
115
+
116
+              const step =
117
+                (centers[bottom[0]][1] - centers[top[0]][1]) / (len - 1)
118
+
119
+              const y = centers[top[0]][1] + step
120
+
121
+              for (let i = 0; i < entriesToMove.length; i++) {
122
+                const [id, bounds] = entriesToMove[i]
123
+                const shape = shapes[id]
124
+                getShapeUtils(shape).translateTo(shape, [
125
+                  bounds.minX,
126
+                  y + step * i - bounds.height / 2,
127
+                ])
128
+              }
129
+            } else {
130
+              const step = (commonBounds.height - span) / (len - 1)
131
+              let y = commonBounds.minY
132
+
133
+              for (let i = 0; i < sortedByCenter.length - 1; i++) {
134
+                const [id, bounds] = sortedByCenter[i]
135
+                const shape = shapes[id]
136
+                getShapeUtils(shape).translateTo(shape, [bounds.minX, y])
137
+                y += bounds.height + step
138
+              }
139
+            }
140
+
141
+            break
29
           }
142
           }
30
         }
143
         }
31
       },
144
       },
32
       undo(data) {
145
       undo(data) {
33
         const { shapes } = getPage(data, currentPageId)
146
         const { shapes } = getPage(data, currentPageId)
34
-        for (let id in initialPoints) {
147
+        for (let id in boundsForShapes) {
35
           const shape = shapes[id]
148
           const shape = shapes[id]
36
-          getShapeUtils(shape).translateTo(shape, initialPoints[id])
149
+          const initialBounds = boundsForShapes[id]
150
+          getShapeUtils(shape).translateTo(shape, [
151
+            initialBounds.minX,
152
+            initialBounds.minY,
153
+          ])
37
         }
154
         }
38
       },
155
       },
39
     })
156
     })

+ 35
- 17
state/data.ts 查看文件

1
 import { Data, ShapeType } from "types"
1
 import { Data, ShapeType } from "types"
2
 import shapeUtils from "lib/shape-utils"
2
 import shapeUtils from "lib/shape-utils"
3
 
3
 
4
-export const colors = {
4
+export const shades = {
5
   transparent: "transparent",
5
   transparent: "transparent",
6
   white: "rgba(248, 249, 250, 1.000)",
6
   white: "rgba(248, 249, 250, 1.000)",
7
   lightGray: "rgba(224, 226, 230, 1.000)",
7
   lightGray: "rgba(224, 226, 230, 1.000)",
8
   gray: "rgba(172, 181, 189, 1.000)",
8
   gray: "rgba(172, 181, 189, 1.000)",
9
   darkGray: "rgba(52, 58, 64, 1.000)",
9
   darkGray: "rgba(52, 58, 64, 1.000)",
10
   black: "rgba(0,0,0, 1.000)",
10
   black: "rgba(0,0,0, 1.000)",
11
+}
12
+
13
+export const strokes = {
11
   lime: "rgba(115, 184, 23, 1.000)",
14
   lime: "rgba(115, 184, 23, 1.000)",
12
   green: "rgba(54, 178, 77, 1.000)",
15
   green: "rgba(54, 178, 77, 1.000)",
13
   teal: "rgba(9, 167, 120, 1.000)",
16
   teal: "rgba(9, 167, 120, 1.000)",
22
   yellow: "rgba(245, 159, 0, 1.000)",
25
   yellow: "rgba(245, 159, 0, 1.000)",
23
 }
26
 }
24
 
27
 
28
+export const fills = {
29
+  lime: "rgba(217, 245, 162, 1.000)",
30
+  green: "rgba(177, 242, 188, 1.000)",
31
+  teal: "rgba(149, 242, 215, 1.000)",
32
+  cyan: "rgba(153, 233, 242, 1.000)",
33
+  blue: "rgba(166, 216, 255, 1.000)",
34
+  indigo: "rgba(186, 200, 255, 1.000)",
35
+  violet: "rgba(208, 191, 255, 1.000)",
36
+  grape: "rgba(237, 190, 250, 1.000)",
37
+  pink: "rgba(252, 194, 215, 1.000)",
38
+  red: "rgba(255, 201, 201, 1.000)",
39
+  orange: "rgba(255, 216, 168, 1.000)",
40
+  yellow: "rgba(255, 236, 153, 1.000)",
41
+}
42
+
25
 export const defaultDocument: Data["document"] = {
43
 export const defaultDocument: Data["document"] = {
26
   pages: {
44
   pages: {
27
     page0: {
45
     page0: {
36
           childIndex: 3,
54
           childIndex: 3,
37
           point: [400, 500],
55
           point: [400, 500],
38
           style: {
56
           style: {
39
-            stroke: colors.black,
40
-            fill: colors.lightGray,
57
+            stroke: shades.black,
58
+            fill: shades.lightGray,
41
             strokeWidth: 1,
59
             strokeWidth: 1,
42
           },
60
           },
43
         }),
61
         }),
48
           point: [100, 600],
66
           point: [100, 600],
49
           radius: 50,
67
           radius: 50,
50
           style: {
68
           style: {
51
-            stroke: colors.black,
52
-            fill: colors.lightGray,
69
+            stroke: shades.black,
70
+            fill: shades.lightGray,
53
             strokeWidth: 1,
71
             strokeWidth: 1,
54
           },
72
           },
55
         }),
73
         }),
61
           radiusX: 50,
79
           radiusX: 50,
62
           radiusY: 100,
80
           radiusY: 100,
63
           style: {
81
           style: {
64
-            stroke: colors.black,
65
-            fill: colors.lightGray,
82
+            stroke: shades.black,
83
+            fill: shades.lightGray,
66
             strokeWidth: 1,
84
             strokeWidth: 1,
67
           },
85
           },
68
         }),
86
         }),
74
           radiusX: 50,
92
           radiusX: 50,
75
           radiusY: 30,
93
           radiusY: 30,
76
           style: {
94
           style: {
77
-            stroke: colors.black,
78
-            fill: colors.lightGray,
95
+            stroke: shades.black,
96
+            fill: shades.lightGray,
79
             strokeWidth: 1,
97
             strokeWidth: 1,
80
           },
98
           },
81
         }),
99
         }),
86
           point: [400, 400],
104
           point: [400, 400],
87
           direction: [0.2, 0.2],
105
           direction: [0.2, 0.2],
88
           style: {
106
           style: {
89
-            stroke: colors.black,
90
-            fill: colors.lightGray,
107
+            stroke: shades.black,
108
+            fill: shades.lightGray,
91
             strokeWidth: 1,
109
             strokeWidth: 1,
92
           },
110
           },
93
         }),
111
         }),
98
           point: [300, 100],
116
           point: [300, 100],
99
           direction: [0.5, 0.5],
117
           direction: [0.5, 0.5],
100
           style: {
118
           style: {
101
-            stroke: colors.black,
102
-            fill: colors.lightGray,
119
+            stroke: shades.black,
120
+            fill: shades.lightGray,
103
             strokeWidth: 1,
121
             strokeWidth: 1,
104
           },
122
           },
105
         }),
123
         }),
114
             [100, 50],
132
             [100, 50],
115
           ],
133
           ],
116
           style: {
134
           style: {
117
-            stroke: colors.black,
118
-            fill: colors.transparent,
135
+            stroke: shades.black,
136
+            fill: shades.transparent,
119
             strokeWidth: 1,
137
             strokeWidth: 1,
120
           },
138
           },
121
         }),
139
         }),
126
           point: [400, 600],
144
           point: [400, 600],
127
           size: [200, 200],
145
           size: [200, 200],
128
           style: {
146
           style: {
129
-            stroke: colors.black,
130
-            fill: colors.lightGray,
147
+            stroke: shades.black,
148
+            fill: shades.lightGray,
131
             strokeWidth: 1,
149
             strokeWidth: 1,
132
           },
150
           },
133
         }),
151
         }),

+ 3
- 3
state/state.ts 查看文件

1
 import { createSelectorHook, createState } from "@state-designer/react"
1
 import { createSelectorHook, createState } from "@state-designer/react"
2
 import * as vec from "utils/vec"
2
 import * as vec from "utils/vec"
3
 import inputs from "./inputs"
3
 import inputs from "./inputs"
4
-import { colors, defaultDocument } from "./data"
4
+import { shades, defaultDocument } from "./data"
5
 import { createShape, getShapeUtils } from "lib/shape-utils"
5
 import { createShape, getShapeUtils } from "lib/shape-utils"
6
 import history from "state/history"
6
 import history from "state/history"
7
 import * as Sessions from "./sessions"
7
 import * as Sessions from "./sessions"
42
     isStyleOpen: false,
42
     isStyleOpen: false,
43
   },
43
   },
44
   currentStyle: {
44
   currentStyle: {
45
-    fill: colors.lightGray,
46
-    stroke: colors.darkGray,
45
+    fill: shades.lightGray,
46
+    stroke: shades.darkGray,
47
   },
47
   },
48
   camera: {
48
   camera: {
49
     point: [0, 0],
49
     point: [0, 0],

Loading…
取消
儲存