├── .gitignore ├── LICENSE ├── README.md ├── assets ├── icons │ ├── icon-eraser.svg │ ├── icon-pen.svg │ ├── trashcan.svg │ ├── width-1.svg │ ├── width-2.svg │ ├── width-3.svg │ └── width-4.svg └── src │ ├── icon-eraser.afdesign │ ├── icon-pen.afdesign │ ├── width-1.afdesign │ ├── width-2.afdesign │ ├── width-3.afdesign │ └── width-4.afdesign ├── core.js ├── index.html └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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://ink.daidr.me](https://ink.daidr.me "demo") -------------------------------------------------------------------------------- /assets/icons/icon-eraser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/icon-pen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 26 | -------------------------------------------------------------------------------- /assets/icons/trashcan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/width-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/src/icon-eraser.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/douBoard/136b5105267f5772daeafe59bd80ffc3836b3a8e/assets/src/icon-eraser.afdesign -------------------------------------------------------------------------------- /assets/src/icon-pen.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/douBoard/136b5105267f5772daeafe59bd80ffc3836b3a8e/assets/src/icon-pen.afdesign -------------------------------------------------------------------------------- /assets/src/width-1.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/douBoard/136b5105267f5772daeafe59bd80ffc3836b3a8e/assets/src/width-1.afdesign -------------------------------------------------------------------------------- /assets/src/width-2.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/douBoard/136b5105267f5772daeafe59bd80ffc3836b3a8e/assets/src/width-2.afdesign -------------------------------------------------------------------------------- /assets/src/width-3.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/douBoard/136b5105267f5772daeafe59bd80ffc3836b3a8e/assets/src/width-3.afdesign -------------------------------------------------------------------------------- /assets/src/width-4.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/douBoard/136b5105267f5772daeafe59bd80ffc3836b3a8e/assets/src/width-4.afdesign -------------------------------------------------------------------------------- /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 toolbarPenOnly = document.querySelector(".toolbar-penonly"); 31 | let toolbarPenMenu = document.querySelector(".toolbarmenu-pen"); 32 | let toolbarEraserMenu = document.querySelector(".toolbarmenu-eraser"); 33 | 34 | let eraser = document.querySelector(".eraser"); 35 | let isPenOnly = false; 36 | for (i in document.images) document.images[i].ondragstart = function () { return false; }; 37 | 38 | window.onresize = function () { 39 | offCanvas.width = canvas.width; 40 | offCanvas.height = canvas.height; 41 | offCtx.drawImage(canvas, 0, 0); 42 | 43 | canvas.width = document.documentElement.clientWidth; 44 | canvas.height = document.documentElement.clientHeight; 45 | 46 | let width = canvas.width, height = canvas.height; 47 | if (window.devicePixelRatio) { 48 | canvas.style.width = width + "px"; 49 | canvas.style.height = height + "px"; 50 | canvas.height = height * window.devicePixelRatio; 51 | canvas.width = width * window.devicePixelRatio; 52 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 53 | } 54 | ctx.drawImage(offCanvas, 0, 0); 55 | ctx.lineJoin = "round"; 56 | ctx.lineCap = "round"; 57 | } 58 | 59 | window.onresize(); 60 | 61 | canDraw = false; 62 | let baseLineList = [6, 10, 15, 25]; 63 | let baseLineMode = 0; 64 | let lineColorList = ["#000", "#5B2D90", "#0069BF", "#F6630C", "#AB228B", "#B7B7B7", "#E3E3E3", "#E71224", "#D20078", "#02A556", "#C09E66", "#FFC114"]; //线条颜色列表 65 | let lineColorMode = 0; 66 | let history = []; 67 | let priviousDraw = 0; 68 | let priviousPressure = 0; 69 | 70 | for (let i = 0; i < 4; i++) { 71 | document.querySelector(`.width-switcher-${i + 1}`).onpointerup = function () { setPenWidth(i) } 72 | } 73 | 74 | for (let i = 0; i < 12; i++) { 75 | let child = document.createElement("div"); 76 | child.classList.add("color-switcher"); 77 | child.style.backgroundColor = lineColorList[i]; 78 | child.onpointerup = function () { 79 | setPenColor(i) 80 | } 81 | if (i == 0) { 82 | child.classList.add("active"); 83 | } 84 | document.querySelector(`.switcher-container[type="color"]`).appendChild(child) 85 | } 86 | 87 | function setPenWidth(mode) { 88 | baseLineMode = mode; 89 | let viewerContainer = document.querySelector(".width-viewer"); 90 | viewerContainer.innerHTML = icons[`width-${mode + 1}`](lineColorList[lineColorMode]); 91 | toolbarPen.innerHTML = icons.pen(lineColorList[lineColorMode]) + ``; 92 | document.querySelector(`.switcher-container[type="width"] .active`).classList.remove("active"); 93 | document.querySelector(`.width-switcher-${mode + 1}`).classList.add("active"); 94 | } 95 | 96 | setPenWidth(0); 97 | 98 | function setPenColor(mode) { 99 | lineColorMode = mode; 100 | toolbarPen.innerHTML = icons.pen(lineColorList[mode]) + ``; 101 | setPenWidth(baseLineMode); 102 | document.querySelector(`.color-switcher.active`).classList.remove("active"); 103 | document.querySelectorAll(`.color-switcher`)[mode].classList.add("active"); 104 | } 105 | 106 | setPenColor(0); 107 | 108 | 109 | let points = []; 110 | let beginPoint = null; 111 | 112 | const drawMode = { 113 | "down": function (e) { 114 | if (isPenOnly && e.pointerType != "pen") return; 115 | setToolbarStatus(false); 116 | // writeHistory(); 117 | canDraw = true; 118 | ctx.globalCompositeOperation = "source-over"; 119 | ctx.strokeStyle = lineColorList[lineColorMode]; 120 | const { x, y, pressure } = getPos(e); 121 | priviousPressure = pressure; 122 | points.push({ x, y }); 123 | beginPoint = { x, y }; 124 | }, 125 | "up": function (e) { 126 | if (!canDraw) return; 127 | if (isPenOnly && e.pointerType != "pen") return; 128 | setToolbarStatus(true); 129 | const { x, y, pressure } = getPos(e); 130 | 131 | points.push({ x, y }); 132 | 133 | if (points.length > 3) { 134 | const lastTwoPoints = points.slice(-2); 135 | const controlPoint = lastTwoPoints[0]; 136 | const endPoint = lastTwoPoints[1]; 137 | usePen(beginPoint, controlPoint, endPoint, (priviousPressure + pressure) / 2 * baseLineList[baseLineMode]); 138 | } else { 139 | priviousPressure = pressure; 140 | } 141 | beginPoint = null; 142 | canDraw = false; 143 | points = []; 144 | }, 145 | "move": function (e) { 146 | if (isPenOnly && e.pointerType != "pen") return; 147 | if (!canDraw) return; 148 | const { x, y, pressure } = getPos(e); 149 | points.push({ x, y }); 150 | 151 | if (points.length > 3) { 152 | const lastTwoPoints = points.slice(-2); 153 | const controlPoint = lastTwoPoints[0]; 154 | const endPoint = { 155 | x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2, 156 | y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2, 157 | } 158 | usePen(beginPoint, controlPoint, endPoint, pressure * baseLineList[baseLineMode]); 159 | beginPoint = endPoint; 160 | } 161 | } 162 | } 163 | 164 | const eraserMode = { 165 | "down": function (e) { 166 | setToolbarStatus(false); 167 | // writeHistory(); 168 | canDraw = true; 169 | ctx.strokeStyle = "rgba(0,0,0,1)"; 170 | ctx.globalCompositeOperation = "destination-out"; 171 | const { x, y } = getPos(e); 172 | eraser.style.top = `${y - 30}px`; 173 | eraser.style.left = `${x - 30}px`; 174 | eraser.style.display = "block"; 175 | points.push({ x, y }); 176 | beginPoint = { x, y }; 177 | }, 178 | "up": function (e) { 179 | if (!canDraw) return; 180 | setToolbarStatus(true); 181 | const { x, y } = getPos(e); 182 | 183 | points.push({ x, y }); 184 | 185 | if (points.length > 3) { 186 | const lastTwoPoints = points.slice(-2); 187 | const controlPoint = lastTwoPoints[0]; 188 | const endPoint = lastTwoPoints[1]; 189 | useEraser(beginPoint, controlPoint, endPoint, 60); 190 | } 191 | beginPoint = null; 192 | canDraw = false; 193 | eraser.style.display = "none"; 194 | points = []; 195 | }, 196 | "move": function (e) { 197 | if (!canDraw) return; 198 | const { x, y } = getPos(e); 199 | points.push({ x, y }); 200 | 201 | if (points.length > 3) { 202 | const lastTwoPoints = points.slice(-2); 203 | const controlPoint = lastTwoPoints[0]; 204 | const endPoint = { 205 | x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2, 206 | y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2, 207 | } 208 | eraser.style.top = `${y - 30}px`; 209 | eraser.style.left = `${x - 30}px`; 210 | useEraser(beginPoint, controlPoint, endPoint, 60); 211 | beginPoint = endPoint; 212 | } 213 | } 214 | } 215 | 216 | canvas.addEventListener("pointerdown", drawMode["down"], { passive: true }) 217 | canvas.addEventListener("pointerup", drawMode["up"], { passive: true }) 218 | canvas.addEventListener("pointermove", drawMode["move"], { passive: true }) 219 | 220 | function getPos(evt) { 221 | return { 222 | x: evt.clientX, 223 | y: evt.clientY, 224 | pressure: evt.pressure 225 | } 226 | } 227 | function usePen(beginPoint, controlPoint, endPoint, width) { 228 | ctx.beginPath(); 229 | ctx.moveTo(beginPoint.x, beginPoint.y); 230 | ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); 231 | ctx.lineWidth = width; 232 | ctx.stroke(); 233 | ctx.closePath(); 234 | } 235 | 236 | function useEraser(beginPoint, controlPoint, endPoint, width) { 237 | ctx.beginPath(); 238 | ctx.moveTo(beginPoint.x, beginPoint.y); 239 | ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); 240 | ctx.lineWidth = width; 241 | ctx.stroke(); 242 | } 243 | 244 | toolbarEraser.innerHTML = icons.eraser(); 245 | 246 | document.querySelector(".clearAll").onpointerup = function () { 247 | ctx.clearRect(0, 0, canvas.width, canvas.height); 248 | toolbarEraserMenu.classList.remove("active"); 249 | } 250 | toolbarPen.onpointerup = function () { 251 | toolbarEraserMenu.classList.remove("active"); 252 | if (toolbarPen.classList.contains("active")) { 253 | toolbarPenMenu.classList.toggle("active"); 254 | } 255 | toolbarPen.classList.add("active"); 256 | toolbarEraser.classList.remove("active"); 257 | canvas.removeEventListener("pointerdown", eraserMode["down"], { passive: true }) 258 | canvas.removeEventListener("pointerup", eraserMode["up"], { passive: true }) 259 | canvas.removeEventListener("pointermove", eraserMode["move"], { passive: true }) 260 | canvas.removeEventListener("pointerdown", drawMode["down"], { passive: true }) 261 | canvas.removeEventListener("pointerup", drawMode["up"], { passive: true }) 262 | canvas.removeEventListener("pointermove", drawMode["move"], { passive: true }) 263 | canvas.addEventListener("pointerdown", drawMode["down"], { passive: true }) 264 | canvas.addEventListener("pointerup", drawMode["up"], { passive: true }) 265 | canvas.addEventListener("pointermove", drawMode["move"], { passive: true }) 266 | } 267 | toolbarEraser.onpointerup = function () { 268 | toolbarPenMenu.classList.remove("active"); 269 | if (toolbarEraser.classList.contains("active")) { 270 | toolbarEraserMenu.classList.toggle("active"); 271 | } 272 | toolbarEraser.classList.add("active"); 273 | toolbarPen.classList.remove("active"); 274 | canvas.removeEventListener("pointerdown", eraserMode["down"], { passive: true }) 275 | canvas.removeEventListener("pointerup", eraserMode["up"], { passive: true }) 276 | canvas.removeEventListener("pointermove", eraserMode["move"], { passive: true }) 277 | canvas.removeEventListener("pointerdown", drawMode["down"], { passive: true }) 278 | canvas.removeEventListener("pointerup", drawMode["up"], { passive: true }) 279 | canvas.removeEventListener("pointermove", drawMode["move"], { passive: true }) 280 | canvas.addEventListener("pointerdown", eraserMode["down"], { passive: true }) 281 | canvas.addEventListener("pointerup", eraserMode["up"], { passive: true }) 282 | canvas.addEventListener("pointermove", eraserMode["move"], { passive: true }) 283 | } 284 | 285 | toolbarPenOnly.onpointerup = function () { 286 | isPenOnly = !isPenOnly; 287 | if (isPenOnly) { 288 | toolbarPenOnly.classList.add("enabled"); 289 | } else { 290 | toolbarPenOnly.classList.remove("enabled"); 291 | } 292 | } 293 | 294 | 295 | window.onkeyup = function (e) { 296 | if (e.ctrlKey == true && e.keyCode == 83) { //Ctrl+S 保存 297 | e.preventDefault(); 298 | e.returnvalue = false; 299 | saveCanvas(); 300 | } 301 | if (e.keyCode == 69) { //E 橡皮擦 302 | e.returnvalue = false; 303 | toolbarEraser.onpointerup(); 304 | } 305 | if (e.keyCode == 66) { //B 笔 306 | e.returnvalue = false; 307 | toolbarPen.onpointerup(); 308 | } 309 | if (e.ctrlKey == true && e.keyCode == 90) { //Ctrl+Z 撤销 310 | e.returnvalue = false; 311 | e.preventDefault(); 312 | let content = popHistory(); 313 | if (content) { 314 | ctx.putImageData(content, 0, 0); 315 | } 316 | } 317 | } 318 | 319 | function writeHistory() { 320 | if (history.length > 15) { 321 | history.shift() 322 | } 323 | if (priviousDraw == 0) { 324 | priviousDraw = new Date().getTime(); 325 | history.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); 326 | } else { 327 | if (new Date().getTime() - priviousDraw > 1000) { 328 | history.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); 329 | } 330 | } 331 | } 332 | 333 | function popHistory() { 334 | if (history.length == 0) { 335 | return false; 336 | } else { 337 | return history.pop(history); 338 | } 339 | } 340 | 341 | function saveCanvas() { 342 | var link = document.createElement("a"); 343 | var imgData = canvas.toDataURL(); 344 | var blob = dataURLtoBlob(imgData); 345 | var objURL = URL.createObjectURL(blob); 346 | link.download = `DouBoard(${new Date().toLocaleString().replace(/\//g, "-")}).png`; 347 | link.href = objURL; 348 | link.click(); 349 | link.remove(); 350 | 351 | setTimeout(function () { URL.revokeObjectURL(objURL); }, 5000); 352 | 353 | function dataURLtoBlob(dataurl) { 354 | var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], 355 | bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); 356 | while (n--) { 357 | u8arr[n] = bstr.charCodeAt(n); 358 | } 359 | return new Blob([u8arr], { type: mime }); 360 | } 361 | } 362 | 363 | function setToolbarStatus(status) { 364 | let toolbarContainer = document.querySelector("#toolbar-container"); 365 | if (!status) { 366 | toolbarContainer.classList.add("untouchable"); 367 | } else { 368 | toolbarContainer.classList.remove("untouchable"); 369 | } 370 | } 371 | })() 372 | 373 | 374 | document.addEventListener("touchstart", function (e) { 375 | e.preventDefault(); 376 | }, { passive: false }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |