Steve Ruiz 4 лет назад
Родитель
Сommit
f38481efee

+ 4
- 5
components/canvas/canvas.tsx Просмотреть файл

@@ -1,21 +1,20 @@
1 1
 import styled from "styles"
2 2
 import { useRef } from "react"
3 3
 import useZoomEvents from "hooks/useZoomEvents"
4
-import useZoomPanEffect from "hooks/useZoomPanEffect"
4
+import useCamera from "hooks/useCamera"
5
+import Page from "./page"
5 6
 
6 7
 export default function Canvas() {
7 8
   const rCanvas = useRef<SVGSVGElement>(null)
8 9
   const rGroup = useRef<SVGGElement>(null)
9 10
   const events = useZoomEvents(rCanvas)
10 11
 
11
-  useZoomPanEffect(rGroup)
12
+  useCamera(rGroup)
12 13
 
13 14
   return (
14 15
     <MainSVG ref={rCanvas} {...events}>
15 16
       <MainGroup ref={rGroup}>
16
-        <circle cx={100} cy={100} r={50} />
17
-        <circle cx={500} cy={500} r={200} />
18
-        <circle cx={200} cy={800} r={100} />
17
+        <Page />
19 18
       </MainGroup>
20 19
     </MainSVG>
21 20
   )

+ 24
- 0
components/canvas/page.tsx Просмотреть файл

@@ -0,0 +1,24 @@
1
+import { useSelector } from "state"
2
+import { deepCompareArrays } from "utils/utils"
3
+import Shape from "./shape"
4
+
5
+/* 
6
+On each state change, compare node ids of all shapes
7
+on the current page. Kind of expensive but only happens
8
+here; and still cheaper than any other pattern I've found.
9
+*/
10
+
11
+export default function Page() {
12
+  const currentPageShapeIds = useSelector((state) => {
13
+    const { currentPageId, document } = state.data
14
+    return Object.keys(document.pages[currentPageId].shapes)
15
+  }, deepCompareArrays)
16
+
17
+  return (
18
+    <>
19
+      {currentPageShapeIds.map((shapeId) => (
20
+        <Shape key={shapeId} id={shapeId} />
21
+      ))}
22
+    </>
23
+  )
24
+}

+ 29
- 0
components/canvas/shape.tsx Просмотреть файл

@@ -0,0 +1,29 @@
1
+import { memo } from "react"
2
+import { useSelector } from "state"
3
+import { ShapeType } from "types"
4
+import Circle from "./shapes/circle"
5
+import Rectangle from "./shapes/rectangle"
6
+
7
+/*
8
+Gets the shape from the current page's shapes, using the
9
+provided ID. Depending on the shape's type, return the
10
+component for that type.
11
+*/
12
+
13
+function Shape({ id }: { id: string }) {
14
+  const shape = useSelector((state) => {
15
+    const { currentPageId, document } = state.data
16
+    return document.pages[currentPageId].shapes[id]
17
+  })
18
+
19
+  switch (shape.type) {
20
+    case ShapeType.Circle:
21
+      return <Circle {...shape} />
22
+    case ShapeType.Rectangle:
23
+      return <Rectangle {...shape} />
24
+    default:
25
+      return null
26
+  }
27
+}
28
+
29
+export default memo(Shape)

+ 5
- 0
components/canvas/shapes/circle.tsx Просмотреть файл

@@ -0,0 +1,5 @@
1
+import { CircleShape } from "types"
2
+
3
+export default function Circle({ point, radius }: CircleShape) {
4
+  return <circle cx={point[0]} cy={point[1]} r={radius} fill="black" />
5
+}

+ 13
- 0
components/canvas/shapes/rectangle.tsx Просмотреть файл

@@ -0,0 +1,13 @@
1
+import { RectangleShape } from "types"
2
+
3
+export default function Rectangle({ point, size }: RectangleShape) {
4
+  return (
5
+    <rect
6
+      x={point[0]}
7
+      y={point[1]}
8
+      width={size[0]}
9
+      height={size[1]}
10
+      fill="black"
11
+    />
12
+  )
13
+}

+ 1
- 0
components/status-bar.tsx Просмотреть файл

@@ -35,6 +35,7 @@ const StatusBarContainer = styled("div", {
35 35
   alignItems: "center",
36 36
   backgroundColor: "white",
37 37
   gap: 8,
38
+  fontSize: "$1",
38 39
   padding: "0 16px",
39 40
   zIndex: 200,
40 41
 })

hooks/useZoomPanEffect.ts → hooks/useCamera.ts Просмотреть файл

@@ -6,9 +6,7 @@ import state from "state"
6 6
  * the SVG group to reflect the correct zoom and pan.
7 7
  * @param ref
8 8
  */
9
-export default function useZoomPanEffect(
10
-  ref: React.MutableRefObject<SVGGElement>
11
-) {
9
+export default function useCamera(ref: React.MutableRefObject<SVGGElement>) {
12 10
   useEffect(() => {
13 11
     let { camera } = state.data
14 12
 
@@ -19,7 +17,6 @@ export default function useZoomPanEffect(
19 17
       const { point, zoom } = data.camera
20 18
 
21 19
       if (point !== camera.point || zoom !== camera.zoom) {
22
-        console.log("changed!")
23 20
         g.setAttribute(
24 21
           "transform",
25 22
           `scale(${zoom}) translate(${point[0]} ${point[1]})`

+ 11
- 4
hooks/useZoomEvents.ts Просмотреть файл

@@ -2,6 +2,11 @@ import React, { useEffect, useRef } from "react"
2 2
 import state from "state"
3 3
 import * as vec from "utils/vec"
4 4
 
5
+/**
6
+ * Capture zoom gestures (pinches, wheels and pans) and send to the state.
7
+ * @param ref
8
+ * @returns
9
+ */
5 10
 export default function useZoomEvents(
6 11
   ref: React.MutableRefObject<SVGSVGElement>
7 12
 ) {
@@ -30,17 +35,19 @@ export default function useZoomEvents(
30 35
     }
31 36
 
32 37
     function handleTouchMove(e: TouchEvent) {
33
-      if (e.ctrlKey) {
34
-        e.preventDefault()
35
-      }
38
+      e.preventDefault()
36 39
 
37 40
       if (e.touches.length === 2) {
38 41
         const { clientX: x0, clientY: y0 } = e.touches[0]
39 42
         const { clientX: x1, clientY: y1 } = e.touches[1]
40 43
 
41 44
         const dist = vec.dist([x0, y0], [x1, y1])
45
+        const point = vec.med([x0, y0], [x1, y1])
42 46
 
43
-        state.send("WHEELED", { delta: [0, dist - rTouchDist.current] })
47
+        state.send("WHEELED", {
48
+          delta: dist - rTouchDist.current,
49
+          point,
50
+        })
44 51
 
45 52
         rTouchDist.current = dist
46 53
       }

+ 3
- 1
package.json Просмотреть файл

@@ -10,10 +10,12 @@
10 10
   "dependencies": {
11 11
     "@state-designer/react": "^1.7.1",
12 12
     "@stitches/react": "^0.1.9",
13
+    "@types/uuid": "^8.3.0",
13 14
     "next": "10.2.0",
14 15
     "perfect-freehand": "^0.4.7",
15 16
     "react": "17.0.2",
16
-    "react-dom": "17.0.2"
17
+    "react-dom": "17.0.2",
18
+    "uuid": "^8.3.2"
17 19
   },
18 20
   "devDependencies": {
19 21
     "@types/next": "^9.0.0",

+ 24
- 13
pages/_document.tsx Просмотреть файл

@@ -1,21 +1,32 @@
1
-import Document, {
2
-  DocumentContext,
3
-  Html,
4
-  Head,
5
-  Main,
6
-  NextScript,
7
-} from "next/document"
8
-import { dark } from "styles"
1
+import NextDocument, { Html, Head, Main, NextScript } from "next/document"
2
+import { dark, getCssString } from "styles"
9 3
 
10
-class MyDocument extends Document {
11
-  static async getInitialProps(ctx: DocumentContext) {
12
-    const initialProps = await Document.getInitialProps(ctx)
13
-    return { ...initialProps }
4
+class MyDocument extends NextDocument {
5
+  static async getInitialProps(ctx) {
6
+    try {
7
+      const initialProps = await NextDocument.getInitialProps(ctx)
8
+
9
+      return {
10
+        ...initialProps,
11
+        styles: (
12
+          <>
13
+            {initialProps.styles}
14
+            <style
15
+              id="stitches"
16
+              dangerouslySetInnerHTML={{ __html: getCssString() }}
17
+            />
18
+          </>
19
+        ),
20
+      }
21
+    } catch (e) {
22
+      console.log(e.message)
23
+    } finally {
24
+    }
14 25
   }
15 26
 
16 27
   render() {
17 28
     return (
18
-      <Html>
29
+      <Html lang="en">
19 30
         <Head />
20 31
         <body className={dark}>
21 32
           <Main />

+ 0
- 2
pages/index.tsx Просмотреть файл

@@ -1,5 +1,3 @@
1
-import Head from "next/head"
2
-import Image from "next/image"
3 1
 import Editor from "components/editor"
4 2
 
5 3
 export default function Home() {

+ 46
- 3
state/state.ts Просмотреть файл

@@ -1,13 +1,56 @@
1 1
 import { createSelectorHook, createState } from "@state-designer/react"
2
-import * as vec from "utils/vec"
3 2
 import { clamp, screenToWorld } from "utils/utils"
4
-import { IData } from "types"
3
+import * as vec from "utils/vec"
4
+import { Data, ShapeType } from "types"
5 5
 
6
-const initialData: IData = {
6
+const initialData: Data = {
7 7
   camera: {
8 8
     point: [0, 0],
9 9
     zoom: 1,
10 10
   },
11
+  currentPageId: "page0",
12
+  document: {
13
+    pages: {
14
+      page0: {
15
+        id: "page0",
16
+        type: "page",
17
+        name: "Page 0",
18
+        childIndex: 0,
19
+        shapes: {
20
+          shape0: {
21
+            id: "shape0",
22
+            type: ShapeType.Circle,
23
+            name: "Shape 0",
24
+            parentId: "page0",
25
+            childIndex: 1,
26
+            point: [100, 100],
27
+            radius: 50,
28
+            rotation: 0,
29
+          },
30
+          shape1: {
31
+            id: "shape1",
32
+            type: ShapeType.Rectangle,
33
+            name: "Shape 1",
34
+            parentId: "page0",
35
+            childIndex: 1,
36
+            point: [300, 300],
37
+            size: [200, 200],
38
+            rotation: 0,
39
+          },
40
+          shape2: {
41
+            id: "shape2",
42
+            type: ShapeType.Circle,
43
+            name: "Shape 2",
44
+            parentId: "page0",
45
+            childIndex: 2,
46
+            point: [200, 800],
47
+            radius: 25,
48
+            rotation: 0,
49
+          },
50
+        },
51
+      },
52
+    },
53
+  },
11 54
 }
12 55
 
13 56
 const state = createState({

+ 1
- 10
styles/globals.css Просмотреть файл

@@ -1,10 +1 @@
1
-* {
2
-  box-sizing: border-box;
3
-}
4
-
5
-html,
6
-body {
7
-  margin: 0;
8
-  padding: 0;
9
-  overscroll-behavior: none;
10
-}
1
+@import url("https://fonts.googleapis.com/css2?family=Recursive:wght@500;700&display=swap");

+ 8
- 2
styles/index.ts Просмотреть файл

@@ -1,4 +1,10 @@
1
-import styled, { css, globalStyles, light, dark } from "./stitches.config"
1
+import styled, {
2
+  css,
3
+  getCssString,
4
+  globalStyles,
5
+  light,
6
+  dark,
7
+} from "./stitches.config"
2 8
 
3 9
 export default styled
4
-export { css, globalStyles, light, dark }
10
+export { css, getCssString, globalStyles, light, dark }

+ 20
- 7
styles/stitches.config.ts Просмотреть файл

@@ -1,11 +1,22 @@
1
-import { createCss, global } from "@stitches/react"
1
+import { createCss, defaultThemeMap } from "@stitches/react"
2 2
 
3
-const { styled, css, theme } = createCss({
3
+const { styled, global, css, theme, getCssString } = createCss({
4
+  themeMap: {
5
+    ...defaultThemeMap,
6
+  },
4 7
   theme: {
5 8
     colors: {},
6 9
     space: {},
7
-    fontSizes: {},
8
-    fonts: {},
10
+    fontSizes: {
11
+      0: "10px",
12
+      1: "12px",
13
+      2: "13px",
14
+      3: "16px",
15
+      4: "18px",
16
+    },
17
+    fonts: {
18
+      ui: `"Recursive", system-ui, sans-serif`,
19
+    },
9 20
     fontWeights: {},
10 21
     lineHeights: {},
11 22
     letterSpacings: {},
@@ -26,12 +37,14 @@ const dark = theme({})
26 37
 const globalStyles = global({
27 38
   "*": { boxSizing: "border-box" },
28 39
   "html, body": {
29
-    padding: "0",
30
-    margin: "0",
40
+    padding: "0px",
41
+    margin: "0px",
31 42
     overscrollBehavior: "none",
43
+    fontFamily: "$ui",
44
+    fontSize: "$2",
32 45
   },
33 46
 })
34 47
 
35 48
 export default styled
36 49
 
37
-export { css, globalStyles, light, dark }
50
+export { css, getCssString, globalStyles, light, dark }

+ 90
- 1
types.ts Просмотреть файл

@@ -1,6 +1,95 @@
1
-export interface IData {
1
+export interface Data {
2 2
   camera: {
3 3
     point: number[]
4 4
     zoom: number
5 5
   }
6
+  currentPageId: string
7
+  selectedIds: string[]
8
+  pointedId: string
9
+  document: {
10
+    pages: Record<string, Page>
11
+  }
12
+}
13
+
14
+export interface Page {
15
+  id: string
16
+  type: "page"
17
+  childIndex: number
18
+  name: string
19
+  shapes: Record<string, Shape>
20
+}
21
+
22
+export enum ShapeType {
23
+  Circle = "circle",
24
+  Ellipse = "ellipse",
25
+  Square = "square",
26
+  Rectangle = "rectangle",
27
+  Line = "line",
28
+  LineSegment = "lineSegment",
29
+  Dot = "dot",
30
+  Ray = "ray",
31
+  Glob = "glob",
32
+  Spline = "spline",
33
+  Cubic = "cubic",
34
+  Conic = "conic",
35
+}
36
+
37
+export interface BaseShape {
38
+  id: string
39
+  type: ShapeType
40
+  parentId: string
41
+  childIndex: number
42
+  name: string
43
+  rotation: 0
44
+}
45
+
46
+export interface DotShape extends BaseShape {
47
+  type: ShapeType.Dot
48
+  point: number[]
49
+}
50
+
51
+export interface CircleShape extends BaseShape {
52
+  type: ShapeType.Circle
53
+  point: number[]
54
+  radius: number
55
+}
56
+
57
+export interface EllipseShape extends BaseShape {
58
+  type: ShapeType.Ellipse
59
+  point: number[]
60
+  radiusX: number
61
+  radiusY: number
62
+}
63
+
64
+export interface LineShape extends BaseShape {
65
+  type: ShapeType.Line
66
+  point: number[]
67
+  vector: number[]
68
+}
69
+
70
+export interface RayShape extends BaseShape {
71
+  type: ShapeType.Ray
72
+  point: number[]
73
+  vector: number[]
74
+}
75
+
76
+export interface LineSegmentShape extends BaseShape {
77
+  type: ShapeType.LineSegment
78
+  start: number[]
79
+  end: number[]
80
+}
81
+
82
+export interface RectangleShape extends BaseShape {
83
+  type: ShapeType.Rectangle
84
+  point: number[]
85
+  size: number[]
6 86
 }
87
+
88
+export type Shape =
89
+  | CircleShape
90
+  | EllipseShape
91
+  | DotShape
92
+  | LineShape
93
+  | RayShape
94
+  | LineSegmentShape
95
+  | RectangleShape

+ 2
- 2
utils/utils.ts Просмотреть файл

@@ -1,8 +1,8 @@
1
-import { IData } from "types"
1
+import { Data } from "types"
2 2
 import * as svg from "./svg"
3 3
 import * as vec from "./vec"
4 4
 
5
-export function screenToWorld(point: number[], data: IData) {
5
+export function screenToWorld(point: number[], data: Data) {
6 6
   return vec.add(vec.div(point, data.camera.zoom), data.camera.point)
7 7
 }
8 8
 

+ 10
- 0
yarn.lock Просмотреть файл

@@ -1440,6 +1440,11 @@
1440 1440
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
1441 1441
   integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
1442 1442
 
1443
+"@types/uuid@^8.3.0":
1444
+  version "8.3.0"
1445
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
1446
+  integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
1447
+
1443 1448
 "@types/yargs-parser@*":
1444 1449
   version "20.2.0"
1445 1450
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
@@ -7263,6 +7268,11 @@ uuid@^3.3.2:
7263 7268
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
7264 7269
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
7265 7270
 
7271
+uuid@^8.3.2:
7272
+  version "8.3.2"
7273
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
7274
+  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
7275
+
7266 7276
 v8-compile-cache@^2.0.3:
7267 7277
   version "2.3.0"
7268 7278
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

Загрузка…
Отмена
Сохранить