Procházet zdrojové kódy

implements distribution

main
Steve Ruiz před 4 roky
rodič
revize
21927845a8

+ 5
- 1
components/editor.tsx Zobrazit soubor

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

+ 27
- 21
components/style-panel/align-distribute.tsx Zobrazit soubor

@@ -55,39 +55,45 @@ function distributeHorizontally() {
55 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 65
   return (
60 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 68
         <AlignLeftIcon />
78 69
       </IconButton>
79
-      <IconButton onClick={alignCenterHorizontal}>
70
+      <IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
80 71
         <AlignCenterHorizontallyIcon />
81 72
       </IconButton>
82
-      <IconButton onClick={alignRight}>
73
+      <IconButton disabled={!hasTwoOrMore} onClick={alignRight}>
83 74
         <AlignRightIcon />
84 75
       </IconButton>
85
-      <IconButton onClick={stretchHorizontally}>
76
+      <IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
86 77
         <StretchHorizontallyIcon />
87 78
       </IconButton>
88
-      <IconButton onClick={distributeHorizontally}>
79
+      <IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
89 80
         <SpaceEvenlyHorizontallyIcon />
90 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 97
     </Container>
92 98
   )
93 99
 }

+ 2
- 2
components/style-panel/color-picker.tsx Zobrazit soubor

@@ -1,15 +1,15 @@
1 1
 import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
2 2
 import { Square } from "react-feather"
3
-import { colors } from "state/data"
4 3
 import styled from "styles"
5 4
 
6 5
 interface Props {
7 6
   label: string
8 7
   color: string
8
+  colors: Record<string, string>
9 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 13
   return (
14 14
     <DropdownMenu.Root>
15 15
       <CurrentColor>

+ 18
- 5
components/style-panel/style-panel.tsx Zobrazit soubor

@@ -3,13 +3,16 @@ import state, { useSelector } from "state"
3 3
 import * as Panel from "components/panel"
4 4
 import { useRef } from "react"
5 5
 import { IconButton } from "components/shared"
6
-import { Circle, Square, Trash, X } from "react-feather"
6
+import { Circle, Trash, X } from "react-feather"
7 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 10
 import ColorPicker from "./color-picker"
11 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 17
 export default function StylePanel() {
15 18
   const rContainer = useRef<HTMLDivElement>(null)
@@ -65,6 +68,8 @@ function SelectedShapeStyles({}: {}) {
65 68
     return style
66 69
   }, deepCompare)
67 70
 
71
+  const hasSelection = selectedIds.length > 0
72
+
68 73
   return (
69 74
     <Panel.Layout>
70 75
       <Panel.Header>
@@ -73,7 +78,10 @@ function SelectedShapeStyles({}: {}) {
73 78
         </IconButton>
74 79
         <h3>Style</h3>
75 80
         <Panel.ButtonsGroup>
76
-          <IconButton onClick={() => state.send("DELETED")}>
81
+          <IconButton
82
+            disabled={!hasSelection}
83
+            onClick={() => state.send("DELETED")}
84
+          >
77 85
             <Trash />
78 86
           </IconButton>
79 87
         </Panel.ButtonsGroup>
@@ -82,14 +90,19 @@ function SelectedShapeStyles({}: {}) {
82 90
         <ColorPicker
83 91
           label="Fill"
84 92
           color={shapesStyle.fill}
93
+          colors={fillColors}
85 94
           onChange={(color) => state.send("CHANGED_STYLE", { fill: color })}
86 95
         />
87 96
         <ColorPicker
88 97
           label="Stroke"
89 98
           color={shapesStyle.stroke}
99
+          colors={strokeColors}
90 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 106
       </Content>
94 107
     </Panel.Layout>
95 108
   )

+ 8
- 3
components/toolbar.tsx Zobrazit soubor

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

+ 124
- 7
state/commands/distribute.ts Zobrazit soubor

@@ -1,16 +1,39 @@
1 1
 import Command from "./command"
2 2
 import history from "../history"
3 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 12
 import { getShapeUtils } from "lib/shape-utils"
6 13
 
7 14
 export default function distributeCommand(data: Data, type: DistributeType) {
8 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,19 +44,113 @@ export default function distributeCommand(data: Data, type: DistributeType) {
21 44
       category: "canvas",
22 45
       do(data) {
23 46
         const { shapes } = getPage(data, currentPageId)
47
+        const len = entries.length
24 48
 
25 49
         switch (type) {
26 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 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 145
       undo(data) {
33 146
         const { shapes } = getPage(data, currentPageId)
34
-        for (let id in initialPoints) {
147
+        for (let id in boundsForShapes) {
35 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 Zobrazit soubor

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

+ 3
- 3
state/state.ts Zobrazit soubor

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

Načítá se…
Zrušit
Uložit