├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .prettierignore ├── .prettierrc.json ├── .readme ├── board.png ├── screenshot1.png ├── screenshot2.png └── screenshot3.png ├── README.md ├── commitlint.config.js ├── package.json ├── public ├── favicon.ico ├── images │ ├── figures.png │ └── pen.png ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ ├── colorSelector │ │ ├── index.css │ │ └── index.js │ ├── controlsBar │ │ ├── index.css │ │ └── index.js │ ├── drawingBox │ │ ├── index.css │ │ └── index.js │ ├── figureSelector │ │ ├── index.css │ │ └── index.js │ ├── figuresOperate │ │ ├── index.css │ │ └── index.js │ ├── historyModal │ │ ├── index.css │ │ └── index.js │ ├── moreSetting │ │ ├── index.css │ │ └── index.js │ ├── propsSelector │ │ ├── index.css │ │ └── index.js │ ├── saveModal │ │ ├── index.css │ │ └── index.js │ └── saveWork │ │ ├── index.css │ │ └── index.js ├── index.css ├── index.js ├── reportWebVitals.js ├── setupTests.js └── utils │ ├── createBesselUtil.js │ ├── createCircleUtil.js │ ├── createLineUtil.js │ ├── createPathUtil.js │ ├── createRectUtil.js │ ├── createSvgChildUtil.js │ ├── createTriangleUtil.js │ ├── deleteItemUtil.js │ ├── figuresOperateUtil.js │ ├── modalUtil.js │ ├── mountComponent.js │ ├── readIndexDbUtil.js │ ├── reduxUtil.js │ └── saveToIndexDbUtil.js └── yarn.lock /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: deploy 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: yarn 29 | - run: yarn run build 30 | - name: ssh deploy 31 | # You may pin to the exact commit or the version. 32 | # uses: easingthemes/ssh-deploy@c711f2c3391cac2876bf4c833590077f02e4bcb8 33 | uses: easingthemes/ssh-deploy@v2.2.11 34 | with: 35 | # Private Key 36 | SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }} 37 | # Remote host 38 | REMOTE_HOST: ${{ secrets.SSH_HOST }} 39 | # Remote user 40 | REMOTE_USER: ${{ secrets.SSH_USERNAME }} 41 | # Source directory 42 | SOURCE: "build/" 43 | # Target directory 44 | TARGET: "/home/liucan/webstorm/svg-drawing-board/" 45 | 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | #idea 3 | .idea 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | 3 | build 4 | public 5 | node_modules -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.readme/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/.readme/board.png -------------------------------------------------------------------------------- /.readme/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/.readme/screenshot1.png -------------------------------------------------------------------------------- /.readme/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/.readme/screenshot2.png -------------------------------------------------------------------------------- /.readme/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/.readme/screenshot3.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前端画板(基于 React 和 SVG) 2 | 3 | 可以绘制各种图形,包括圆形、矩形、三角形、直线、弧线;可以设置图形轮廓线颜色、宽度;可以填充封闭图形,填充可选择颜色,图形可重叠。可以保存绘制内容到浏览器储存。具有撤销、恢复功能。 4 | 5 |

6 | 7 | 8 | 9 |

前端画板(基于React和SVG)

10 |

11 | 可以绘制各种图形,包括圆形、矩形、三角形、直线、弧线 12 |

13 | 查看Demo 14 | · 15 | 报告Bug 16 | · 17 | 提出新特性 18 |

