├── .gitignore ├── client ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.jsx │ ├── api │ │ └── socket.js │ ├── assets │ │ ├── fonts │ │ │ ├── Roboto-Bold.ttf │ │ │ └── Roboto-Medium.ttf │ │ └── icons │ │ │ └── index.jsx │ ├── components │ │ ├── Canvas.jsx │ │ ├── Collaboration.jsx │ │ ├── Credits.jsx │ │ ├── Menu.jsx │ │ ├── Style.jsx │ │ ├── ToolBar.jsx │ │ ├── Ui.jsx │ │ ├── UndoRedo.jsx │ │ └── Zoom.jsx │ ├── global │ │ └── var.js │ ├── helper │ │ ├── canvas.js │ │ ├── element.js │ │ ├── keys.js │ │ └── ui.js │ ├── hooks │ │ ├── useCanvas.jsx │ │ ├── useDimension.jsx │ │ ├── useHistory.jsx │ │ └── useKeys.jsx │ ├── main.jsx │ ├── provider │ │ └── AppStates.jsx │ ├── styles │ │ └── index.css │ └── views │ │ └── WorkSpace.jsx ├── vercel.json └── vite.config.js ├── package.json ├── readme.md └── server ├── .gitignore ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | 10 | # env 11 | .env 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Editor directories and files 22 | .idea 23 | .DS_Store -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | dist 6 | dist-ssr 7 | *.local 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # env 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Editor directories and files 27 | .vscode/* 28 | !.vscode/extensions.json 29 | .idea 30 | .DS_Store 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sketchflow 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketchflow", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "dotenv": "^16.4.5", 14 | "framer-motion": "^11.0.8", 15 | "perfect-freehand": "^1.2.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.22.3", 19 | "socket.io-client": "^4.7.4", 20 | "socket.io-msgpack-parser": "^3.0.2", 21 | "uuid": "^9.0.1" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.2.43", 25 | "@types/react-dom": "^18.2.17", 26 | "@vitejs/plugin-react": "^4.2.1", 27 | "eslint": "^8.55.0", 28 | "eslint-plugin-react": "^7.33.2", 29 | "eslint-plugin-react-hooks": "^4.6.0", 30 | "eslint-plugin-react-refresh": "^0.4.5", 31 | "vite": "^5.0.8" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toouil/sketchflow/0428ce17dc596850431a8ed34be86328e7ce2a8b/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes, Navigate } from "react-router-dom"; 2 | import WorkSpace from "./views/WorkSpace"; 3 | 4 | function App() { 5 | return ( 6 | 7 | } /> 8 | } /> 9 | 10 | ); 11 | } 12 | 13 | export default App; -------------------------------------------------------------------------------- /client/src/api/socket.js: -------------------------------------------------------------------------------- 1 | import { io } from "socket.io-client"; 2 | import parser from "socket.io-msgpack-parser" 3 | 4 | const BACKEND_URL = import.meta.env.VITE_APP_SERVER_URL; 5 | export const socket = io(BACKEND_URL, { 6 | parser 7 | }); -------------------------------------------------------------------------------- /client/src/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toouil/sketchflow/0428ce17dc596850431a8ed34be86328e7ce2a8b/client/src/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /client/src/assets/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toouil/sketchflow/0428ce17dc596850431a8ed34be86328e7ce2a8b/client/src/assets/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /client/src/assets/icons/index.jsx: -------------------------------------------------------------------------------- 1 | const demention = 15; 2 | 3 | export const Pencil = () => ( 4 | 10 | 11 | 12 | ); 13 | 14 | export const Eraser = () => ( 15 | 21 | 22 | 23 | ); 24 | 25 | export const Circle = () => ( 26 | 32 | 33 | 34 | ); 35 | 36 | export const Rectangle = () => ( 37 | 43 | 44 | 45 | ); 46 | 47 | export const Line = () => ( 48 | 56 | 57 | 58 | ); 59 | 60 | export const Arrow = () => ( 61 | 74 | 75 | 76 | ); 77 | 78 | export const Selection = () => ( 79 | 85 | 86 | 87 | ); 88 | 89 | export const Diamond = () => ( 90 | 97 | 98 | 99 | ); 100 | 101 | export const Hand = () => ( 102 | 110 | 111 | 112 | ); 113 | 114 | export const Lock = () => ( 115 | 124 | 125 | 126 | 127 | ); 128 | 129 | export const SolidLine = () => ( 130 | 140 | 141 | 142 | ); 143 | 144 | export const DashedLine = () => ( 145 | 150 | 154 | 155 | ); 156 | 157 | export const DottedLine = () => ( 158 | 164 | 165 | 166 | ); 167 | 168 | export const Duplicate = () => ( 169 | 175 | 176 | 177 | ); 178 | 179 | export const Delete = () => ( 180 | 190 | 191 | 192 | ); 193 | 194 | export const Backward = () => ( 195 | 202 | 203 | 204 | ); 205 | 206 | export const Forward = () => ( 207 | 214 | 215 | 216 | ); 217 | 218 | export const ToBack = () => ( 219 | 226 | 227 | 228 | ); 229 | 230 | export const ToFront = () => ( 231 | 238 | 239 | 240 | ); 241 | 242 | export const Undo = () => ( 243 | 249 | 250 | 251 | ); 252 | 253 | export const Redo = () => ( 254 | 260 | 261 | 262 | ); 263 | 264 | export const MenuIcon = () => ( 265 | 275 | 276 | 277 | 278 | 279 | ); 280 | 281 | export const Xmark = () => ( 282 | 292 | 293 | 294 | 295 | ); 296 | 297 | export const Folder = () => ( 298 | 307 | 308 | 309 | ); 310 | 311 | export const Download = () => ( 312 | 321 | 322 | 323 | 324 | 325 | ); 326 | 327 | export const Image = () => ( 328 | 337 | 338 | 339 | 340 | 341 | ); 342 | export const Github = () => ( 343 | 352 | 353 | 354 | 355 | ); 356 | -------------------------------------------------------------------------------- /client/src/components/Canvas.jsx: -------------------------------------------------------------------------------- 1 | import useCanvas from "../hooks/useCanvas"; 2 | 3 | export default function Canvas() { 4 | const { 5 | canvasRef, 6 | dimension, 7 | handleMouseDown, 8 | handleMouseMove, 9 | handleMouseUp, 10 | handleWheel, 11 | } = useCanvas(); 12 | 13 | return ( 14 | <> 15 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/Collaboration.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { Xmark } from "../assets/icons"; 3 | import { useState } from "react"; 4 | import { useAppContext } from "../provider/AppStates"; 5 | import { v4 as uuid } from "uuid"; 6 | import { useSearchParams } from "react-router-dom"; 7 | import { socket } from "../api/socket"; 8 | 9 | export default function Collaboration() { 10 | const [searchParams, setSearchParams] = useSearchParams(); 11 | const { session, setSession } = useAppContext(); 12 | const [open, setOpen] = useState(false); 13 | const users = 0; 14 | 15 | const startSession = () => { 16 | const sessionId = uuid(); 17 | setSearchParams({ room: sessionId }); 18 | setSession(sessionId); 19 | socket.emit("join", sessionId); 20 | }; 21 | 22 | const endSession = () => { 23 | searchParams.delete("room"); 24 | socket.emit("leave", session); 25 | setSession(null); 26 | setOpen(false); 27 | window.history.replaceState(null, null, "/"); 28 | }; 29 | 30 | return ( 31 |
32 | 40 | 41 | {open && ( 42 | 43 | {session ? ( 44 | 45 | ) : ( 46 | 47 | )} 48 | 49 | )} 50 |
51 | ); 52 | } 53 | 54 | function CreateSession({ startSession }) { 55 | return ( 56 |
57 |

Live collaboration

58 |
59 |

Invite people to collaborate on your drawing.

60 |

61 | Don't worry, the session is end-to-end encrypted, and fully private. 62 | Not even our server can see what you draw. 63 |

64 |
65 | 66 |
67 | ); 68 | } 69 | 70 | function SessionInfo({ endSession }) { 71 | const copy = () => { 72 | navigator.clipboard.writeText(window.location.href); 73 | }; 74 | 75 | return ( 76 |
77 |

Live collaboration

78 | 79 |
80 | 81 |
82 | 88 | 91 |
92 |
93 |
94 | 97 |
98 |
99 | ); 100 | } 101 | 102 | function CollabBox({ collabState, children }) { 103 | const [Open, setOpen] = collabState; 104 | const exit = () => setOpen(false); 105 | 106 | return ( 107 |
108 | 115 | 121 | 124 | 125 | {children} 126 | 127 |
128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /client/src/components/Credits.jsx: -------------------------------------------------------------------------------- 1 | import { Github } from "../assets/icons"; 2 | 3 | export default function Credits() { 4 | return ( 5 |
6 | 7 | Created by KYROS 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/src/components/Menu.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Delete, Download, Folder, MenuIcon, Xmark } from "../assets/icons"; 3 | import { useAppContext } from "../provider/AppStates"; 4 | import { saveElements, uploadElements } from "../helper/element"; 5 | 6 | export default function Menu() { 7 | const { elements, setElements } = useAppContext(); 8 | const [show, setShow] = useState(false); 9 | 10 | return ( 11 |
12 | 19 | 20 | {show && } 21 |
22 | ); 23 | } 24 | 25 | function MenuBox({ elements, setElements, setShow }) { 26 | const uploadJson = () => uploadElements(setElements); 27 | const downloadJson = () => saveElements(elements); 28 | const reset = () => setElements([]); 29 | 30 | return ( 31 | <> 32 |
setShow(false)}>
33 |
34 | 37 | 40 | 43 |
44 | 45 | ); 46 | } -------------------------------------------------------------------------------- /client/src/components/Style.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | deleteElement, 4 | duplicateElement, 5 | minmax, 6 | moveElementLayer, 7 | updateElement, 8 | } from "../helper/element"; 9 | import { useAppContext } from "../provider/AppStates"; 10 | import { BACKGROUND_COLORS, STROKE_COLORS, STROKE_STYLES } from "../global/var"; 11 | import { Backward, Delete, Duplicate, Forward, ToBack, ToFront } from "../assets/icons"; 12 | 13 | export default function Style({ selectedElement }) { 14 | const { elements, setElements, setSelectedElement, setStyle } = 15 | useAppContext(); 16 | const [elementStyle, setElementStyle] = useState({ 17 | fill: selectedElement?.fill, 18 | strokeWidth: selectedElement?.strokeWidth, 19 | strokeStyle: selectedElement?.strokeStyle, 20 | strokeColor: selectedElement?.strokeColor, 21 | opacity: selectedElement?.opacity, 22 | }); 23 | 24 | useEffect(() => { 25 | setElementStyle({ 26 | fill: selectedElement?.fill, 27 | strokeWidth: selectedElement?.strokeWidth, 28 | strokeStyle: selectedElement?.strokeStyle, 29 | strokeColor: selectedElement?.strokeColor, 30 | opacity: selectedElement?.opacity, 31 | }); 32 | }, [selectedElement]); 33 | 34 | const setStylesStates = (styleObject) => { 35 | setElementStyle((prevState) => ({ ...prevState, ...styleObject })); 36 | setStyle((prevState) => ({ ...prevState, ...styleObject })); 37 | }; 38 | 39 | if (!selectedElement) return; 40 | return ( 41 |
42 |
43 |

Stroke

44 |
45 | {STROKE_COLORS.map((color, index) => ( 46 | 67 | ))} 68 |
69 |
70 |
71 |

Background

72 |
73 | {BACKGROUND_COLORS.map((fill, index) => ( 74 | 95 | ))} 96 |
97 |
98 |
99 |

Stroke width

100 |
101 | { 109 | setStylesStates({ strokeWidth: minmax(+target.value, [0, 20]) }); 110 | updateElement( 111 | selectedElement.id, 112 | { 113 | strokeWidth: minmax(+target.value, [0, 20]), 114 | }, 115 | setElements, 116 | elements 117 | ); 118 | }} 119 | /> 120 |
121 |
122 |
123 |

Stroke style

124 |
125 | {STROKE_STYLES.map((style, index) => ( 126 | 148 | ))} 149 |
150 |
151 |
152 |

Opacity

153 |
154 | { 162 | setStylesStates({ 163 | opacity: minmax(+target.value, [0, 100]), 164 | }); 165 | updateElement( 166 | selectedElement.id, 167 | { 168 | opacity: minmax(+target.value, [0, 100]), 169 | }, 170 | setElements, 171 | elements 172 | ); 173 | }} 174 | /> 175 |
176 |
177 | {selectedElement?.id && ( 178 | 179 |
180 |

Layers

181 |
182 | 197 | 212 | 222 | 232 |
233 |
234 | 235 |
236 |

Actions

237 |
238 | 252 | 267 |
268 |
269 |
270 | )} 271 |
272 | ); 273 | } 274 | -------------------------------------------------------------------------------- /client/src/components/ToolBar.jsx: -------------------------------------------------------------------------------- 1 | import { useAppContext } from "../provider/AppStates"; 2 | 3 | export default function ToolBar() { 4 | const { tools: toolCols, selectedTool, lockTool } = useAppContext(); 5 | 6 | return ( 7 |
8 | {toolCols.map((tools, index) => ( 9 |
10 | {tools.map((tool, index_) => ( 11 | 24 | ))} 25 |
26 | ))} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/Ui.jsx: -------------------------------------------------------------------------------- 1 | import { useAppContext } from "../provider/AppStates"; 2 | import Style from "./Style"; 3 | import ToolBar from "./ToolBar"; 4 | import Zoom from "./Zoom"; 5 | import UndoRedo from "./UndoRedo"; 6 | import Menu from "./Menu"; 7 | import Collaboration from "./Collaboration"; 8 | import Credits from "./Credits"; 9 | 10 | export default function Ui() { 11 | const { selectedElement, selectedTool, style } = useAppContext(); 12 | 13 | return ( 14 |
15 |
16 | 17 | 18 | 19 |
20 | {(!["selection", "hand"].includes(selectedTool) || selectedElement) && ( 21 |