Browse Source

Adds zoom controls

main
Steve Ruiz 4 years ago
parent
commit
09659ab9ba

+ 17
- 24
components/canvas/shape.tsx View File

@@ -43,39 +43,32 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
43 43
           strokeWidth={+shape.style.strokeWidth + 8}
44 44
         />
45 45
       )}
46
-      <StyledShape id={id} style={shape.style} />
47
-      {/* 
48
-      <text
49
-        y={4}
50
-        x={4}
51
-        fontSize={18}
52
-        fill="black"
53
-        stroke="none"
54
-        alignmentBaseline="text-before-edge"
55
-        pointerEvents="none"
56
-      >
57
-        {center.toString()}
58
-      </text> */}
46
+      {!shape.isHidden && <StyledShape id={id} style={shape.style} />}
59 47
     </StyledGroup>
60 48
   )
61 49
 }
62 50
 
63 51
 const StyledShape = memo(
64 52
   ({ id, style }: { id: string; style: ShapeStyles }) => {
65
-    return (
66
-      <MainShape
67
-        as="use"
68
-        href={'#' + id}
69
-        {...style}
70
-        // css={{ zStrokeWidth: Number(style.strokeWidth) }}
71
-      />
72
-    )
53
+    return <use href={'#' + id} {...style} />
73 54
   }
74 55
 )
75 56
 
76
-const MainShape = styled('use', {
77
-  // zStrokeWidth: 1,
78
-})
57
+function Label({ text }: { text: string }) {
58
+  return (
59
+    <text
60
+      y={4}
61
+      x={4}
62
+      fontSize={18}
63
+      fill="black"
64
+      stroke="none"
65
+      alignmentBaseline="text-before-edge"
66
+      pointerEvents="none"
67
+    >
68
+      {text}
69
+    </text>
70
+  )
71
+}
79 72
 