19 | 20 | ## 目录 21 | 22 | - [屏幕截图](#屏幕截图) 23 | - [上手指南](#上手指南) 24 | - [环境要求](#开发前的配置要求) 25 | - [安装步骤](#安装步骤) 26 | - [文件目录说明](#文件目录说明) 27 | - [开发的架构](#开发的架构) 28 | - [部署](#部署) 29 | - [使用到的框架](#使用到的框架) 30 | - [版本控制](#版本控制) 31 | - [作者](#作者) 32 | 33 | ### 屏幕截图 34 | 35 | 1. 主界面,绘画界面,底部工具栏支持鼠标滚轮滚动,对触屏滑动进行了优化 36 | 37 | ![screenshot1](./.readme/screenshot1.png) 38 | 39 | 2. 保存绘画和导出绘画 Modal,支持导出 SVG\PNG 两种格式,支持储存到 indexDb 40 | 41 | ![screenshot2](./.readme/screenshot2.png) 42 | 43 | 3. 历史记录,可以查看、打开和删除(删除时无提示)过去保存的绘图 44 | 45 | ![screenshot3](./.readme/screenshot3.png) 46 | 47 | ### 上手指南 48 | 49 | 打开网页即可食用,支持触屏操作,也能通过鼠标来绘制 50 | 51 | ###### 环境要求 52 | 53 | 1. Node.js(尽量新的版本) 54 | 2. 最好使用 chrome、safari 或 firefox 55 | 56 | ###### **安装步骤** 57 | 58 | 1. 克隆本项目源码 59 | 60 | ```shell 61 | git clone https://github.com/kfyidrig/svg-drawing-board.git 62 | ``` 63 | 64 | 2. 按照项目依赖 65 | 66 | ```shell 67 | yarn 68 | #如果你没安装yarn,请先安装yarn 69 | ``` 70 | 71 | 3. 在浏览器预览 72 | 73 | ```shell 74 | yarn start 75 | ``` 76 | 77 | 4. 打包生成成品 78 | 79 | ```shell 80 | yarn run build 81 | #build目录即可看到成品 82 | ``` 83 | 84 | ### 文件目录说明 85 | 86 | 暂无 87 | 88 | ### 部署 89 | 90 | 暂无 91 | 92 | ### 使用到的框架 93 | 94 | - [react](https://react.docschina.org/) 95 | 96 | ### 贡献者 97 | 98 | 请阅读**CONTRIBUTING.md** 查阅为该项目做出贡献的开发者。 99 | 100 | ### 版本控制 101 | 102 | 该项目使用 Git 进行版本管理。您可以在 repository 参看当前可用版本。 103 | 104 | ### 作者 105 | 106 | liucan@ieleven.xyz 107 | 108 | _您也可以在贡献者名单中参看所有参与该项目的开发者。_ 109 | 110 | ### 版权说明 111 | 112 | 该项目签署了 MIT 授权许可,详情请参阅 [LICENSE.txt](https://github.com/shaojintian/Best_README_template/blob/master/LICENSE.txt) 113 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-drawing-board", 3 | "version": "1.3.0", 4 | "private": false, 5 | "dependencies": { 6 | "@sedan-utils/svgsaver": "^0.9.3", 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "htmlsvg": "^1.2.3", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-redux": "^7.2.4", 14 | "react-scripts": "4.0.3", 15 | "redux": "^4.1.0", 16 | "web-vitals": "^1.0.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "postinstall": "husky install", 24 | "prepublishOnly": "pinst --disable", 25 | "postpublish": "pinst --enable" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "commitlint": "^12.1.4", 41 | "@commitlint/cli": "^12.1.4", 42 | "@commitlint/config-conventional": "^12.1.4", 43 | "husky": "^6.0.0", 44 | "pinst": "^2.1.6", 45 | "prettier": "2.3.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/public/favicon.ico -------------------------------------------------------------------------------- /public/images/figures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/public/images/figures.png -------------------------------------------------------------------------------- /public/images/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liucan233/svg-drawing-board/2e5d14c1684f8fce8d92062aa895a44689e4d3fc/public/images/pen.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | painting works-随时开启你的创作 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #svg-wrap { 2 | height: 100vh; 3 | overflow-y: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import React, { Fragment } from "react"; 3 | import DrawingBox from "./components/drawingBox"; 4 | import ToolBar from "./components/controlsBar"; 5 | import HistoryModal from "./components/historyModal"; 6 | import SaveModal from "./components/saveModal"; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | test("renders learn react link", () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/colorSelector/index.css: -------------------------------------------------------------------------------- 1 | .color-selector { 2 | height: 100%; 3 | overflow: hidden; 4 | } 5 | .color-selector > span { 6 | display: inline-block; 7 | margin: 0 10px; 8 | width: 35px; 9 | height: 35px; 10 | border-radius: 5px; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/colorSelector/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | 5 | const colors = ["#ff776d", "#020826", "#8c7851", "#008eff", "orange"]; 6 | 7 | function ColorSelector() { 8 | const newAction = useDispatch(); 9 | const { drawingStyle } = useSelector((state) => state); 10 | 11 | function handleClick({ target }) { 12 | const { dataset } = target; 13 | if (!dataset.color) return; 14 | drawingStyle.color = dataset.color; 15 | if (drawingStyle.fill !== "transparent") drawingStyle.fill = dataset.color; 16 | newAction({ type: "SET_DRAWING_STYLE", style: drawingStyle }); 17 | } 18 | return ( 19 |
20 | {colors.map((item, index) => { 21 | return ( 22 | 28 | ); 29 | })} 30 |
31 | ); 32 | } 33 | 34 | export default ColorSelector; 35 | -------------------------------------------------------------------------------- /src/components/controlsBar/index.css: -------------------------------------------------------------------------------- 1 | .tool-bar-wrap { 2 | margin: 0 auto; 3 | position: absolute; 4 | bottom: 25px; 5 | left: 50%; 6 | height: 59px; 7 | width: 80%; 8 | user-select: none; 9 | background-color: #fffffe; 10 | border-radius: 10px; 11 | box-shadow: 0 0 1px 0 #e2e2e2; 12 | transform: translateX(-50%); 13 | overflow: auto; 14 | box-sizing: border-box; 15 | scrollbar-width: thin; 16 | } 17 | .tool-bar { 18 | height: 100%; 19 | touch-action: pan-x; 20 | width: 1400px; 21 | display: flow-root; 22 | white-space: nowrap; 23 | } 24 | .tool-bar > div { 25 | margin: 0 10px; 26 | float: left; 27 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 28 | } 29 | 30 | .tool-bar-wrap::-webkit-scrollbar { 31 | height: 5px; 32 | } 33 | .tool-bar-wrap::-webkit-scrollbar-thumb { 34 | border-radius: 8px; 35 | background-color: #adb5bd; 36 | border-top: 2px solid transparent; 37 | } 38 | .tool-bar-wrap::-webkit-scrollbar-track { 39 | width: 8px; 40 | border-radius: 8px; 41 | background: none; 42 | } 43 | 44 | .tool-bar-item { 45 | margin: 0 10px; 46 | display: inline-block; 47 | cursor: pointer; 48 | position: relative; 49 | top: 50%; 50 | transform: translateY(-50%); 51 | /*vertical-align: bottom;*/ 52 | } 53 | -------------------------------------------------------------------------------- /src/components/controlsBar/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React, { useRef } from "react"; 3 | import ColorSelector from "../colorSelector"; 4 | import FigureSelector from "../figureSelector"; 5 | import PropsSelector from "../propsSelector"; 6 | import FiguresOperate from "../figuresOperate"; 7 | import SaveWork from "../saveWork"; 8 | 9 | function ToolBar() { 10 | const wrapRef = useRef(); 11 | 12 | function handleWheel(e) { 13 | const { current } = wrapRef; 14 | e.stopPropagation(); 15 | current.scrollTo({ 16 | left: ~~(current.scrollLeft + e.deltaY * 0.5) 17 | }); 18 | } 19 | return ( 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default ToolBar; 33 | -------------------------------------------------------------------------------- /src/components/drawingBox/index.css: -------------------------------------------------------------------------------- 1 | .drawing-box { 2 | margin: 0; 3 | padding: 0; 4 | height: 100vh; 5 | width: 100vw; 6 | touch-action: none; 7 | box-sizing: border-box; 8 | background-color: #f9f4ef; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/drawingBox/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import React, { Fragment, useEffect, useState } from "react"; 4 | import { createLine } from "../../utils/createLineUtil"; 5 | import { createPath } from "../../utils/createPathUtil"; 6 | import { createRect } from "../../utils/createRectUtil"; 7 | import { createCircle } from "../../utils/createCircleUtil"; 8 | import { createTriangle } from "../../utils/createTriangleUtil"; 9 | import { createBessel } from "../../utils/createBesselUtil"; 10 | import createSvgChildUtil from "../../utils/createSvgChildUtil"; 11 | 12 | let onTouch = false; 13 | let onPainting = false; 14 | let updated = false; 15 | let requested = false; 16 | 17 | function DrawingBox() { 18 | const newAction = useDispatch(); 19 | const [figure, setFigure] = useState({}); 20 | const { drawingStyle } = useSelector((state) => state); 21 | const { figures } = useSelector((state) => state); 22 | 23 | function handleMouseDown(e) { 24 | onPainting = true; 25 | const { nativeEvent } = e; 26 | const { color, width, fill } = drawingStyle; 27 | figure.color = color; 28 | figure.width = width; 29 | figure.fill = fill; 30 | figure.path = ""; 31 | figure.type = drawingStyle.type; 32 | figure.key = Date.now(); 33 | figure.downX = nativeEvent.offsetX; 34 | figure.downY = nativeEvent.offsetY; 35 | } 36 | 37 | function updateComponent() { 38 | requested = true; 39 | setFigure({ ...figure }); 40 | } 41 | 42 | function handleMouseMove(e) { 43 | if (!onPainting) return; 44 | figure.flag = true; 45 | let handleDrawing = function () {}; 46 | const { type } = drawingStyle; 47 | if (type === "path") handleDrawing = createPath; 48 | else if (type === "line") handleDrawing = createLine; 49 | else if (type === "rect") handleDrawing = createRect; 50 | else if (type === "circle") handleDrawing = createCircle; 51 | else if (type === "triangle") handleDrawing = createTriangle; 52 | else if (type === "arc") handleDrawing = createBessel; 53 | handleDrawing(e, figure); 54 | if (!updated) { 55 | updated = true; 56 | requestAnimationFrame(updateComponent); 57 | } 58 | } 59 | 60 | function handleMoseUp() { 61 | if (onTouch) return; 62 | onPainting = false; 63 | if (!figure?.flag) return; 64 | const lastFigure = { ...figure }; 65 | figure.type = ""; 66 | newAction({ type: "ADD_NEW_FIGURE", figure: lastFigure }); 67 | } 68 | 69 | function touchToMouse(e) { 70 | const { 71 | touches: [{ clientX, clientY }], 72 | } = e; 73 | return { nativeEvent: { offsetX: clientX, offsetY: clientY } }; 74 | } 75 | 76 | function handleTouchEnd(e) { 77 | e.preventDefault(); 78 | onTouch = false; 79 | handleMoseUp(); 80 | } 81 | 82 | function handleTouchMove(e) { 83 | if (onTouch) { 84 | handleMouseMove(touchToMouse(e)); 85 | } else { 86 | onTouch = true; 87 | handleMouseDown(touchToMouse(e)); 88 | } 89 | } 90 | 91 | function handleMouseLeave() { 92 | onPainting = false; 93 | } 94 | 95 | useEffect(function () { 96 | if (requested){ 97 | updated = false; 98 | requested = false; 99 | } 100 | },[figure]); 101 | 102 | return ( 103 | 113 | {figures.map((item) => createSvgChildUtil(item))} 114 | {createSvgChildUtil(figure)} 115 | 116 | ); 117 | } 118 | export default DrawingBox; 119 | -------------------------------------------------------------------------------- /src/components/figureSelector/index.css: -------------------------------------------------------------------------------- 1 | .figure-selector { 2 | --fillColor: orange; 3 | height: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | .figure-selector > svg { 8 | fill: var(--fillColor); 9 | width: 35px; 10 | height: 35px; 11 | margin: 0 10px; 12 | box-sizing: border-box; 13 | vertical-align: top; 14 | cursor: pointer; 15 | border-bottom-width: 0; 16 | transition: border-bottom-width ease-in-out 0.1s; 17 | } 18 | 19 | .figure-selector > svg > path { 20 | pointer-events: none; 21 | } 22 | 23 | .item-active { 24 | border-bottom: #e82a2a solid 2px !important; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/figureSelector/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React, { useState } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | 5 | function FigureSelector() { 6 | const newAction = useDispatch(); 7 | const { drawingStyle } = useSelector((state) => state); 8 | const [active, setActive] = useState("path"); 9 | 10 | function handleClick({ target }) { 11 | const { dataset } = target; 12 | if (dataset.type === active) return; 13 | setActive(dataset.type); 14 | console.log(dataset.type); 15 | newAction({ 16 | type: "SET_DRAWING_STYLE", 17 | style: { ...drawingStyle, type: dataset.type }, 18 | }); 19 | } 20 | return ( 21 |
26 | 35 | 36 | 37 | 46 | 47 | 48 | 56 | 57 | 58 | 66 | 67 | 68 | 76 | 77 | 78 | 86 | 87 | 88 |
89 | ); 90 | } 91 | 92 | export default FigureSelector; 93 | -------------------------------------------------------------------------------- /src/components/figuresOperate/index.css: -------------------------------------------------------------------------------- 1 | .tool-operate { 2 | height: 100%; 3 | display: flow-root; 4 | } 5 | .tool-operate > svg, 6 | .tool-operate > span { 7 | float: left; 8 | } 9 | .tool-operate > svg > path { 10 | pointer-events: none; 11 | } 12 | .tool-operate > svg[data-active="true"] { 13 | fill: #bb9e5a; 14 | } 15 | .tool-operate > svg[data-active="false"] { 16 | fill: #eee; 17 | } 18 | .tool-operate > svg[data-active="true"]:active { 19 | fill: #d4b15f; 20 | transition: fill ease-in-out 0.1s; 21 | } 22 | 23 | .tool-clear { 24 | color: #f25042; 25 | margin-left: 15px; 26 | } 27 | 28 | .tool-clear:active { 29 | color: #ff786b; 30 | transition: color ease-in-out 0.1s; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/figuresOperate/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React, { useEffect, useState } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import Figures from "../../utils/figuresOperateUtil"; 5 | 6 | function FiguresOperate() { 7 | const newAction = useDispatch(); 8 | const [cancel, setCancel] = useState(true); 9 | const [recover, setRecover] = useState(false); 10 | const state = useSelector((state) => state); 11 | 12 | function handleCancel() { 13 | if (!cancel) return; 14 | newAction({ type: "BACK_TO" }); 15 | } 16 | function handleRecover() { 17 | if (!recover) return; 18 | newAction({ type: "RECOVER_TO" }); 19 | } 20 | function handleClear() { 21 | if (!cancel) return; 22 | newAction({ type: "CLEAR_FIGURES" }); 23 | } 24 | useEffect( 25 | function () { 26 | if (Figures.cancelStack.length > 1) setCancel(true); 27 | else setCancel(false); 28 | if (Figures.recoveryStack.length) setRecover(true); 29 | else setRecover(false); 30 | }, 31 | [state] 32 | ); 33 | return ( 34 |
35 | 44 | 52 | 53 | 54 | 63 | 70 | 71 | 72 | 清空画布 73 | 74 |
75 | ); 76 | } 77 | 78 | export default FiguresOperate; 79 | -------------------------------------------------------------------------------- /src/components/historyModal/index.css: -------------------------------------------------------------------------------- 1 | .modal-wrap { 2 | width: 100%; 3 | height: 100vh; 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | transform: scale(0); 8 | } 9 | .modal-wrap[data-active="true"] { 10 | width: 100%; 11 | height: 100vh; 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | transform: scale(1); 16 | background-color: rgba(0, 0, 0, 0.2); 17 | transition: background-color ease-in-out 0.2s; 18 | } 19 | .modal { 20 | padding: 8px 35px; 21 | margin: 0 auto; 22 | width: 400px; 23 | max-width: 90%; 24 | height: 500px; 25 | position: relative; 26 | background-color: #f9f4ef; 27 | border-radius: 10px; 28 | box-sizing: border-box; 29 | box-shadow: 0 0 20px 0 #c6c6c6; 30 | transition: all ease-in-out 0.1s; 31 | top: 30px; 32 | opacity: 0.5; 33 | color: #020826; 34 | overflow-y: auto; 35 | touch-action: pan-y; 36 | } 37 | .modal::-webkit-scrollbar { 38 | display: none; 39 | } 40 | .modal[data-active="true"] { 41 | top: 70px; 42 | opacity: 1; 43 | } 44 | .modal > h1 { 45 | text-align: center; 46 | font-size: 22px; 47 | } 48 | .modal > h3 { 49 | margin-top: 30px; 50 | font-size: 18px; 51 | } 52 | .history-project-wrap { 53 | margin: 0; 54 | padding: 0; 55 | width: 100%; 56 | height: 157px; 57 | overflow: hidden; 58 | } 59 | .center{ 60 | position: relative; 61 | top:30%; 62 | } 63 | .history-project-info{ 64 | color: #f25042; 65 | text-align: center; 66 | } 67 | .history-project { 68 | margin: 15px auto; 69 | padding: 15px; 70 | width: 100%; 71 | background-color: #fffffe; 72 | border-radius: 8px; 73 | box-shadow: 0 0 5px 1px #eeeeee; 74 | box-sizing: border-box; 75 | display: flow-root; 76 | } 77 | .history-project > div { 78 | float: left; 79 | } 80 | 81 | .project-preview { 82 | width: 50%; 83 | color: #020826; 84 | } 85 | .project-preview > img { 86 | width: 100%; 87 | height: 95px; 88 | object-fit: cover; 89 | border-radius: 5px; 90 | border: #8c7851 solid 1px; 91 | object-position: center; 92 | vertical-align: bottom; 93 | box-sizing: border-box; 94 | } 95 | 96 | .project-details { 97 | width: 49%; 98 | height: 100%; 99 | padding-left: 12px; 100 | box-sizing: border-box; 101 | } 102 | 103 | .project-details > h3 { 104 | margin: 5px 0; 105 | font-size: 16px; 106 | text-overflow: ellipsis; 107 | white-space: nowrap; 108 | overflow: hidden; 109 | } 110 | .project-details > h4 { 111 | color: #716040; 112 | margin: 10px 0; 113 | font-size: 12px; 114 | text-overflow: ellipsis; 115 | white-space: nowrap; 116 | overflow: hidden; 117 | } 118 | .project-btn { 119 | position: relative; 120 | bottom: 0; 121 | } 122 | .project-btn > span { 123 | cursor: pointer; 124 | font-size: 14px; 125 | display: inline-block; 126 | padding: 3px 5px; 127 | color: #fffffe; 128 | border-radius: 3px; 129 | margin: 5px 10px; 130 | background-color: #8c7851; 131 | } 132 | 133 | .project-delete { 134 | background-color: #d7bc83; 135 | opacity: 0.85; 136 | } 137 | .history-project-wrap[data-remove="true"] { 138 | height: 0; 139 | transition: all ease-in-out 0.4s; 140 | } 141 | -------------------------------------------------------------------------------- /src/components/historyModal/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React, { useEffect, useState } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import readIndexDb from "../../utils/readIndexDbUtil"; 5 | import deleteItem from "../../utils/deleteItemUtil"; 6 | 7 | function HistoryModal() { 8 | const newAction = useDispatch(); 9 | const [projects, setProjects] = useState([]); 10 | const { historyModal } = useSelector((state) => state); 11 | 12 | function handleClick({ target }) { 13 | if (target.className === "modal-wrap") 14 | newAction({ type: "CHANGE_HISTORY_MODAL" }); 15 | } 16 | 17 | function handleSuccess(data) { 18 | setProjects(data); 19 | } 20 | 21 | function handleError() {} 22 | 23 | function handleOpen({ target }) { 24 | newAction({ type: "CHANGE_HISTORY_MODAL" }); 25 | const tar = projects[target.dataset.index]; 26 | if (!tar.id) return; 27 | newAction({ type: "CHANGE_PROJECT", project: tar }); 28 | } 29 | 30 | function handleDelete({ target }) { 31 | deleteItem( 32 | projects[target.dataset.index].id, 33 | function () { 34 | projects[target.dataset.index].remove = true; 35 | setProjects([...projects]); 36 | }, 37 | function (event) { 38 | console.log(event); 39 | } 40 | ); 41 | } 42 | 43 | useEffect( 44 | function () { 45 | if (historyModal) readIndexDb(handleSuccess, handleError); 46 | }, 47 | [historyModal] 48 | ); 49 | 50 | return ( 51 |
56 |
57 |

继续上次的工作

58 | {projects.map((item, index) => { 59 | const { id, preview, date, name, remove } = item; 60 | return ( 61 |
62 |
63 |
64 | 65 |
66 |
67 |

{name}

68 |

{date}

69 |
70 | 75 | 删除 76 | 77 | 78 | 打开 79 | 80 |
81 |
82 |
83 |
84 | ); 85 | })} 86 | {projects.length===0? 87 |

这里似乎什么也没有哦! 😜

88 | : 89 |

这么快就到底啦! 😜😜😜

90 | } 91 |
92 |
93 | ); 94 | } 95 | 96 | export default HistoryModal; 97 | -------------------------------------------------------------------------------- /src/components/moreSetting/index.css: -------------------------------------------------------------------------------- 1 | /*.more-tool {*/ 2 | /* --width: 320px;*/ 3 | /* padding: 0 20px;*/ 4 | /* position: fixed;*/ 5 | /* left: calc((100% - var(--width)) / 2);*/ 6 | /* width: var(--width);*/ 7 | /* height: 220px;*/ 8 | /* background-color: #f9f4ef;*/ 9 | /* border-radius: 10px;*/ 10 | /* box-sizing: border-box;*/ 11 | /* box-shadow: 0 0 20px 0 #c6c6c6;*/ 12 | /*}*/ 13 | 14 | @keyframes show { 15 | from { 16 | top: 0; 17 | opacity: 0; 18 | } 19 | to { 20 | top: 200px; 21 | opacity: 1; 22 | } 23 | } 24 | @keyframes hidden { 25 | from { 26 | top: 200px; 27 | opacity: 1; 28 | } 29 | to { 30 | top: 10px; 31 | opacity: 0; 32 | } 33 | } 34 | 35 | .more-tool-wrap { 36 | width: 100%; 37 | height: 100vh; 38 | position: fixed; 39 | top: 0; 40 | left: 0; 41 | background-color: rgba(0, 0, 0, 0.2); 42 | transition: background-color ease-in 0.25s; 43 | } 44 | .more-tool > h1 { 45 | text-align: center; 46 | font-size: 22px; 47 | color: #020826; 48 | letter-spacing: 5px; 49 | } 50 | .more-tool > p { 51 | font-size: 17px; 52 | color: #716040; 53 | } 54 | .more-tool > p:first-letter { 55 | margin: 0 3px; 56 | padding: 0; 57 | font-size: 38px; 58 | float: left; 59 | line-height: 40px; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/moreSetting/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React, { Fragment, useState } from "react"; 3 | 4 | function MoreSetting({ figures, destroy }) { 5 | const [show, setShow] = useState({ 6 | top: "200px", 7 | animation: "show ease-in .25s", 8 | }); 9 | 10 | function handleHidden() { 11 | setShow({ 12 | top: "10px", 13 | opacity: 0, 14 | animation: "hidden ease-out .25s", 15 | }); 16 | } 17 | 18 | function handleAnimationEnd({ animationName }) { 19 | if (animationName === "hidden") destroy(); 20 | } 21 | 22 | return ( 23 | 24 |
25 |
30 |

关于

31 |

32 | 简易的在线画布,能绘制各种图形,包括圆形、矩形、三角形、直线、弧线; 33 | 可以设置图形轮廓线颜色、宽度;可以填充封闭图形,填充可选择颜色,图形可重叠。 34 |

35 |
36 | 37 | ); 38 | } 39 | 40 | export default MoreSetting; 41 | -------------------------------------------------------------------------------- /src/components/propsSelector/index.css: -------------------------------------------------------------------------------- 1 | .props-selector { 2 | height: 100%; 3 | font-size: 17px; 4 | color: #020826; 5 | display: flow-root; 6 | } 7 | 8 | #range { 9 | cursor: pointer; 10 | width: 120px; 11 | vertical-align: text-bottom; 12 | -webkit-appearance: none; 13 | } 14 | #range::-webkit-slider-runnable-track { 15 | -webkit-appearance: none; 16 | border-radius: 5px; 17 | background-color: #eee; 18 | } 19 | #range::-webkit-slider-thumb { 20 | -webkit-appearance: none; 21 | border: 2px solid #1ba1e2; 22 | height: 16px; 23 | width: 16px; 24 | border-radius: 8px; 25 | background-color: #fffffe; 26 | } 27 | input[type="range"]::-moz-range-track{ 28 | height: 17px; 29 | background: #eee; 30 | border:none; 31 | border-radius: 4px; 32 | } 33 | 34 | input[type="range"]::-moz-range-thumb{ 35 | width:16px; 36 | height:16px; 37 | border-radius: 8px; 38 | background-color: #fffffe; 39 | border: 2px solid #1ba1e2; 40 | } 41 | #checkbox { 42 | display: none; 43 | } 44 | 45 | .input-emoji { 46 | padding: 0 5px; 47 | font-size: 12px; 48 | cursor: pointer; 49 | display: inline-block; 50 | width: 38px; 51 | height: 20px; 52 | position: relative; 53 | background-color: #cce4f5; 54 | vertical-align: bottom; 55 | border-radius: 12px; 56 | box-sizing: border-box; 57 | transition: background-color ease-in-out 0.15s; 58 | } 59 | .input-tips { 60 | line-height: 12px; 61 | width: 50%; 62 | color: #020826; 63 | position: absolute; 64 | top: 50%; 65 | transform: translateY(-50%); 66 | } 67 | .input-tips:nth-child(2) { 68 | right: 0; 69 | color: #fffffe; 70 | } 71 | .input-emoji::after { 72 | content: "😛"; 73 | display: inline-block; 74 | font-size: 15px; 75 | position: absolute; 76 | left: 0; 77 | top: 50%; 78 | transform: translateY(-50%); 79 | transition: left ease-in-out 0.15s; 80 | } 81 | .input-fill > #checkbox:checked + .input-emoji { 82 | background-color: #bfd5e7; 83 | } 84 | .input-fill > #checkbox:checked + .input-emoji::after { 85 | content: "😄"; 86 | left: 17px; 87 | } 88 | -------------------------------------------------------------------------------- /src/components/propsSelector/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | 5 | function PropsSelector() { 6 | const newAction = useDispatch(); 7 | const { drawingStyle } = useSelector((state) => state); 8 | 9 | function handleOnchange({ target }) { 10 | drawingStyle.width = Number(target.value); 11 | newAction({ type: "SET_DRAWING_STYLE", style: drawingStyle }); 12 | } 13 | 14 | function handleFill({ target }) { 15 | drawingStyle.fill = target.checked ? drawingStyle.color : "transparent"; 16 | newAction({ type: "SET_DRAWING_STYLE", style: drawingStyle }); 17 | } 18 | 19 | return ( 20 |
21 | 22 | 23 | 墨迹粗细: 24 | 32 | 33 | 34 | 35 | 36 | 填充内部: 37 | 38 | 43 | 44 |
45 | ); 46 | } 47 | 48 | export default PropsSelector; 49 | -------------------------------------------------------------------------------- /src/components/saveModal/index.css: -------------------------------------------------------------------------------- 1 | .export-item { 2 | display: flow-root; 3 | } 4 | 5 | .name-input { 6 | color: #716040; 7 | width: 70%; 8 | padding: 0 8px; 9 | outline: none; 10 | height: 25px; 11 | font-size: 16px; 12 | box-sizing: border-box; 13 | border: #eeeeee solid 1px; 14 | vertical-align: text-bottom; 15 | border-radius: 6px; 16 | box-shadow: 0 0 0 1px #c6c6c6; 17 | } 18 | .name-input:focus { 19 | outline: none; 20 | border-color: #2177d5; 21 | /*border: #0366d6 solid 1px;*/ 22 | box-shadow: 0 0 2px 1px #227bdb; 23 | } 24 | 25 | .export-item-name { 26 | float: left; 27 | color: #716040; 28 | } 29 | .export-btn { 30 | float: right; 31 | font-size: 17px; 32 | color: #f25042; 33 | cursor: pointer; 34 | margin-left: 10px; 35 | } 36 | .export-btn:hover { 37 | color: #ff7467; 38 | } 39 | .save-tips { 40 | height: 18px; 41 | color: #fc5a5a; 42 | text-align: center; 43 | transform: scaleY(0); 44 | transform-origin: top; 45 | } 46 | .save-tips[data-active="true"] { 47 | transform: scaleY(1); 48 | transition: transform ease-in-out 0.2s; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/saveModal/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React, { useState } from "react"; 3 | import SvgSaver from "@sedan-utils/svgsaver"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import Figures from "../../utils/figuresOperateUtil"; 6 | import saveToIndexDb from "../../utils/saveToIndexDbUtil"; 7 | 8 | function SaveModal() { 9 | const newAction = useDispatch(); 10 | const { project } = useSelector((state) => state); 11 | const { saveModal } = useSelector((state) => state); 12 | const [warning, setWarning] = useState({ download: "", saveProject: "" }); 13 | 14 | function handleClick({ target }) { 15 | if (target.className === "modal-wrap") { 16 | setWarning({ download: "", saveProject: "" }); 17 | newAction({ type: "CHANGE_SAVE_MODAL" }); 18 | } 19 | } 20 | 21 | function handleExport({ target }) { 22 | const svg = document.querySelector(".drawing-box"); 23 | svg.setAttribute("width", svg.clientWidth.toString()); 24 | const svgDownloader = new SvgSaver(); 25 | target.innerText === "PNG" 26 | ? svgDownloader.asPng(svg, `works-${Date.now()}.png`) 27 | : svgDownloader.asSvg(svg, `works-${Date.now()}.svg`); 28 | warning.download = "导出成功咯,快去打开看看吧!"; 29 | setWarning({ ...warning }); 30 | } 31 | 32 | function handleSave() { 33 | const { length } = Figures.cancelStack; 34 | if (length < 2) { 35 | warning.saveProject = "画板为空或你未做任何更改哦,稍后再来吧!"; 36 | setWarning({ ...warning }); 37 | return; 38 | } 39 | let { id, name } = project; 40 | const now = new Date(); 41 | if (!id) id = now.getTime(); 42 | const svg = document.querySelector(".drawing-box"); 43 | svg.setAttribute("width", svg.clientWidth.toString()); 44 | const svgDownloader = new SvgSaver(); 45 | if (!name) name = "works-undefined-name"; 46 | const data = { 47 | id, 48 | name, 49 | date: now.toLocaleString(), 50 | preview: svgDownloader.getUri(svg), 51 | figures: Figures.cancelStack[length - 1], 52 | }; 53 | newAction({ type: "SET_PROJECT", project: data }); 54 | saveToIndexDb(data, handleSuccess, handleError); 55 | } 56 | 57 | function handleSuccess() { 58 | warning.saveProject = "作品保存成功咯!🎉🎉🎉"; 59 | setWarning({ ...warning }); 60 | } 61 | 62 | function handleError() { 63 | warning.saveProject = "项目保存失败,请到GitHub提交issue"; 64 | setWarning({ ...warning }); 65 | } 66 | 67 | function handleChange({ target }) { 68 | newAction({ 69 | type: "SET_PROJECT", 70 | project: { ...project, name: target.value }, 71 | }); 72 | } 73 | 74 | return ( 75 |
76 |
77 |

保存作品

78 |

导出作品

79 |
80 | 导出为PNG/SVG 81 | 82 | PNG 83 | 84 | 85 | SVG 86 | 87 |
88 |

89 | {warning.download} 90 |

91 | 92 |

保存进度

93 |
94 | 100 | 101 | 保存 102 | 103 |
104 | 105 |

关于项目

106 |
107 | 项目地址 108 | 109 | svg-drawing-board 110 | 111 |
112 |

113 | {warning.saveProject} 114 |

115 |
116 |
117 | ); 118 | } 119 | 120 | export default SaveModal; 121 | -------------------------------------------------------------------------------- /src/components/saveWork/index.css: -------------------------------------------------------------------------------- 1 | .save-work { 2 | height: 100%; 3 | font-size: 17px; 4 | } 5 | .save-work > span { 6 | vertical-align: top; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/saveWork/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | 5 | function SaveWork() { 6 | const newAction = useDispatch(); 7 | 8 | function handleSave() { 9 | newAction({ type: "CHANGE_SAVE_MODAL" }); 10 | } 11 | 12 | function handleHistory() { 13 | newAction({ type: "CHANGE_HISTORY_MODAL" }); 14 | } 15 | return ( 16 |
17 | 18 | 历史 19 | 20 | 21 | 保存 22 | 23 |
24 | ); 25 | } 26 | 27 | export default SaveWork; 28 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import App from "./App"; 5 | import store from "./utils/reduxUtil"; 6 | import reportWebVitals from "./reportWebVitals"; 7 | import { Provider } from "react-redux"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById("svg-wrap") 16 | ); 17 | 18 | // If you want to start measuring performance in your app, pass a function 19 | // to log results (for example: reportWebVitals(console.log)) 20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 21 | reportWebVitals(); 22 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/utils/createBesselUtil.js: -------------------------------------------------------------------------------- 1 | function createBessel(e, figure) { 2 | const { nativeEvent } = e; 3 | figure.path = ` Q ${nativeEvent.offsetX} ${figure.downY} ${nativeEvent.offsetX} ${nativeEvent.offsetY}`; 4 | } 5 | 6 | function renderBessel(figure) { 7 | const { key, color, width, downX, downY, path } = figure; 8 | const newPath = `M${downX} ${downY} ` + path; 9 | return ( 10 | 19 | ); 20 | } 21 | 22 | export { createBessel, renderBessel }; 23 | -------------------------------------------------------------------------------- /src/utils/createCircleUtil.js: -------------------------------------------------------------------------------- 1 | function createCircle(e, figure) { 2 | const { nativeEvent } = e; 3 | const { downX, downY } = figure; 4 | figure.cx = (downX + nativeEvent.offsetX) / 2; 5 | figure.cy = (downY + nativeEvent.offsetY) / 2; 6 | figure.r = 7 | Math.max( 8 | Math.abs(nativeEvent.offsetX - downX), 9 | Math.abs(nativeEvent.offsetY - downY) 10 | ) / 2; 11 | } 12 | 13 | function renderCircle(figure) { 14 | const { cx, cy, r, key, color, fill, width } = figure; 15 | return ( 16 | 27 | ); 28 | } 29 | 30 | export { createCircle, renderCircle }; 31 | -------------------------------------------------------------------------------- /src/utils/createLineUtil.js: -------------------------------------------------------------------------------- 1 | function createLine(e, figure) { 2 | const { nativeEvent } = e; 3 | figure.moveX = nativeEvent.offsetX; 4 | figure.moveY = nativeEvent.offsetY; 5 | } 6 | 7 | function renderLine(figure) { 8 | const { downX, downY, moveX, moveY, key, color, width, fill } = figure; 9 | if (!moveX) return; 10 | return ( 11 | 23 | ); 24 | } 25 | 26 | export { createLine, renderLine }; 27 | -------------------------------------------------------------------------------- /src/utils/createPathUtil.js: -------------------------------------------------------------------------------- 1 | function createPath(e, figures) { 2 | const { nativeEvent } = e; 3 | figures.path += `L${nativeEvent.offsetX} ${nativeEvent.offsetY}`; 4 | } 5 | 6 | function renderPath(figure) { 7 | const { path, key, color, width, downX, downY } = figure; 8 | if (!path) return null; 9 | const newPath = `M${downX} ${downY} ` + path; 10 | return ( 11 | 20 | ); 21 | } 22 | 23 | export { createPath, renderPath }; 24 | -------------------------------------------------------------------------------- /src/utils/createRectUtil.js: -------------------------------------------------------------------------------- 1 | function createRect(e, figure) { 2 | const { nativeEvent } = e; 3 | figure.moveX = nativeEvent.offsetX; 4 | figure.moveY = nativeEvent.offsetY; 5 | } 6 | 7 | function renderRect(figure) { 8 | const { key, color, fill, width } = figure; 9 | let { downX, downY, moveX, moveY } = figure; 10 | if (!moveX) return; 11 | let rectWidth = moveX - downX, 12 | rectHeight = moveY - downY; 13 | if (rectWidth < 0) { 14 | downX += rectWidth; 15 | rectWidth = -rectWidth; 16 | } 17 | if (rectHeight < 0) { 18 | downY += rectHeight; 19 | rectHeight = -rectHeight; 20 | } 21 | return ( 22 | 34 | ); 35 | } 36 | 37 | export { createRect, renderRect }; 38 | -------------------------------------------------------------------------------- /src/utils/createSvgChildUtil.js: -------------------------------------------------------------------------------- 1 | import { renderPath } from "./createPathUtil"; 2 | import { renderLine } from "./createLineUtil"; 3 | import { renderRect } from "./createRectUtil"; 4 | import { renderCircle } from "./createCircleUtil"; 5 | import { renderTriangle } from "./createTriangleUtil"; 6 | import { renderBessel } from "./createBesselUtil"; 7 | 8 | function createSvgChildUtil(item) { 9 | const { type } = item; 10 | if (type === "empty") return null; 11 | let getPath = function () {}; 12 | if (type === "path") getPath = renderPath; 13 | else if (type === "line") getPath = renderLine; 14 | else if (type === "rect") getPath = renderRect; 15 | else if (type === "circle") getPath = renderCircle; 16 | else if (type === "triangle") getPath = renderTriangle; 17 | else if (type === "arc") getPath = renderBessel; 18 | else return null; 19 | return getPath(item); 20 | } 21 | 22 | export default createSvgChildUtil; 23 | -------------------------------------------------------------------------------- /src/utils/createTriangleUtil.js: -------------------------------------------------------------------------------- 1 | function createTriangle(e, figure) { 2 | const { nativeEvent } = e; 3 | figure.moveX = nativeEvent.offsetX; 4 | figure.moveY = nativeEvent.offsetY; 5 | } 6 | 7 | function renderTriangle(figure) { 8 | const { downX, downY, moveX, moveY, key, color, width, fill } = figure; 9 | if (!moveX) return; 10 | const pointsStr = `${ 11 | (downX + moveX) / 2 12 | } ${downY},${downX} ${moveY},${moveX} ${moveY}`; 13 | return ( 14 | 23 | ); 24 | } 25 | 26 | export { createTriangle, renderTriangle }; 27 | -------------------------------------------------------------------------------- /src/utils/deleteItemUtil.js: -------------------------------------------------------------------------------- 1 | function handleSuccess({ target: { result } }) { 2 | const { key, onsuccess, onerror } = this.callback; 3 | const transaction = result.transaction("lists", "readwrite"); 4 | const store = transaction.objectStore("lists"); 5 | const request = store.delete(key); 6 | request.onerror = onerror; 7 | request.onsuccess = onsuccess; 8 | } 9 | 10 | function handleError() { 11 | console.error("数据库连接失败"); 12 | this.callback.onerror(); 13 | } 14 | 15 | function deleteItem(key, onsuccess, onerror) { 16 | const request = indexedDB.open("projects", 3); 17 | request.callback = { key, onsuccess, onerror }; 18 | request.onsuccess = handleSuccess; 19 | request.onerror = handleError; 20 | } 21 | 22 | export default deleteItem; 23 | -------------------------------------------------------------------------------- /src/utils/figuresOperateUtil.js: -------------------------------------------------------------------------------- 1 | function FiguresBak() { 2 | this.recoveryStack = []; 3 | this.cancelStack = [[]]; 4 | this.clearFlag = false; 5 | } 6 | 7 | FiguresBak.prototype.addStatus = function (figures) { 8 | this.clearFlag = false; 9 | this.recoveryStack = []; 10 | this.cancelStack.push([...figures]); 11 | }; 12 | 13 | FiguresBak.prototype.backStatus = function () { 14 | if (!this.clearFlag) { 15 | const tmp = this.cancelStack.pop(); 16 | if (tmp) this.recoveryStack.push(tmp); 17 | } else { 18 | this.clearFlag = false; 19 | } 20 | const { length } = this.cancelStack; 21 | if (length) return this.cancelStack[length - 1]; 22 | return null; 23 | }; 24 | FiguresBak.prototype.recStatus = function () { 25 | const tmp = this.recoveryStack.pop(); 26 | if (tmp) this.cancelStack.push(tmp); 27 | return tmp; 28 | }; 29 | 30 | FiguresBak.prototype.clearAllStack = function () { 31 | if(this.clearFlag) return; 32 | this.clearFlag = true; 33 | }; 34 | 35 | const Figures = new FiguresBak(); 36 | 37 | export default Figures; 38 | -------------------------------------------------------------------------------- /src/utils/modalUtil.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import MoreSetting from "../components/moreSetting"; 4 | 5 | function moreSetting(figures) { 6 | const container = document.getElementById("setting"); 7 | 8 | function destroy() { 9 | ReactDOM.unmountComponentAtNode(container); 10 | } 11 | 12 | ReactDOM.render( 13 | , 14 | container 15 | ); 16 | } 17 | 18 | const Modal = { moreSetting }; 19 | 20 | export default Modal; 21 | -------------------------------------------------------------------------------- /src/utils/mountComponent.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import store from "./reduxUtil"; 3 | import { Provider } from "react-redux"; 4 | 5 | function mountComponent(Component, id) { 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById(id) 11 | ); 12 | } 13 | 14 | export default mountComponent; 15 | -------------------------------------------------------------------------------- /src/utils/readIndexDbUtil.js: -------------------------------------------------------------------------------- 1 | function handleSuccess({ target: { result } }) { 2 | const data = []; 3 | const { onsuccess, onerror } = this.callback; 4 | const transaction = result.transaction("lists"); 5 | const store = transaction.objectStore("lists"); 6 | const request = store.openCursor(); 7 | request.onerror = onerror; 8 | request.onsuccess = function (event) { 9 | const cursor = event.target.result; 10 | if (cursor) { 11 | cursor.value.remove = false; 12 | data.push(cursor.value); 13 | cursor.continue(); 14 | } else { 15 | onsuccess(data); 16 | console.info("数据库读取完成"); 17 | } 18 | }; 19 | } 20 | 21 | function handleError() { 22 | console.error("数据库连接失败"); 23 | this.callback.onerror(); 24 | } 25 | 26 | function handleUpgrade({ target: { result } }) { 27 | console.info("尝试创建数据库"); 28 | if (!result.objectStoreNames.contains("lists")) { 29 | result.createObjectStore("lists", { keyPath: "id" }); 30 | } 31 | } 32 | 33 | function readIndexDb(onsuccess, onerror) { 34 | const request = indexedDB.open("projects", 3); 35 | request.callback = { onsuccess, onerror }; 36 | request.onsuccess = handleSuccess; 37 | request.onerror = handleError; 38 | request.onupgradeneeded=handleUpgrade; 39 | } 40 | 41 | export default readIndexDb; 42 | -------------------------------------------------------------------------------- /src/utils/reduxUtil.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import Figures from "./figuresOperateUtil"; 3 | 4 | const defaultState = { 5 | figures: [], 6 | drawingStyle: { 7 | type: "path", 8 | color: "orange", 9 | width: 5, 10 | fill: "transparent", 11 | }, 12 | historyModal: false, 13 | saveModal: false, 14 | project: { 15 | id: "", 16 | name: "", 17 | }, 18 | }; 19 | 20 | function handleAction(state = defaultState, action) { 21 | const { type } = action; 22 | if (type === "ADD_NEW_FIGURE") { 23 | state.figures.push(action.figure); 24 | Figures.addStatus(state.figures); 25 | return { ...state }; 26 | } else if (type === "SET_PROJECT") { 27 | state.project = action.project; 28 | return { ...state }; 29 | } else if (type === "SET_DRAWING_STYLE") { 30 | state.drawingStyle = action.style; 31 | return { ...state }; 32 | } else if (type === "CLEAR_FIGURES") { 33 | Figures.clearAllStack(); 34 | state.figures = []; 35 | return { ...state }; 36 | } else if (type === "BACK_TO") { 37 | const lastStatus = Figures.backStatus(); 38 | if (lastStatus) { 39 | state.figures = [...lastStatus]; 40 | return { ...state }; 41 | } 42 | } else if (type === "RECOVER_TO") { 43 | const lastStatus = Figures.recStatus(); 44 | if (lastStatus) { 45 | state.figures = [...lastStatus]; 46 | return { ...state }; 47 | } 48 | } else if (type === "CHANGE_HISTORY_MODAL") { 49 | state.historyModal = !state.historyModal; 50 | return { ...state }; 51 | } else if (type === "CHANGE_SAVE_MODAL") { 52 | state.saveModal = !state.saveModal; 53 | return { ...state }; 54 | } else if (type === "CHANGE_PROJECT") { 55 | const { figures, id, name } = action.project; 56 | console.log(figures, id, name); 57 | state.figures = figures; 58 | state.project.name = name; 59 | state.project.id = id; 60 | Figures.clearAllStack(); 61 | Figures.addStatus(state.figures); 62 | return { ...state }; 63 | } 64 | return state; 65 | } 66 | 67 | const store = createStore(handleAction); 68 | export default store; 69 | -------------------------------------------------------------------------------- /src/utils/saveToIndexDbUtil.js: -------------------------------------------------------------------------------- 1 | function handleOpenSuccess({ target: { result } }) { 2 | const { data, onsuccess, onerror } = this.callback; 3 | const transaction = result.transaction("lists", "readwrite"); 4 | const store = transaction.objectStore("lists"); 5 | const request = store.put(data); 6 | request.onerror = onerror; 7 | request.onsuccess = onsuccess; 8 | } 9 | 10 | function handleUpgrade({ target: { result } }) { 11 | console.info("尝试创建数据库"); 12 | if (!result.objectStoreNames.contains("lists")) { 13 | result.createObjectStore("lists", { keyPath: "id" }); 14 | } 15 | } 16 | 17 | function handleOpenError() { 18 | console.error("数据库连接失败"); 19 | this.callback.onerror(); 20 | } 21 | 22 | function saveToIndexDb(data, onsuccess, onerror) { 23 | const request = indexedDB.open("projects", 3); 24 | request.callback = { data, onsuccess, onerror }; 25 | request.onsuccess = handleOpenSuccess; 26 | request.onerror = handleOpenError; 27 | request.onupgradeneeded = handleUpgrade; 28 | } 29 | 30 | export default saveToIndexDb; 31 | --------------------------------------------------------------------------------