├── .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 |    
 38 | 
 39 | 2. 保存绘画和导出绘画 Modal,支持导出 SVG\PNG 两种格式,支持储存到 indexDb
 40 | 
 41 |    
 42 | 
 43 | 3. 历史记录,可以查看、打开和删除(删除时无提示)过去保存的绘图
 44 | 
 45 |    
 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 |     
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 |       
37 |       
48 |       
58 |       
68 |       
78 |       
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 |       
53 | 
54 |       
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 |         
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 | 
--------------------------------------------------------------------------------