├── .gitignore ├── assets ├── src │ ├── width-1.afdesign │ ├── width-2.afdesign │ ├── width-3.afdesign │ ├── width-4.afdesign │ ├── icon-pen.afdesign │ └── icon-eraser.afdesign └── icons │ ├── width-1.svg │ ├── width-2.svg │ ├── width-3.svg │ ├── width-4.svg │ ├── icon-eraser.svg │ ├── icon-pen.svg │ └── trashcan.svg ├── README.md ├── LICENSE ├── index.html ├── style.css └── core.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | -------------------------------------------------------------------------------- /assets/src/width-1.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-1.afdesign -------------------------------------------------------------------------------- /assets/src/width-2.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-2.afdesign -------------------------------------------------------------------------------- /assets/src/width-3.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-3.afdesign -------------------------------------------------------------------------------- /assets/src/width-4.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-4.afdesign -------------------------------------------------------------------------------- /assets/src/icon-pen.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/icon-pen.afdesign -------------------------------------------------------------------------------- /assets/src/icon-eraser.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/icon-eraser.afdesign -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # douBoard 2 | 3 | > 一个支持压感的在线白板 4 | 5 | ## 快捷键 6 | 7 | - Ctrl+S 保存 8 | - Ctrl+Z 撤回 9 | - E 橡皮擦 10 | - B 画笔 11 | 12 | ## 在线demo 13 | 14 | [https://fastmirror-mc.github.io/douBoard/](https://fastmirror-mc.github.io/douBoard/ "demo") 15 | -------------------------------------------------------------------------------- /assets/icons/width-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 戴兜 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /assets/icons/icon-eraser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/icon-pen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /assets/icons/trashcan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | douBoard 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
橡皮
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |

清空画布