80 73
 const HoverIndicator = styled('path', {
81 74
   fill: 'none',

+ 1
- 4
components/style-panel/color-picker.tsx View File

@@ -29,10 +29,7 @@ export default function ColorPicker({ label, color, colors, onChange }: Props) {
29 29
 
30 30
 function ColorIcon({ color }: { color: string }) {
31 31
   return (
32
-    <Square
33
-      fill={color}
34
-      strokeDasharray={color === 'transparent' ? '2, 3' : 'none'}
35
-    />
32
+    <Square fill={color} strokeDasharray={color === 'none' ? '2, 3' : 'none'} />
36 33
   )
37 34
 }
38 35
 

+ 6
- 6
components/style-panel/style-panel.tsx View File

@@ -145,12 +145,6 @@ function SelectedShapeStyles({}: {}) {
145 145
           >
146 146
             <RotateCounterClockwiseIcon />
147 147
           </IconButton>
148
-          <IconButton
149
-            disabled={!hasSelection}
150
-            onClick={() => state.send('DELETED')}
151
-          >
152
-            <TrashIcon />
153
-          </IconButton>
154 148
           <IconButton
155 149
             disabled={!hasSelection}
156 150
             onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
@@ -169,6 +163,12 @@ function SelectedShapeStyles({}: {}) {
169 163
           >
170 164
             {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
171 165
           </IconButton>
166
+          <IconButton
167
+            disabled={!hasSelection}
168
+            onClick={() => state.send('DELETED')}
169
+          >
170
+            <TrashIcon />
171
+          </IconButton>
172 172
         </ButtonsRow>
173 173
       </Content>
174 174
     </Panel.Layout>

+ 5
- 0
components/tools-panel/tools-panel.tsx View File

@@ -16,6 +16,7 @@ import React from 'react'
16 16
 import state, { useSelector } from 'state'
17 17
 import styled from 'styles'
18 18
 import { ShapeType } from 'types'
19
+import Zoom from './zoom'
19 20
 
20 21
 const selectSelectTool = () => state.send('SELECTED_SELECT_TOOL')
21 22
 const selectDrawTool = () => state.send('SELECTED_DRAW_TOOL')
@@ -49,6 +50,7 @@ export default function ToolsPanel() {
49 50
 
50 51
   return (
51 52
     <OuterContainer>
53
+      <Zoom />
52 54
       <Container>
53 55
         <IconButton
54 56
           name="select"
@@ -131,7 +133,10 @@ export default function ToolsPanel() {
131 133
   )
132 134
 }
133 135
 
136
+const Spacer = styled('div', { flexGrow: 2 })
137
+
134 138
 const OuterContainer = styled('div', {
139
+  position: 'relative',
135 140
   gridArea: 'tools',
136 141
   padding: '0 8px 12px 8px',
137 142
   height: '100%',

+ 59
- 0
components/tools-panel/zoom.tsx View File

@@ -0,0 +1,59 @@
1
+import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
2
+import { IconButton } from 'components/shared'
3
+import state, { useSelector } from 'state'
4
+import styled from 'styles'
5
+
6
+const zoomIn = () => state.send('ZOOMED_IN')
7
+const zoomOut = () => state.send('ZOOMED_OUT')
8
+const zoomToFit = () => state.send('ZOOMED_TO_FIT')
9
+const zoomToActual = () => state.send('ZOOMED_TO_ACTUAL')
10
+
11
+export default function Zoom() {
12
+  return (
13
+    <Container>
14
+      <IconButton onClick={zoomOut}>
15
+        <ZoomOutIcon />
16
+      </IconButton>
17
+      <IconButton onClick={zoomIn}>
18
+        <ZoomInIcon />
19
+      </IconButton>
20
+      <ZoomCounter />
21
+    </Container>
22
+  )
23
+}
24
+
25
+function ZoomCounter() {
26
+  const camera = useSelector((s) => s.data.camera)
27
+  return (
28
+    <ZoomButton onClick={zoomToActual} onDoubleClick={zoomToFit}>
29
+      {Math.round(camera.zoom * 100)}%
30
+    </ZoomButton>
31
+  )
32
+}
33
+
34
+const Container = styled('div', {
35
+  position: 'absolute',
36
+  bottom: 12,
37
+  left: 12,
38
+  backgroundColor: '$panel',
39
+  borderRadius: '4px',
40
+  overflow: 'hidden',
41
+  alignSelf: 'flex-end',
42
+  border: '1px solid $border',
43
+  pointerEvents: 'all',
44
+  userSelect: 'none',
45
+  zIndex: 200,
46
+  boxShadow: '0px 2px 12px rgba(0,0,0,.08)',
47
+  display: 'flex',
48
+  padding: 4,
49
+
50
+  '& svg': {
51
+    strokeWidth: 0,
52
+  },
53
+})
54
+
55
+const ZoomButton = styled(IconButton, {
56
+  fontSize: '$0',
57
+  padding: 8,
58
+  width: 44,
59
+})

+ 12
- 0
hooks/useKeyboardEvents.ts View File

@@ -27,6 +27,18 @@ export default function useKeyboardEvents() {
27 27
           state.send('NUDGED', { delta: [-1, 0], ...getKeyboardEventInfo(e) })
28 28
           break
29 29
         }
30
+        case '=': {
31
+          if (e.metaKey) {
32
+            state.send('ZOOMED_IN')
33
+          }
34
+          break
35
+        }
36
+        case '-': {
37
+          if (e.metaKey) {
38
+            state.send('ZOOMED_OUT')
39
+          }
40
+          break
41
+        }
30 42
         case '!': {
31 43
           // Shift + 1
32 44
           if (e.shiftKey) {

+ 12
- 17
lib/shape-utils/draw.tsx View File

@@ -12,7 +12,7 @@ import {
12 12
   translateBounds,
13 13
 } from 'utils/utils'
14 14
 
15
-const pathCache = new WeakMap<DrawShape, string>([])
15
+const pathCache = new WeakMap<DrawShape['points'], string>([])
16 16
 
17 17
 const draw = registerShapeUtils<DrawShape>({
18 18
   boundsCache: new WeakMap([]),
@@ -45,25 +45,20 @@ const draw = registerShapeUtils<DrawShape>({
45 45
   render(shape) {
46 46
     const { id, points, style } = shape
47 47
 
48
-    if (!pathCache.has(shape)) {
49
-      if (points.length < 2) {
50
-        const left = [+style.strokeWidth, 0]
51
-        let d: number[][] = []
52
-        for (let i = 0; i < 10; i++) {
53
-          d.push(vec.rotWith(left, [0, 0], i * ((Math.PI * 2) / 8)))
54
-        }
55
-        pathCache.set(shape, getSvgPathFromStroke(d))
56
-      } else {
57
-        pathCache.set(
58
-          shape,
59
-          getSvgPathFromStroke(
60
-            getStroke(points, { size: +style.strokeWidth * 2, thinning: 0.9 })
61
-          )
48
+    if (!pathCache.has(points)) {
49
+      pathCache.set(
50
+        points,
51
+        getSvgPathFromStroke(
52
+          getStroke(points, { size: +style.strokeWidth * 2, thinning: 0.9 })
62 53
         )
63
-      }
54
+      )
55
+    }
56
+
57
+    if (points.length < 2) {
58
+      return <circle id={id} r={+style.strokeWidth * 0.618} />
64 59
     }
65 60
 
66
-    return <path id={id} d={pathCache.get(shape)} />
61
+    return <path id={id} d={pathCache.get(points)} />
67 62
   },
68 63
 
69 64
   applyStyles(shape, style) {

+ 2
- 0
lib/shape-utils/ellipse.tsx View File

@@ -124,6 +124,8 @@ const ellipse = registerShapeUtils<EllipseShape>({
124 124
   },
125 125
 
126 126
   transform(shape, bounds, { scaleX, scaleY, initialShape }) {
127
+    // TODO: Locked aspect ratio transform
128
+
127 129
     shape.point = [bounds.minX, bounds.minY]
128 130
     shape.radiusX = bounds.width / 2
129 131
     shape.radiusY = bounds.height / 2

+ 1
- 1
lib/shape-utils/rectangle.tsx View File

@@ -110,7 +110,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
110 110
   },
111 111
 
112 112
   transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
113
-    if (shape.rotation === 0) {
113
+    if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
114 114
       shape.size = [bounds.width, bounds.height]
115 115
       shape.point = [bounds.minX, bounds.minY]
116 116
     } else {

+ 20
- 12
state/commands/move.ts View File

@@ -1,8 +1,8 @@
1
-import Command from "./command"
2
-import history from "../history"
3
-import { Data, MoveType, Shape } from "types"
4
-import { forceIntegerChildIndices, getChildren, getPage } from "utils/utils"
5
-import { getShapeUtils } from "lib/shape-utils"
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, MoveType, Shape } from 'types'
4
+import { forceIntegerChildIndices, getChildren, getPage } from 'utils/utils'
5
+import { getShapeUtils } from 'lib/shape-utils'
6 6
 
7 7
 export default function moveCommand(data: Data, type: MoveType) {
8 8
   const { currentPageId } = data
@@ -18,8 +18,8 @@ export default function moveCommand(data: Data, type: MoveType) {
18 18
   history.execute(
19 19
     data,
20 20
     new Command({
21
-      name: "move_shapes",
22
-      category: "canvas",
21
+      name: 'move_shapes',
22
+      category: 'canvas',
23 23
       manualSelection: true,
24 24
       do(data) {
25 25
         const page = getPage(data, currentPageId)
@@ -77,7 +77,11 @@ export default function moveCommand(data: Data, type: MoveType) {
77 77
 
78 78
         for (let id of selectedIds) {
79 79
           const shape = page.shapes[id]
80
-          getShapeUtils(shape).setChildIndex(shape, initialIndices[id])
80
+          getShapeUtils(shape).setProperty(
81
+            shape,
82
+            'childIndex',
83
+            initialIndices[id]
84
+          )
81 85
         }
82 86
       },
83 87
     })
@@ -96,7 +100,7 @@ function moveToFront(shapes: Shape[], siblings: Shape[]) {
96 100
   const startIndex = Math.ceil(diff[0].childIndex) + 1
97 101
 
98 102
   shapes.forEach((shape, i) =>
99
-    getShapeUtils(shape).setChildIndex(shape, startIndex + i)
103
+    getShapeUtils(shape).setProperty(shape, 'childIndex', startIndex + i)
100 104
   )
101 105
 }
102 106
 
@@ -114,7 +118,11 @@ function moveToBack(shapes: Shape[], siblings: Shape[]) {
114 118
   const step = startIndex / (shapes.length + 1)
115 119
 
116 120
   shapes.forEach((shape, i) =>
117
-    getShapeUtils(shape).setChildIndex(shape, startIndex - (i + 1) * step)
121
+    getShapeUtils(shape).setProperty(
122
+      shape,
123
+      'childIndex',
124
+      startIndex - (i + 1) * step
125
+    )
118 126
   )
119 127
 }
120 128
 
@@ -138,7 +146,7 @@ function moveForward(shape: Shape, siblings: Shape[], visited: Set<string>) {
138 146
         : Math.ceil(nextSibling.childIndex + 1)
139 147
     }
140 148
 
141
-    getShapeUtils(shape).setChildIndex(shape, nextIndex)
149
+    getShapeUtils(shape).setProperty(shape, 'childIndex', nextIndex)
142 150
 
143 151
     siblings.sort((a, b) => a.childIndex - b.childIndex)
144 152
   }
@@ -164,7 +172,7 @@ function moveBackward(shape: Shape, siblings: Shape[], visited: Set<string>) {
164 172
         : nextSibling.childIndex / 2
165 173
     }
166 174
 
167
-    getShapeUtils(shape).setChildIndex(shape, nextIndex)
175
+    getShapeUtils(shape).setProperty(shape, 'childIndex', nextIndex)
168 176
 
169 177
     siblings.sort((a, b) => a.childIndex - b.childIndex)
170 178
   }

+ 7
- 2
state/sessions/transform-session.ts View File

@@ -32,7 +32,7 @@ export default class TransformSession extends BaseSession {
32 32
   update(data: Data, point: number[], isAspectRatioLocked = false) {
33 33
     const { transformType } = this
34 34
 
35
-    const { shapeBounds, initialBounds } = this.snapshot
35
+    const { shapeBounds, initialBounds, isAllAspectRatioLocked } = this.snapshot
36 36
 
37 37
     const { shapes } = getPage(data)
38 38
 
@@ -41,7 +41,7 @@ export default class TransformSession extends BaseSession {
41 41
       transformType,
42 42
       vec.vec(this.origin, point),
43 43
       data.boundsRotation,
44
-      isAspectRatioLocked
44
+      isAspectRatioLocked || isAllAspectRatioLocked
45 45
     )
46 46
 
47 47
     this.scaleX = newBoundingBox.scaleX
@@ -115,6 +115,10 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
115 115
 
116 116
   const hasUnlockedShapes = initialShapes.length > 0
117 117
 
118
+  const isAllAspectRatioLocked = initialShapes.every(
119
+    (shape) => shape.isAspectRatioLocked
120
+  )
121
+
118 122
   const shapesBounds = Object.fromEntries(
119 123
     initialShapes.map((shape) => [
120 124
       shape.id,
@@ -133,6 +137,7 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
133 137
   return {
134 138
     type: transformType,
135 139
     hasUnlockedShapes,
140
+    isAllAspectRatioLocked,
136 141
     currentPageId,
137 142
     initialBounds: commonBounds,
138 143
     shapeBounds: Object.fromEntries(

+ 56
- 26
state/state.ts View File

@@ -75,6 +75,19 @@ const state = createState({
75 75
     PANNED_CAMERA: {
76 76
       do: 'panCamera',
77 77
     },
78
+    ZOOMED_TO_ACTUAL: {
79
+      if: 'hasSelection',
80
+      do: 'zoomCameraToSelectionActual',
81
+      else: 'zoomCameraToActual',
82
+    },
83
+    ZOOMED_TO_SELECTION: {
84
+      if: 'hasSelection',
85
+      do: 'zoomCameraToSelection',
86
+    },
87
+    ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
88
+    ZOOMED_IN: 'zoomIn',
89
+    ZOOMED_OUT: 'zoomOut',
90
+    RESET_CAMERA: 'resetCamera',
78 91
     TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
79 92
     TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
80 93
     TOGGLED_SHAPE_ASPECT_LOCK: {
@@ -93,17 +106,6 @@ const state = createState({
93 106
     TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
94 107
     TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
95 108
     CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
96
-    RESET_CAMERA: 'resetCamera',
97
-    ZOOMED_TO_FIT: 'zoomCameraToFit',
98
-    ZOOMED_TO_SELECTION: {
99
-      if: 'hasSelection',
100
-      do: 'zoomCameraToSelection',
101
-    },
102
-    ZOOMED_TO_ACTUAL: {
103
-      if: 'hasSelection',
104
-      do: 'zoomCameraToSelectionActual',
105
-      else: 'zoomCameraToActual',
106
-    },
107 109
     SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
108 110
     NUDGED: { do: 'nudgeSelection' },
109 111
     USED_PEN_DEVICE: 'enablePenLock',
@@ -115,11 +117,6 @@ const state = createState({
115 117
       on: {
116 118
         MOUNTED: [
117 119
           'restoreSavedData',
118
-          {
119
-            if: 'hasSelection',
120
-            do: 'zoomCameraToSelectionActual',
121
-            else: ['zoomCameraToFit', 'zoomCameraToActual'],
122
-          },
123 120
           {
124 121
             to: 'ready',
125 122
           },
@@ -127,6 +124,12 @@ const state = createState({
127 124
       },
128 125
     },
129 126
     ready: {
127
+      onEnter: {
128
+        wait: 0.01,
129
+        if: 'hasSelection',
130
+        do: 'zoomCameraToSelectionActual',
131
+        else: ['zoomCameraToFit', 'zoomCameraToActual'],
132
+      },
130 133
       on: {
131 134
         UNMOUNTED: [
132 135
           { unless: 'isReadOnly', do: 'forceSave' },
@@ -141,7 +144,7 @@ const state = createState({
141 144
             UNDO: 'undo',
142 145
             REDO: 'redo',
143 146
             SAVED_CODE: 'saveCode',
144
-            DELETED: 'deleteSelectedIds',
147
+            DELETED: 'deleteSelection',
145 148
             STARTED_PINCHING: { to: 'pinching' },
146 149
             INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
147 150
             DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
@@ -282,6 +285,9 @@ const state = createState({
282 285
         usingTool: {
283 286
           initial: 'draw',
284 287
           onEnter: 'clearSelectedIds',
288
+          on: {
289
+            TOGGLED_TOOL_LOCK: 'toggleToolLock',
290
+          },
285 291
           states: {
286 292
             draw: {
287 293
               initial: 'creating',
@@ -311,7 +317,7 @@ const state = createState({
311 317
                       to: 'draw.creating',
312 318
                     },
313 319
                     CANCELLED: {
314
-                      do: ['cancelSession', 'deleteSelectedIds'],
320
+                      do: ['cancelSession', 'deleteSelection'],
315 321
                       to: 'selecting',
316 322
                     },
317 323
                     MOVED_POINTER: 'updateDrawSession',
@@ -351,7 +357,7 @@ const state = createState({
351 357
                       },
352 358
                     ],
353 359
                     CANCELLED: {
354
-                      do: ['cancelSession', 'deleteSelectedIds'],
360
+                      do: ['cancelSession', 'deleteSelection'],
355 361
                       to: 'selecting',
356 362
                     },
357 363
                   },
@@ -537,7 +543,7 @@ const state = createState({
537 543
               },
538 544
             ],
539 545
             CANCELLED: {
540
-              do: ['cancelSession', 'deleteSelectedIds'],
546
+              do: ['cancelSession', 'deleteSelection'],
541 547
               to: 'selecting',
542 548
             },
543 549
           },
@@ -849,14 +855,35 @@ const state = createState({
849 855
     aspectLockSelection(data) {
850 856
       commands.toggle(data, 'isAspectRatioLocked')
851 857
     },
858
+    deleteSelection(data) {
859
+      commands.deleteSelected(data)
860
+    },
852 861
 
853 862
     /* --------------------- Camera --------------------- */
854 863
 
855
-    resetCamera(data) {
856
-      data.camera.zoom = 1
857
-      data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
864
+    zoomIn(data) {
865
+      const { camera } = data
866
+      const i = Math.round((camera.zoom * 100) / 25)
867
+      const center = [window.innerWidth / 2, window.innerHeight / 2]
858 868
 
859
-      document.documentElement.style.setProperty('--camera-zoom', '1')
869
+      const p0 = screenToWorld(center, data)
870
+      camera.zoom = Math.min(3, (i + 1) * 0.25)
871
+      const p1 = screenToWorld(center, data)
872
+      camera.point = vec.add(camera.point, vec.sub(p1, p0))
873
+
874
+      setZoomCSS(camera.zoom)
875
+    },
876
+    zoomOut(data) {
877
+      const { camera } = data
878
+      const i = Math.round((camera.zoom * 100) / 25)
879
+      const center = [window.innerWidth / 2, window.innerHeight / 2]
880
+
881
+      const p0 = screenToWorld(center, data)
882
+      camera.zoom = Math.max(0.1, (i - 1) * 0.25)
883
+      const p1 = screenToWorld(center, data)
884
+      camera.point = vec.add(camera.point, vec.sub(p1, p0))
885
+
886
+      setZoomCSS(camera.zoom)
860 887
     },
861 888
     zoomCameraToActual(data) {
862 889
       const { camera } = data
@@ -967,8 +994,11 @@ const state = createState({
967 994
 
968 995
       setZoomCSS(camera.zoom)
969 996
     },
970
-    deleteSelectedIds(data) {
971
-      commands.deleteSelected(data)
997
+    resetCamera(data) {
998
+      data.camera.zoom = 1
999
+      data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
1000
+
1001
+      document.documentElement.style.setProperty('--camera-zoom', '1')
972 1002
     },
973 1003
 
974 1004
     /* ---------------------- History ---------------------- */

+ 1
- 1
utils/utils.ts View File

@@ -1487,7 +1487,7 @@ export function getChildIndexBelow(
1487 1487
 export function forceIntegerChildIndices(shapes: Shape[]) {
1488 1488
   for (let i = 0; i < shapes.length; i++) {
1489 1489
     const shape = shapes[i]
1490
-    getShapeUtils(shape).setChildIndex(shape, i + 1)
1490
+    getShapeUtils(shape).setProperty(shape, 'childIndex', i + 1)
1491 1491
   }
1492 1492
 }
1493 1493
 export function setZoomCSS(zoom: number) {

Loading…
Cancel
Save