42 |
43 |
44 | 55 | 88 |
89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | padding: 0; 6 | margin: 0; 7 | } 8 | 9 | * { 10 | -webkit-user-select: none; 11 | user-select: none; 12 | } 13 | 14 | canvas { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | touch-action: none; 19 | } 20 | 21 | .eraser { 22 | border-radius: 100%; 23 | width: 60px; 24 | height: 60px; 25 | border: 2px #d3d3d3 solid; 26 | background: rgba(207, 207, 207, 0.2); 27 | opacity: 0.7; 28 | display: none; 29 | position: fixed; 30 | pointer-events: none; 31 | z-index: 10; 32 | box-sizing: border-box; 33 | } 34 | 35 | .toolbar { 36 | height: 50px; 37 | width: 100px; 38 | position: fixed; 39 | bottom: 0px; 40 | left: 50%; 41 | z-index: 12; 42 | transform: translateX(-50%); 43 | box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.2); 44 | user-select: none; 45 | background: #fff; 46 | } 47 | 48 | [class^="toolbar-"] { 49 | height: 50px; 50 | width: 50px; 51 | line-height: 50px; 52 | text-align: center; 53 | display: inline-block; 54 | float: left; 55 | cursor: pointer; 56 | transition: background-color 0.2s ease-out; 57 | } 58 | 59 | [class^="toolbar-"]:hover { 60 | background: #f1f1f2; 61 | } 62 | 63 | [class^="toolbar-"].active { 64 | background: #f1f1f2; 65 | } 66 | 67 | [class^="toolbar-"] svg { 68 | transform: translateY(12px); 69 | transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 70 | } 71 | [class^="toolbar-"].active svg { 72 | transform: translateY(3px); 73 | } 74 | 75 | .toolbar-pen { 76 | position: relative; 77 | } 78 | 79 | .toolbar-pen span { 80 | opacity: 0; 81 | transition: opacity 0.1s ease-out; 82 | } 83 | 84 | .toolbar-pen.active span { 85 | opacity: 1; 86 | position: absolute; 87 | background: #000; 88 | top: 3px; 89 | right: 3px; 90 | border-radius: 100%; 91 | } 92 | 93 | .toolbar-pen-viewer-1 { 94 | width: 3px; 95 | height: 3px; 96 | } 97 | .toolbar-pen-viewer-2 { 98 | width: 5px; 99 | height: 5px; 100 | } 101 | .toolbar-pen-viewer-3 { 102 | width: 7px; 103 | height: 7px; 104 | } 105 | .toolbar-pen-viewer-4 { 106 | width: 9px; 107 | height: 9px; 108 | } 109 | 110 | .toolbar-eraser { 111 | } 112 | 113 | .toolbarmenu { 114 | height: 50px; 115 | width: 100px; 116 | position: fixed; 117 | bottom: 50px; 118 | left: 50%; 119 | transform: translateX(-50%); 120 | user-select: none; 121 | z-index: 11; 122 | } 123 | 124 | [class^="toolbarmenu-"] { 125 | height: auto; 126 | width: 160px; 127 | background: #fff; 128 | box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.15); 129 | pointer-events: none; 130 | } 131 | 132 | [class^="toolbarmenu-"].active { 133 | pointer-events: initial; 134 | } 135 | 136 | .toolbarmenu-pen { 137 | opacity: 0; 138 | width: 260px; 139 | transition: opacity 0.2s linear; 140 | pointer-events: none; 141 | animation-fill-mode: forwards; 142 | animation: fadelogOut 0.4s; 143 | position: fixed; 144 | bottom: 5px; 145 | padding: 5px 5px 5px 0; 146 | } 147 | 148 | .width-viewer { 149 | height: 200px; 150 | width: 50px; 151 | float: left; 152 | } 153 | 154 | .switcher-container { 155 | height: 200px; 156 | float: left; 157 | display: flex; 158 | flex-direction: column; 159 | justify-content: space-around; 160 | } 161 | 162 | .switcher-container[type="width"] { 163 | width: 50px; 164 | } 165 | 166 | .switcher-container [class^="width-switcher-"] { 167 | height: 25px; 168 | width: 25px; 169 | border-radius: 100%; 170 | border: 2px solid rgba(179, 179, 179, 0.8); 171 | box-shadow: 0 0 0px 3px transparent; 172 | transition: box-shadow 0.1s ease-out; 173 | cursor: pointer; 174 | display: flex; 175 | justify-content: center; 176 | align-items: center; 177 | } 178 | 179 | .switcher-container [class^="width-switcher-"]:hover { 180 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8); 181 | } 182 | 183 | .switcher-container [class^="width-switcher-"].active { 184 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8); 185 | } 186 | 187 | .switcher-container [class^="width-switcher-"] span { 188 | background: #000; 189 | display: block; 190 | border-radius: 100%; 191 | } 192 | 193 | .switcher-container .width-switcher-4 span { 194 | height: 13px; 195 | width: 13px; 196 | } 197 | 198 | .switcher-container .width-switcher-3 span { 199 | height: 11px; 200 | width: 11px; 201 | } 202 | 203 | .switcher-container .width-switcher-2 span { 204 | height: 9px; 205 | width: 9px; 206 | } 207 | 208 | .switcher-container .width-switcher-1 span { 209 | height: 5px; 210 | width: 5px; 211 | } 212 | 213 | .switcher-container[type="color"] { 214 | width: 150px; 215 | flex-flow: column wrap; 216 | } 217 | 218 | .switcher-container .color-switcher { 219 | height: 25px; 220 | width: 25px; 221 | border-radius: 100%; 222 | border: 2px solid rgba(179, 179, 179, 0.8); 223 | box-shadow: 0 0 0px 3px transparent; 224 | transition: box-shadow 0.1s ease-out; 225 | cursor: pointer; 226 | flex-shrink: 0; 227 | background-color: white; 228 | margin: 10px 10px; 229 | } 230 | 231 | .switcher-container .color-switcher:hover { 232 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8); 233 | } 234 | 235 | .switcher-container .color-switcher.active { 236 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8); 237 | } 238 | 239 | .toolbarmenu-pen.active { 240 | pointer-events: all; 241 | opacity: 1; 242 | animation-fill-mode: forwards; 243 | animation: fadelogIn 0.3s; 244 | } 245 | 246 | .toolbarmenu-eraser { 247 | opacity: 0; 248 | transition: opacity 0.2s linear; 249 | pointer-events: none; 250 | animation-fill-mode: forwards; 251 | animation: fadelogOut 0.4s; 252 | margin-left: 50px; 253 | } 254 | 255 | .toolbarmenu-eraser.active { 256 | pointer-events: all; 257 | opacity: 1; 258 | animation-fill-mode: forwards; 259 | animation: fadelogIn 0.3s; 260 | } 261 | 262 | .toolbarmenu-eraser p { 263 | cursor: pointer; 264 | height: 45px; 265 | margin: 0; 266 | padding: 0; 267 | display: flex; 268 | align-items: center; 269 | background: #fff; 270 | transition: background-color 0.1s ease-out; 271 | } 272 | 273 | .toolbarmenu-eraser p:hover { 274 | background: #f1f1f2; 275 | } 276 | 277 | .toolbarmenu-eraser p img { 278 | float: left; 279 | margin-right: 7px; 280 | margin-left: 10px; 281 | transform: scale(0.8); 282 | } 283 | 284 | @keyframes fadelogIn { 285 | 0% { 286 | transform: translate3d(0, 80px, 0) scale(0.7); 287 | } 288 | 100% { 289 | transform: none; 290 | } 291 | } 292 | 293 | @keyframes fadelogOut { 294 | 0% { 295 | transform: none; 296 | } 297 | 100% { 298 | transform: translate3d(0, 80px, 0) scale(0.7); 299 | } 300 | } 301 | 302 | #toolbar-container { 303 | transition: opacity 0.2s ease-out; 304 | } 305 | 306 | .untouchable { 307 | pointer-events: none; 308 | opacity: 0.7; 309 | } 310 | 311 | .untouchable .active { 312 | pointer-events: none !important; 313 | } 314 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const icons = { 3 | "pen": function (color) { 4 | return ``; 5 | }, 6 | "eraser": function () { 7 | return ``; 8 | }, 9 | "width-1": function (color) { 10 | return ``; 11 | }, 12 | "width-2": function (color) { 13 | return ``; 14 | }, 15 | "width-3": function (color) { 16 | return ``; 17 | }, 18 | "width-4": function (color) { 19 | return ``; 20 | }, 21 | } 22 | let canvas = document.querySelector("#mainCanvas"); 23 | let ctx = canvas.getContext("2d"); 24 | 25 | let offCanvas = document.createElement("canvas"); 26 | let offCtx = offCanvas.getContext("2d"); 27 | 28 | let toolbarPen = document.querySelector(".toolbar-pen"); 29 | let toolbarEraser = document.querySelector(".toolbar-eraser"); 30 | let toolbarPenMenu = document.querySelector(".toolbarmenu-pen"); 31 | let toolbarEraserMenu = document.querySelector(".toolbarmenu-eraser"); 32 | 33 | let eraser = document.querySelector(".eraser"); 34 | 35 | for (i in document.images) document.images[i].ondragstart = function () { return false; }; 36 | 37 | window.onresize = function () { 38 | offCanvas.width = canvas.width; 39 | offCanvas.height = canvas.height; 40 | offCtx.drawImage(canvas, 0, 0); 41 | 42 | canvas.width = document.documentElement.clientWidth; 43 | canvas.height = document.documentElement.clientHeight; 44 | 45 | let width = canvas.width, height = canvas.height; 46 | if (window.devicePixelRatio) { 47 | canvas.style.width = width + "px"; 48 | canvas.style.height = height + "px"; 49 | canvas.height = height * window.devicePixelRatio; 50 | canvas.width = width * window.devicePixelRatio; 51 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 52 | } 53 | ctx.drawImage(offCanvas, 0, 0); 54 | ctx.lineJoin = "round"; 55 | ctx.lineCap = "round"; 56 | } 57 | 58 | window.onresize(); 59 | 60 | canDraw = false; 61 | let baseLineList = [6, 10, 15, 25]; 62 | let baseLineMode = 0; 63 | let lineColorList = ["#000", "#5B2D90", "#0069BF", "#F6630C", "#AB228B", "#B7B7B7", "#E3E3E3", "#E71224", "#D20078", "#02A556", "#C09E66", "#FFC114"]; //线条颜色列表 64 | let lineColorMode = 0; 65 | let history = []; 66 | let priviousDraw = 0; 67 | let priviousPressure = 0; 68 | 69 | for (let i = 0; i < 4; i++) { 70 | document.querySelector(`.width-switcher-${i + 1}`).onpointerup = function () { setPenWidth(i) } 71 | } 72 | 73 | for (let i = 0; i < 12; i++) { 74 | let child = document.createElement("div"); 75 | child.classList.add("color-switcher"); 76 | child.style.backgroundColor = lineColorList[i]; 77 | child.onpointerup = function () { 78 | setPenColor(i) 79 | } 80 | if (i == 0) { 81 | child.classList.add("active"); 82 | } 83 | document.querySelector(`.switcher-container[type="color"]`).appendChild(child) 84 | } 85 | 86 | function setPenWidth(mode) { 87 | baseLineMode = mode; 88 | let viewerContainer = document.querySelector(".width-viewer"); 89 | viewerContainer.innerHTML = icons[`width-${mode + 1}`](lineColorList[lineColorMode]); 90 | toolbarPen.innerHTML = icons.pen(lineColorList[lineColorMode]) + ``; 91 | document.querySelector(`.switcher-container[type="width"] .active`).classList.remove("active"); 92 | document.querySelector(`.width-switcher-${mode + 1}`).classList.add("active"); 93 | } 94 | 95 | setPenWidth(0); 96 | 97 | function setPenColor(mode) { 98 | lineColorMode = mode; 99 | toolbarPen.innerHTML = icons.pen(lineColorList[mode]) + ``; 100 | setPenWidth(baseLineMode); 101 | document.querySelector(`.color-switcher.active`).classList.remove("active"); 102 | document.querySelectorAll(`.color-switcher`)[mode].classList.add("active"); 103 | } 104 | 105 | setPenColor(0); 106 | 107 | 108 | let points = []; 109 | let beginPoint = null; 110 | 111 | const drawMode = { 112 | "down": function (e) { 113 | setToolbarStatus(false); 114 | writeHistory(); 115 | canDraw = true; 116 | ctx.globalCompositeOperation = "source-over"; 117 | ctx.strokeStyle = lineColorList[lineColorMode]; 118 | const { x, y, pressure } = getPos(e); 119 | priviousPressure = pressure; 120 | points.push({ x, y }); 121 | beginPoint = { x, y }; 122 | }, 123 | "up": function (e) { 124 | if (!canDraw) return; 125 | setToolbarStatus(true); 126 | const { x, y, pressure } = getPos(e); 127 | 128 | points.push({ x, y }); 129 | 130 | if (points.length > 3) { 131 | const lastTwoPoints = points.slice(-2); 132 | const controlPoint = lastTwoPoints[0]; 133 | const endPoint = lastTwoPoints[1]; 134 | usePen(beginPoint, controlPoint, endPoint, (priviousPressure + pressure) / 2 * baseLineList[baseLineMode]); 135 | } else { 136 | priviousPressure = pressure; 137 | } 138 | beginPoint = null; 139 | canDraw = false; 140 | points = []; 141 | }, 142 | "move": function (e) { 143 | if (!canDraw) return; 144 | const { x, y, pressure } = getPos(e); 145 | points.push({ x, y }); 146 | 147 | if (points.length > 3) { 148 | const lastTwoPoints = points.slice(-2); 149 | const controlPoint = lastTwoPoints[0]; 150 | const endPoint = { 151 | x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2, 152 | y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2, 153 | } 154 | usePen(beginPoint, controlPoint, endPoint, pressure * baseLineList[baseLineMode]); 155 | beginPoint = endPoint; 156 | } 157 | } 158 | } 159 | 160 | const eraserMode = { 161 | "down": function (e) { 162 | setToolbarStatus(false); 163 | writeHistory(); 164 | canDraw = true; 165 | ctx.strokeStyle = "rgba(0,0,0,1)"; 166 | ctx.globalCompositeOperation = "destination-out"; 167 | const { x, y } = getPos(e); 168 | eraser.style.top = `${y - 30}px`; 169 | eraser.style.left = `${x - 30}px`; 170 | eraser.style.display = "block"; 171 | points.push({ x, y }); 172 | beginPoint = { x, y }; 173 | }, 174 | "up": function (e) { 175 | if (!canDraw) return; 176 | setToolbarStatus(true); 177 | const { x, y } = getPos(e); 178 | 179 | points.push({ x, y }); 180 | 181 | if (points.length > 3) { 182 | const lastTwoPoints = points.slice(-2); 183 | const controlPoint = lastTwoPoints[0]; 184 | const endPoint = lastTwoPoints[1]; 185 | useEraser(beginPoint, controlPoint, endPoint, 60); 186 | } 187 | beginPoint = null; 188 | canDraw = false; 189 | eraser.style.display = "none"; 190 | points = []; 191 | }, 192 | "move": function (e) { 193 | if (!canDraw) return; 194 | const { x, y } = getPos(e); 195 | points.push({ x, y }); 196 | 197 | if (points.length > 3) { 198 | const lastTwoPoints = points.slice(-2); 199 | const controlPoint = lastTwoPoints[0]; 200 | const endPoint = { 201 | x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2, 202 | y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2, 203 | } 204 | eraser.style.top = `${y - 30}px`; 205 | eraser.style.left = `${x - 30}px`; 206 | useEraser(beginPoint, controlPoint, endPoint, 60); 207 | beginPoint = endPoint; 208 | } 209 | } 210 | } 211 | canvas.onpointerdown = drawMode["down"] 212 | canvas.onpointerup = drawMode["up"] 213 | canvas.onpointermove = drawMode["move"] 214 | 215 | function getPos(evt) { 216 | return { 217 | x: evt.clientX, 218 | y: evt.clientY, 219 | pressure: evt.pressure 220 | } 221 | } 222 | function usePen(beginPoint, controlPoint, endPoint, width) { 223 | ctx.beginPath(); 224 | ctx.moveTo(beginPoint.x, beginPoint.y); 225 | ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); 226 | ctx.lineWidth = width; 227 | ctx.stroke(); 228 | ctx.closePath(); 229 | } 230 | 231 | function useEraser(beginPoint, controlPoint, endPoint, width) { 232 | ctx.beginPath(); 233 | ctx.moveTo(beginPoint.x, beginPoint.y); 234 | ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); 235 | ctx.lineWidth = width; 236 | ctx.stroke(); 237 | } 238 | 239 | toolbarEraser.innerHTML = icons.eraser(); 240 | 241 | document.querySelector(".clearAll").onpointerup = function () { 242 | ctx.clearRect(0, 0, canvas.width, canvas.height); 243 | toolbarEraserMenu.classList.remove("active"); 244 | } 245 | toolbarPen.onpointerup = function () { 246 | toolbarEraserMenu.classList.remove("active"); 247 | if (toolbarPen.classList.contains("active")) { 248 | toolbarPenMenu.classList.toggle("active"); 249 | } 250 | toolbarPen.classList.add("active"); 251 | toolbarEraser.classList.remove("active"); 252 | canvas.onpointerdown = drawMode["down"]; 253 | canvas.onpointerup = drawMode["up"]; 254 | canvas.onpointermove = drawMode["move"]; 255 | } 256 | toolbarEraser.onpointerup = function () { 257 | toolbarPenMenu.classList.remove("active"); 258 | if (toolbarEraser.classList.contains("active")) { 259 | toolbarEraserMenu.classList.toggle("active"); 260 | } 261 | toolbarEraser.classList.add("active"); 262 | toolbarPen.classList.remove("active"); 263 | canvas.onpointerdown = eraserMode["down"]; 264 | canvas.onpointerup = eraserMode["up"]; 265 | canvas.onpointermove = eraserMode["move"]; 266 | } 267 | 268 | 269 | window.onkeyup = function (e) { 270 | if (e.ctrlKey == true && e.keyCode == 83) { //Ctrl+S 保存 271 | e.preventDefault(); 272 | e.returnvalue = false; 273 | saveCanvas(); 274 | } 275 | if (e.keyCode == 69) { //E 橡皮擦 276 | e.returnvalue = false; 277 | toolbarEraser.onpointerup(); 278 | } 279 | if (e.keyCode == 66) { //B 笔 280 | e.returnvalue = false; 281 | toolbarPen.onpointerup(); 282 | } 283 | if (e.ctrlKey == true && e.keyCode == 90) { //Ctrl+Z 撤销 284 | e.returnvalue = false; 285 | e.preventDefault(); 286 | let content = popHistory(); 287 | if (content) { 288 | ctx.putImageData(content, 0, 0); 289 | } 290 | } 291 | } 292 | 293 | function writeHistory() { 294 | if (history.length > 15) { 295 | history.shift() 296 | } 297 | if (priviousDraw == 0) { 298 | priviousDraw = new Date().getTime(); 299 | history.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); 300 | } else { 301 | if (new Date().getTime() - priviousDraw > 1000) { 302 | history.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); 303 | } 304 | } 305 | } 306 | 307 | function popHistory() { 308 | if (history.length == 0) { 309 | return false; 310 | } else { 311 | return history.pop(history); 312 | } 313 | } 314 | 315 | function saveCanvas() { 316 | var link = document.createElement("a"); 317 | var imgData = canvas.toDataURL(); 318 | var blob = dataURLtoBlob(imgData); 319 | var objURL = URL.createObjectURL(blob); 320 | link.download = `DouBoard(${new Date().toLocaleString().replace(/\//g, "-")}).png`; 321 | link.href = objURL; 322 | link.click(); 323 | link.remove(); 324 | 325 | setTimeout(function () { URL.revokeObjectURL(objURL); }, 5000); 326 | 327 | function dataURLtoBlob(dataurl) { 328 | var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], 329 | bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); 330 | while (n--) { 331 | u8arr[n] = bstr.charCodeAt(n); 332 | } 333 | return new Blob([u8arr], { type: mime }); 334 | } 335 | } 336 | 337 | function setToolbarStatus(status) { 338 | let toolbarContainer = document.querySelector("#toolbar-container"); 339 | if (!status) { 340 | toolbarContainer.classList.add("untouchable"); 341 | } else { 342 | toolbarContainer.classList.remove("untouchable"); 343 | } 344 | } 345 | })() --------------------------------------------------------------------------------