├── README.md ├── css └── style.css ├── image ├── PC端.png ├── clear.png ├── close.png ├── eraser.png ├── favicon.png ├── ic_droplet1.png ├── line.png ├── line_sel.png ├── pen.png ├── pen_sel.png ├── redo.png ├── redo_sel.png ├── save.png ├── save_sel.png ├── undo.png ├── undo_sel.png └── 移动端.png ├── index.html └── js └── main.js /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 本文主要介绍: 4 | 5 | 1. 项目介绍 6 | 2. 项目效果展示 7 | 3. 一步步实现项目效果 8 | 4. 踩坑 9 | 10 | ## 一、项目介绍 11 | 12 | **名称:** 智绘画板 13 | 14 | **技术栈:** HTML5,CSS3,JavaScript,移动端 15 | 16 | **功能描述:** 17 | 18 | - 支持PC端和移动端在线绘画功能 19 | - 实现任意选择画笔颜色、调整画笔粗细以及橡皮檫擦除等绘画功能 20 | - 实现在线画板的本地保存功能 21 | - 支持撤销和返回操作 22 | - ~~自定义背景颜色~~[这个功能尚未完善好] 23 | 24 | ## 二、项目效果展示 25 | 26 | [项目地址](https://github.com/xyyojl/drawingborad) [预览地址](https://xyyojl.github.io/drawingborad/index.html) 27 | 28 | ### 预览图 29 | 30 | PC端的预览图: 31 | 32 | 33 | ![](https://user-gold-cdn.xitu.io/2019/3/3/16944271120d4aca?w=1366&h=664&f=png&s=13244) 34 | 35 | 移动端的预览图: 36 | 37 | 38 | ![](https://user-gold-cdn.xitu.io/2019/3/3/169442736b704a5d?w=248&h=443&f=png&s=6192) 39 | 40 | 41 | 42 | 看完上面的预览图和体验过**智绘画板**觉得还可以的,记得点个赞哦,不管你是否十分激动,反正我是挺激动的,毕竟自己实现出现的项目效果,挺自豪的,说了一堆废话,下面就可以动起手来敲代码,实现自己想要的效果!!! 43 | 44 | 注:下面实现项目效果主要是关于JavaScript方面的,下面仅仅是提供**实现思路的代码**,**并非全部代码**。 45 | 46 | ## 三、一步步实现项目效果 47 | 48 | ### (一)分析页面 49 | 50 | 通过**用例图**,我们知道用户进入我们这个网站有哪些功能? 51 | 52 | 用户可以进行的操作: 53 | 54 | - 画画 55 | - 改变画笔的粗细 56 | - 切换画笔的颜色 57 | - 使用橡皮檫擦除不想要的部分 58 | - 清空画板 59 | - 将自己画的东西保存成图片 60 | - 进行撤销和重做操作 61 | - ~~切换画板背景颜色~~ 62 | - 兼容移动端(支持触摸) 63 | 64 | ### (二)进行HTML布局 65 | 66 | 我书写html的同时,引入了css文件和js文件 67 | 68 | ```html 69 | 70 | 71 | 72 | 73 | 74 | 75 | 智绘画板 76 | 77 | 78 | 79 | 80 | 81 | 82 | 101 |
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 |
111 |
112 | 113 |

笔大小

114 | 115 |

笔颜色

116 | 124 |

不透明度

125 | 126 |
127 | 128 | 129 | 130 | ``` 131 | 132 | ### (三)用CSS美化界面 133 | 134 | css代码可以根据个人习惯进行美化界面,所以这里就不写css的代码了,大家可以直接看**项目代码**或者从开发者工具中审查元素观看。如果有问题可以私聊我,我觉得问题不大。 135 | 136 | ### (四)使用JS实现项目的具体功能 137 | 138 | #### 1.准备工作 139 | 140 | 首先,准备个容器,也就是画板了,前面的html已经书写好这个容器,这里纯属是废话。 141 | 142 | ```html 143 | 144 | ``` 145 | 146 | 然后初始化js 147 | 148 | ```js 149 | let canvas = document.getElementById('canvas'); 150 | let context = canvas.getContext('2d'); 151 | ``` 152 | 153 | 我打算把画板做成全屏的,所以接下来设置一下canvas的宽高 154 | 155 | ```js 156 | let pageWidth = document.documentElement.clientWidth; 157 | let pageHeight = document.documentElement.clientHeight; 158 | 159 | canvas.width = pageWidth; 160 | canvas.height = pageHeight; 161 | ``` 162 | 163 | 由于部分IE不支持canvas,如果要兼容IE,我们可以创建一个canvas,然后使用`excanvas`初始化,针对IE加上[exCanvas.js](http://code.google.com/p/explorercanvas/),这里我们明确不考虑IE。 164 | 165 | 但是我在电脑上对浏览器的窗口进行改变,画板不会自适应的放缩。解决办法: 166 | 167 | ```js 168 | // 记得要执行autoSetSize这个函数哦 169 | function autoSetSize(){ 170 | canvasSetSize(); 171 | // 当执行这个函数的时候,会先设置canvas的宽高 172 | function canvasSetSize(){ 173 | // 把变化之前的画布内容copy一份,然后重新画到画布上 174 | let imgData = context.getImageData(0,0,canvas.width,canvas.height); 175 | let pageWidth = document.documentElement.clientWidth; 176 | let pageHeight = document.documentElement.clientHeight; 177 | 178 | canvas.width = pageWidth; 179 | canvas.height = pageHeight; 180 | context.putImageData(imgData,0,0); 181 | } 182 | // 在窗口大小改变之后,就会触发resize事件,重新设置canvas的宽高 183 | window.onresize = function(){ 184 | canvasSetSize(); 185 | } 186 | } 187 | ``` 188 | 189 | #### 2.实现画画的功能 190 | 191 | 实现思路:监听鼠标事件, 用`drawLine()`方法把记录的数据画出来。 192 | 193 | 1. 初始化当前画板的画笔状态,`painting = false`。 194 | 2. 当鼠标按下时(`mousedown`),把`painting`设为`true`,表示正在画,鼠标没松开。把鼠标点记录下来。 195 | 3. 当按下鼠标的时候,鼠标移动(`mousemove`)就**把点记录**下来并画出来。 196 | 4. 如果鼠标移动过快,浏览器跟不上绘画速度,点与点之间会出现间隙,所以我们需要将画出的点用线连起来(`lineTo()`)。 197 | 5. 鼠标松开的时候(`mouseup`),把`painting`设为`false`。 198 | 199 | 注:`drawCircle`这个方法其实可以不用书写,这个只是为了让大家能够理解开始点击的位置在哪里? 200 | 201 | ```js 202 | function listenToUser() { 203 | // 定义一个变量初始化画笔状态 204 | let painting = false; 205 | // 记录画笔最后一次的位置 206 | let lastPoint = {x: undefined, y: undefined}; 207 | 208 | // 鼠标按下事件 209 | canvas.onmousedown = function(e){ 210 | painting = true; 211 | let x = e.clientX; 212 | let y = e.clientY; 213 | lastPoint = {'x':x,'y':y}; 214 | drawCircle(x,y,5); 215 | } 216 | 217 | // 鼠标移动事件 218 | canvas.onmousemove = function(e){ 219 | if(painting){ 220 | let x = e.clientX; 221 | let y = e.clientY; 222 | let newPoint = {'x':x,'y':y}; 223 | drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y); 224 | lastPoint = newPoint; 225 | } 226 | } 227 | 228 | // 鼠标松开事件 229 | canvas.onmouseup = function(){ 230 | painting = false; 231 | } 232 | } 233 | 234 | // 画点函数 235 | function drawCircle(x,y,radius){ 236 | // 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。 237 | context.beginPath(); 238 | // 画一个以(x,y)为圆心的以radius为半径的圆弧(圆), 239 | // 从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。 240 | context.arc(x,y,radius,0,Math.PI*2); 241 | // 通过填充路径的内容区域生成实心的图形 242 | context.fill(); 243 | // 闭合路径之后图形绘制命令又重新指向到上下文中。 244 | context.closePath(); 245 | } 246 | 247 | function drawLine(x1,y1,x2,y2){ 248 | // 设置线条宽度 249 | context.lineWidth = 10; 250 | // 设置线条末端样式。 251 | context.lineCap = "round"; 252 | // 设定线条与线条间接合处的样式 253 | context.lineJoin = "round"; 254 | // moveTo(x,y)将笔触移动到指定的坐标x以及y上 255 | context.moveTo(x1,y1); 256 | // lineTo(x, y) 绘制一条从当前位置到指定x以及y位置的直线 257 | context.lineTo(x2,y2); 258 | // 通过线条来绘制图形轮廓 259 | context.stroke(); 260 | context.closePath(); 261 | } 262 | ``` 263 | 264 | #### 3.实现橡皮擦功能 265 | 266 | 实现思路: 267 | 268 | 1. 获取橡皮擦元素 269 | 2. 设置橡皮擦初始状态,`eraserEnabled = false`。 270 | 3. 监听橡皮擦`click`事件,点击橡皮擦,改变橡皮擦状态,`eraserEnabled = true`,并且切换class,实现**被激活**的效果。 271 | 4. `eraserEnabled`为`true`,移动鼠标用`context.clearRect()`实现了橡皮檫。 272 | 273 | 但是我发现canvas的API中,可以清除像素的就是`clearRect`方法,但是`clearRect`方法的清除区域矩形,毕竟大部分人的习惯中的橡皮擦都是圆形的,所以就引入了剪辑区域这个强大的功能,也就是`clip`方法。下面的代码是使用`context.clearRect()`实现了 橡皮檫。请看踩坑部分,了解如何更好的实现橡皮檫。 274 | 275 | ```js 276 | let eraser = document.getElementById("eraser"); 277 | let eraserEnabled = false; 278 | 279 | // 记得要执行listenToUser这个函数哦 280 | function listenToUser() { 281 | // ... 代表省略了之前写的代码 282 | // ... 283 | 284 | // 鼠标按下事件 285 | canvas.onmousedown = function(e){ 286 | // ... 287 | if(eraserEnabled){//要使用eraser 288 | context.clearRect(x-5,y-5,10,10) 289 | }else{ 290 | lastPoint = {'x':x,'y':y} 291 | } 292 | } 293 | 294 | // 鼠标移动事件 295 | canvas.onmousemove = function(e){ 296 | let x = e.clientX; 297 | let y = e.clientY; 298 | if(!painting){return} 299 | if(eraserEnabled){ 300 | context.clearRect(x-5,y-5,10,10); 301 | }else{ 302 | var newPoint = {'x':x,'y':y}; 303 | drawLine(lastPoint.x, lastPoint.y,newPoint.x, newPoint.y); 304 | lastPoint = newPoint; 305 | } 306 | } 307 | 308 | // ... 309 | } 310 | 311 | 312 | // 点击橡皮檫 313 | eraser.onclick = function(){ 314 | eraserEnabled = true; 315 | eraser.classList.add('active'); 316 | brush.classList.remove('active'); 317 | } 318 | ``` 319 | 320 | ### 4.实现清屏功能 321 | 322 | 实现思路: 323 | 324 | 1. 获取元素节点。 325 | 2. 点击清空按钮清空canvas画布。 326 | 327 | ```js 328 | let reSetCanvas = document.getElementById("clear"); 329 | 330 | // 实现清屏 331 | reSetCanvas.onclick = function(){ 332 | ctx.clearRect(0,0,canvas.width,canvas.height); 333 | setCanvasBg('white'); 334 | } 335 | 336 | // 重新设置canvas背景颜色 337 | function setCanvasBg(color) { 338 | ctx.fillStyle = color; 339 | ctx.fillRect(0, 0, canvas.width, canvas.height); 340 | } 341 | ``` 342 | 343 | #### 5.实现保存成图片功能 344 | 345 | 实现思路: 346 | 347 | 1. 获取`canvas.toDateURL` 348 | 2. 在页面里创建并插入一个a标签 349 | 3. a标签href等于`canvas.toDateURL`,并添加download属性 350 | 4. 点击保存按钮,a标签触发`click`事件 351 | 352 | ```js 353 | let save = document.getElementById("save"); 354 | 355 | // 下载图片 356 | save.onclick = function(){ 357 | let imgUrl = canvas.toDataURL('image/png'); 358 | let saveA = document.createElement('a'); 359 | document.body.appendChild(saveA); 360 | saveA.href = imgUrl; 361 | saveA.download = 'mypic'+(new Date).getTime(); 362 | saveA.target = '_blank'; 363 | saveA.click(); 364 | } 365 | ``` 366 | 367 | #### ~~6.实现改变背景颜色的功能~~ 368 | 369 | ~~实现思路:~~ 370 | 371 | 1. ~~获取相应的元素节点。~~ 372 | 2. ~~给每一个class为bgcolor-item的标签添加点击事件,当点击事件触发时,改变背景颜色。~~ 373 | 3. ~~点击设置背景颜色的div之外的地方,实现隐藏那个div。~~ 374 | 375 | ```js 376 | let selectBg = document.querySelector('.bg-btn'); 377 | let bgGroup = document.querySelector('.color-group'); 378 | let bgcolorBtn = document.querySelectorAll('.bgcolor-item'); 379 | let penDetail = document.getElementById("penDetail"); 380 | let activeBgColor = '#fff'; 381 | 382 | 383 | // 实现了切换背景颜色 384 | for (let i = 0; i < bgcolorBtn.length; i++) { 385 | bgcolorBtn[i].onclick = function (e) { 386 | // 阻止冒泡 387 | e.stopPropagation(); 388 | for (let i = 0; i < bgcolorBtn.length; i++) { 389 | bgcolorBtn[i].classList.remove("active"); 390 | this.classList.add("active"); 391 | activeBgColor = this.style.backgroundColor; 392 | setCanvasBg(activeBgColor); 393 | } 394 | } 395 | } 396 | 397 | document.onclick = function(){ 398 | bgGroup.classList.remove('active'); 399 | } 400 | 401 | selectBg.onclick = function(e){ 402 | bgGroup.classList.add('active'); 403 | e.stopPropagation(); 404 | } 405 | ``` 406 | 407 | #### 7.实现改变画笔粗细的功能 408 | 409 | 实现思路: 410 | 411 | 1. 实现让设置画笔的属性的对话框出现。 412 | 2. 获取相应的元素节点。 413 | 3. 当input=range的元素发生改变的时候,获取到的值赋值给lWidth。 414 | 4. 然后设置`context.lineWidth = lWidth`。 415 | 416 | ```js 417 | let range1 = document.getElementById('range1'); 418 | let lWidth = 2; 419 | let ifPop = false; 420 | 421 | range1.onchange = function(){ 422 | console.log(range1.value); 423 | console.log(typeof range1.value) 424 | thickness.style.transform = 'scale('+ (parseInt(range1.value)) +')'; 425 | console.log(thickness.style.transform ) 426 | lWidth = parseInt(range1.value*2); 427 | } 428 | 429 | 430 | // 画线函数 431 | function drawLine(x1,y1,x2,y2){ 432 | // ... 433 | context.lineWidth = lWidth; 434 | // ... 435 | } 436 | 437 | // 点击画笔 438 | brush.onclick = function(){ 439 | eraserEnabled = false; 440 | brush.classList.add('active'); 441 | eraser.classList.remove('active'); 442 | if(!ifPop){ 443 | // 弹出框 444 | console.log('弹一弹') 445 | penDetail.classList.add('active'); 446 | }else{ 447 | penDetail.classList.remove('active'); 448 | } 449 | ifPop = !ifPop; 450 | } 451 | ``` 452 | 453 | #### 8.实现改变画笔颜色的功能 454 | 455 | 实现思路跟**改变画板背景颜色**的思路类似。 456 | 457 | ```js 458 | let aColorBtn = document.getElementsByClassName("color-item"); 459 | 460 | getColor(); 461 | 462 | function getColor(){ 463 | for (let i = 0; i < aColorBtn.length; i++) { 464 | aColorBtn[i].onclick = function () { 465 | for (let i = 0; i < aColorBtn.length; i++) { 466 | aColorBtn[i].classList.remove("active"); 467 | this.classList.add("active"); 468 | activeColor = this.style.backgroundColor; 469 | ctx.fillStyle = activeColor; 470 | ctx.strokeStyle = activeColor; 471 | } 472 | } 473 | } 474 | } 475 | ``` 476 | 477 | #### 9.实现改变撤销和重做的功能 478 | 479 | 实现思路: 480 | 481 | 1. 保存快照:每完成一次绘制操作则保存一份 canvas 快照到 `canvasHistory` 数组(生成快照使用 canvas 的 `toDataURL()` 方法,生成的是 base64 的图片); 482 | 2. 撤销和反撤销:把 `canvasHistory` 数组中对应索引的快照使用 canvas 的 `drawImage()` 方法重绘一遍; 483 | 3. 绘制新图像:执行新的绘制操作时,删除当前位置之后的数组记录,然后添加新的快照。 484 | 485 | ```js 486 | let undo = document.getElementById("undo"); 487 | let redo = document.getElementById("redo"); 488 | 489 | // ... 490 | canvas.ontouchend = function () { 491 | painting = false; 492 | canvasDraw(); 493 | } 494 | 495 | // ... 496 | canvas.onmouseup = function(){ 497 | painting = false; 498 | canvasDraw(); 499 | } 500 | 501 | let canvasHistory = []; 502 | let step = -1; 503 | 504 | // 绘制方法 505 | function canvasDraw(){ 506 | step++; 507 | if(step < canvasHistory.length){ 508 | canvasHistory.length = step; // 截断数组 509 | } 510 | // 添加新的绘制到历史记录 511 | canvasHistory.push(canvas.toDataURL()); 512 | } 513 | 514 | // 撤销方法 515 | function canvasUndo(){ 516 | if(step > 0){ 517 | step--; 518 | // ctx.clearRect(0,0,canvas.width,canvas.height); 519 | let canvasPic = new Image(); 520 | canvasPic.src = canvasHistory[step]; 521 | canvasPic.onload = function () { ctx.drawImage(canvasPic, 0, 0); } 522 | undo.classList.add('active'); 523 | }else{ 524 | undo.classList.remove('active'); 525 | alert('不能再继续撤销了'); 526 | } 527 | } 528 | // 重做方法 529 | function canvasRedo(){ 530 | if(step < canvasHistory.length - 1){ 531 | step++; 532 | let canvasPic = new Image(); 533 | canvasPic.src = canvasHistory[step]; 534 | canvasPic.onload = function () { 535 | // ctx.clearRect(0,0,canvas.width,canvas.height); 536 | ctx.drawImage(canvasPic, 0, 0); 537 | } 538 | redo.classList.add('active'); 539 | }else { 540 | redo.classList.remove('active') 541 | alert('已经是最新的记录了'); 542 | } 543 | } 544 | undo.onclick = function(){ 545 | canvasUndo(); 546 | } 547 | redo.onclick = function(){ 548 | canvasRedo(); 549 | } 550 | ``` 551 | 552 | #### 10.兼容移动端 553 | 554 | 实现思路: 555 | 556 | 1. 判断设备是否支持触摸 557 | 2. `true`,则使用`touch`事件;`false`,则使用`mouse`事件 558 | 559 | ```js 560 | // ... 561 | if (document.body.ontouchstart !== undefined) { 562 | // 使用touch事件 563 | anvas.ontouchstart = function (e) { 564 | // 开始触摸 565 | } 566 | canvas.ontouchmove = function (e) { 567 | // 开始滑动 568 | } 569 | canvas.ontouchend = function () { 570 | // 滑动结束 571 | } 572 | }else{ 573 | // 使用mouse事件 574 | // ... 575 | } 576 | // ... 577 | ``` 578 | 579 | ## 四、踩坑 580 | 581 | ### 问题1:在电脑上对浏览器的窗口进行改变,画板不会自适应 582 | 583 | 解决办法: 584 | 585 | onresize响应事件处理中,获取到的页面尺寸参数是变更后的参数 。 586 | 587 | 当窗口大小发生改变之后,重新设置canvas的宽高,简单来说,就是窗口改变之后,给canvas.width和canvas.height重新赋值。 588 | 589 | ```js 590 | // 记得要执行autoSetSize这个函数哦 591 | function autoSetSize(){ 592 | canvasSetSize(); 593 | // 当执行这个函数的时候,会先设置canvas的宽高 594 | function canvasSetSize(){ 595 | let pageWidth = document.documentElement.clientWidth; 596 | let pageHeight = document.documentElement.clientHeight; 597 | 598 | canvas.width = pageWidth; 599 | canvas.height = pageHeight; 600 | } 601 | // 在窗口大小改变之后,就会触发resize事件,重新设置canvas的宽高 602 | window.onresize = function(){ 603 | canvasSetSize(); 604 | } 605 | } 606 | ``` 607 | 608 | ### 问题2:当绘制线条宽度比较小的时候还好,一旦比较粗就会出现问题 609 | 610 | 解决办法:看一下文档,得出方法,只需要简单修改一下**绘制线条的代码**就行 611 | 612 | ```js 613 | // 画线函数 614 | function drawLine(x1,y1,x2,y2){ 615 | context.beginPath(); 616 | context.lineWidth = lWidth; 617 | //-----加入----- 618 | // 设置线条末端样式。 619 | context.lineCap = "round"; 620 | // 设定线条与线条间接合处的样式 621 | context.lineJoin = "round"; 622 | //-----加入----- 623 | context.moveTo(x1,y1); 624 | context.lineTo(x2,y2); 625 | context.stroke(); 626 | context.closePath(); 627 | } 628 | ``` 629 | 630 | ### 问题3:如何实现圆形的橡皮檫? 631 | 632 | 解决办法: 633 | 634 | canvas的API中,可以清除像素的就是`clearRect`方法,但是`clearRect`方法的清除区域矩形,毕竟大部分人的习惯中的橡皮擦都是圆形的,所以就引入了剪辑区域这个强大的功能,也就是`clip`方法。用法很简单:  635 | 636 | ```js 637 | ctx.save() 638 | ctx.beginPath() 639 | ctx.arc(x2,y2,a,0,2*Math.PI); 640 | ctx.clip() 641 | ctx.clearRect(0,0,canvas.width,canvas.height); 642 | ctx.restore(); 643 | ``` 644 | 645 | 上面那段代码就实现了圆形区域的擦除,也就是先实现一个圆形路径,然后把这个路径作为剪辑区域,再清除像素就行了。有个注意点就是需要先保存绘图环境,清除完像素后要重置绘图环境,如果不重置的话以后的绘图都是会被限制在那个剪辑区域中。 646 | 647 | ### 问题4:如何兼容移动端? 648 | 649 | #### 1.添加meta标签 650 | 651 | 因为浏览器初始会将页面现在手机端显示时进行缩放,因此我们可以在meta标签中设置meta viewport属性,告诉浏览器不将页面进行缩放,页面宽度=用户设备屏幕宽度 652 | 653 | ```js 654 | 657 | 658 | /* 659 | 页面宽度=移动宽度 :width=device-width 660 | 用户不可以缩放:user-scalable=no 661 | 缩放比例:initial-scale=1 662 | 最大缩放比例:maximum-scale=1.0 663 | 最小缩放比例:minimum-scale=1.0 664 | */ 665 | ``` 666 | 667 | #### 2.在移动端几乎使用的都是touch事件,与PC端不同 668 | 669 | 由于移动端是触摸事件,所以要用到H5的属性touchstart/touchmove/touchend,但是PC端只支持鼠标事件,所以要进行特性检测。 670 | 671 | 在`touch`事件里,是通过`.touches[0].clientX`和`.touches[0].clientY`来获取坐标的,这点要和`mouse`事件区别开。 672 | 673 | ### 问题5:当浏览器大小变化时,画布被清空 674 | 解决办法1:http://js.jirengu.com/dafic/2/edit 675 | 676 | 解决办法2:http://js.jirengu.com/worus/2/edit 677 | 678 | 参考链接:[canvas长宽变化时,画布内容消失](https://blog.csdn.net/vuturn/article/details/47807899) 679 | ### 问题6:当橡皮擦移动很快时会变成圆点 680 | 参考链接: 681 | [HTML5 实现橡皮擦的擦除效果](https://www.cnblogs.com/axes/p/3850309.html) 682 | 683 | ### 问题7:橡皮擦把背景层都给擦掉了,橡皮擦需要优化 684 | 嗯嗯,这个问题尚未解决,所以我就先把自定义背景颜色的功能取消掉 685 | 686 | ### 问题8:出现一个问题就是清空之后,重新画,然后出现原来的画的东西 687 | 688 | 这个嘛,问题不大,只不过是我漏写context.beginPath(); ,也花了一点时间在上面解决bug,让我想起“代码千万行,注释第一行;编程不规范,同事两行泪 ”,还是按照文档操作规范操作好,真香!!! 689 | 690 | >本文作者 **xyyojl** 691 | > 692 | >本文如有错误之处,请留言,我会及时更正 693 | > 694 | >或者提bug、提需求也是可以的 695 | > 696 | >觉得对您有帮助的话就**点个赞**或**收藏**吧! 697 | > 698 | >*欢迎转载或分享,转载时请注明出处* 699 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | *{margin: 0;padding: 0;box-sizing: border-box;} 2 | *::before{box-sizing: border-box;} 3 | *::after{box-sizing: border-box;} 4 | ul{padding: 0; } 5 | li{list-style: none;} 6 | .clearfix::after{ 7 | content: ''; 8 | display: block; 9 | clear: both; 10 | } 11 | .bg-btn{ 12 | position: fixed; 13 | top: 25px; 14 | left: 25px; 15 | width: 50px; 16 | height: 50px; 17 | background: url('../image/ic_droplet1.png') center center no-repeat; 18 | background-size: 30px 30px; 19 | border-radius: 50%; 20 | box-shadow: 0 1px 2px 0 rgba(32,33,36,0.28); 21 | cursor: pointer; 22 | background-color: #fff; 23 | } 24 | .bg-btn.active{ 25 | box-shadow: 0 1px 8px 0 rgba(32,33,36,0.28); 26 | } 27 | .tools{ 28 | position: fixed; 29 | left: 0; 30 | bottom: 30px; 31 | width: 100%; 32 | height: 50px; 33 | display: flex; 34 | justify-content: center; 35 | text-align: center; 36 | } 37 | .tools .container{ 38 | /* height: 70px; */ 39 | padding: 8px 20px; 40 | border-radius: 40px; 41 | box-shadow: 0 1px 2px 0 rgba(32,33,36,0.28); 42 | background: #fff; 43 | } 44 | .tools button{ 45 | width: 30px; 46 | height: 30px; 47 | border: none; 48 | outline: none; 49 | background-size: 20px 20px; 50 | background-position: center center; 51 | background-repeat: no-repeat; 52 | background-color: #fff; 53 | margin: 0 6px; 54 | transition: 0.3s; 55 | cursor: pointer; 56 | border: 2px solid transparent; 57 | } 58 | .tools button.save{ 59 | background-image: url('../image/save.png'); 60 | } 61 | .tools button.brush{ 62 | background-image: url('../image/pen.png'); 63 | } 64 | .tools button.eraser{ 65 | background-image: url('../image/eraser.png'); 66 | } 67 | .tools button.clear{ 68 | background-image: url('../image/clear.png'); 69 | } 70 | .tools button.undo{ 71 | background-image: url('../image/undo_sel.png'); 72 | } 73 | .tools button.redo{ 74 | background-image: url('../image/redo_sel.png'); 75 | } 76 | .tools button.undo.active{ 77 | background-image: url('../image/undo.png'); 78 | border-color: transparent; 79 | } 80 | .tools button.redo.active{ 81 | background-image: url('../image/redo.png'); 82 | border-color: transparent; 83 | } 84 | .tools button.active{ 85 | border-radius: 5px; 86 | border-color: #1398E6; 87 | } 88 | 89 | .color-group{ 90 | display: none; 91 | position: absolute; 92 | top: 50px; 93 | left: 50%; 94 | margin-left: -161px; 95 | width: 322px; 96 | padding: 16px; 97 | border-radius: 5px; 98 | box-shadow: 0 1px 4px 0 rgba(32,33,36,0.28); 99 | background-color: #fff; 100 | } 101 | .color-group .closeBtn, 102 | .pen-detail .closeBtn{ 103 | position: absolute; 104 | top: 6px; 105 | right: 8px; 106 | width: 32px; 107 | height: 32px; 108 | background: url('../image/close.png') center center no-repeat; 109 | cursor: pointer; 110 | } 111 | .color-group.active{ 112 | display: block; 113 | } 114 | .color-group h3{ 115 | font-weight: 500; 116 | margin-bottom: 8px; 117 | font-size: 18px; 118 | } 119 | .color-group li{ 120 | position: relative; 121 | float: left; 122 | list-style: none; 123 | width: 40px; 124 | height: 40px; 125 | border-radius: 50%; 126 | margin: 4px; 127 | cursor: pointer; 128 | } 129 | .color-group li.active::before{ 130 | position: absolute; 131 | left: 4px; 132 | top: 4px; 133 | content: ''; 134 | display: block; 135 | width: 32px; 136 | height: 32px; 137 | background: transparent; 138 | border: 3px solid #fff; 139 | border-radius: 50%; 140 | } 141 | .pen-detail{ 142 | display: none; 143 | position: fixed; 144 | left: 50%; 145 | margin-left: -140px; 146 | bottom: 90px; 147 | width: 280px; 148 | height: 210px; 149 | padding: 20px 24px; 150 | border: 1px solid #81A4BD; 151 | border-radius: 5px; 152 | color: #808FA2; 153 | font-style: 18px; 154 | background: #fff; 155 | font-size: 14px; 156 | } 157 | .pen-detail.active{ 158 | display: block; 159 | } 160 | .pen-detail p{ 161 | margin-top: 2px; 162 | margin-bottom: 4px; 163 | } 164 | .pen-detail .pen-type li{ 165 | float: left; 166 | width: 30px; 167 | height: 30px; 168 | background-size: 28px 28px; 169 | background-position: center center; 170 | background-repeat: no-repeat; 171 | cursor: pointer; 172 | } 173 | .pen-detail .pen-type li.pen{ 174 | background-image: url('../image/pen_sel.png'); 175 | } 176 | .pen-detail .pen-type li.pen.active{ 177 | background-image: url('../image/pen.png'); 178 | } 179 | .pen-detail .pen-type li.line{ 180 | background-image: url('../image/line_sel.png'); 181 | } 182 | .pen-detail .pen-type li.line.active{ 183 | background-image: url('../image/line.png'); 184 | } 185 | .pen-detail .pen-type li.circle{ 186 | border-radius: 50%; 187 | border: 2px solid #CDCDCD; 188 | } 189 | .pen-detail .pen-type li.circle.active, 190 | .pen-detail .pen-type li.rect.active{ 191 | border-color: #1F1F1F; 192 | } 193 | .pen-detail .pen-type li.rect{ 194 | border-radius: 4px; 195 | border: 2px solid #CDCDCD; 196 | } 197 | .pen-detail .pen-type li + li{ 198 | margin-left: 28px; 199 | } 200 | 201 | .circle-box{ 202 | line-height: 24px; 203 | } 204 | 205 | .circle-box{ 206 | position: relative; 207 | width: 24px; 208 | height: 24px; 209 | display: inline-block; 210 | text-align: center; 211 | margin-right: 8px; 212 | } 213 | #thickness{ 214 | position: absolute; 215 | top: 50%; 216 | left: 50%; 217 | margin-left: -1px; 218 | margin-top: -1px; 219 | background: #000; 220 | border-radius: 50%; 221 | transform-origin: center; 222 | width: 2px; 223 | height: 2px; 224 | } 225 | input[type=range]{ 226 | -webkit-appearance: none; 227 | width: 180px; 228 | height: 24px; 229 | outline: none; 230 | } 231 | input[type='range']::-webkit-slider-runnable-track{ 232 | background-color: #DBDBDB; 233 | height: 4px; 234 | border-radius: 5px; 235 | } 236 | input[type='range']::-webkit-slider-thumb { 237 | -webkit-appearance: none; 238 | /* border: 5px solid #fff; */ 239 | width: 12px; 240 | height: 12px; 241 | border-radius: 50%; 242 | background: #FF4081; 243 | cursor: pointer; 244 | margin-top: -4px; 245 | } 246 | .pen-color li{ 247 | position: relative; 248 | float: left; 249 | list-style: none; 250 | width: 30px; 251 | height: 30px; 252 | border-radius: 50%; 253 | margin: 4px; 254 | cursor: pointer; 255 | } 256 | .pen-color li.active::before{ 257 | position: absolute; 258 | left: 3px; 259 | top: 3px; 260 | content: ''; 261 | display: block; 262 | width: 24px; 263 | height: 24px; 264 | background: transparent; 265 | border: 2px solid #fff; 266 | border-radius: 50%; 267 | } 268 | .pen-detail i.showOpacity{ 269 | display: inline-block; 270 | width: 26px; 271 | height: 26px; 272 | background: #000; 273 | border-radius: 50%; 274 | margin-right: 4px; 275 | margin-left: 4px; 276 | } 277 | canvas{ 278 | display: block; 279 | background: #fff; 280 | } 281 | 282 | 283 | @media screen and (max-width: 768px) { 284 | .color-group{ 285 | top: 80px; 286 | } 287 | .tools{ 288 | bottom: 15px; 289 | } 290 | .pen-detail{ 291 | bottom: 80px; 292 | } 293 | } -------------------------------------------------------------------------------- /image/PC端.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/PC端.png -------------------------------------------------------------------------------- /image/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/clear.png -------------------------------------------------------------------------------- /image/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/close.png -------------------------------------------------------------------------------- /image/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/eraser.png -------------------------------------------------------------------------------- /image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/favicon.png -------------------------------------------------------------------------------- /image/ic_droplet1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/ic_droplet1.png -------------------------------------------------------------------------------- /image/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/line.png -------------------------------------------------------------------------------- /image/line_sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/line_sel.png -------------------------------------------------------------------------------- /image/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/pen.png -------------------------------------------------------------------------------- /image/pen_sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/pen_sel.png -------------------------------------------------------------------------------- /image/redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/redo.png -------------------------------------------------------------------------------- /image/redo_sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/redo_sel.png -------------------------------------------------------------------------------- /image/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/save.png -------------------------------------------------------------------------------- /image/save_sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/save_sel.png -------------------------------------------------------------------------------- /image/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/undo.png -------------------------------------------------------------------------------- /image/undo_sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/undo_sel.png -------------------------------------------------------------------------------- /image/移动端.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyyojl/drawingborad/25ec34e88df857072d3fb801dda5b76f208170c0/image/移动端.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 智绘画板 8 | 9 | 10 | 11 | 12 | 13 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 | 44 | 51 |

笔大小

52 | 53 |

笔颜色

54 | 62 |

不透明度

63 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | let canvas = document.getElementById('canvas'); 2 | let context = canvas.getContext('2d'); 3 | let eraser = document.getElementById('eraser'); 4 | let brush = document.getElementById('brush'); 5 | let reSetCanvas = document.getElementById("clear"); 6 | let save = document.getElementById("save"); 7 | /* let selectBg = document.querySelector('.bg-btn'); 8 | let bgGroup = document.querySelector('.color-group'); 9 | let bgcolorBtn = document.querySelectorAll('.bgcolor-item'); */ 10 | let penDetail = document.getElementById("penDetail"); 11 | let aColorBtn = document.getElementsByClassName("color-item"); 12 | let undo = document.getElementById("undo"); 13 | let redo = document.getElementById("redo"); 14 | 15 | 16 | let range1 = document.getElementById('range1'); 17 | let range2 = document.getElementById('range2'); 18 | let showOpacity = document.querySelector('.showOpacity'); 19 | let closeBtn = document.querySelectorAll('.closeBtn'); 20 | let eraserEnabled = false; 21 | let activeBgColor = '#fff'; 22 | let ifPop = false; 23 | let lWidth = 2; 24 | let opacity = 1; 25 | let strokeColor = 'rgba(0,0,0,1)'; 26 | let radius = 5; 27 | 28 | 29 | autoSetSize(); 30 | 31 | setCanvasBg('white'); 32 | 33 | listenToUser(); 34 | 35 | 36 | /* 下面是实现相关效果的函数,可以不用看 */ 37 | 38 | function autoSetSize(){ 39 | canvasSetSize(); 40 | function canvasSetSize(){ 41 | // 把变化之前的画布内容copy一份,然后重新画到画布上 42 | let imgData = context.getImageData(0,0,canvas.width,canvas.height); 43 | let pageWidth = document.documentElement.clientWidth; 44 | let pageHeight = document.documentElement.clientHeight; 45 | 46 | canvas.width = pageWidth; 47 | canvas.height = pageHeight; 48 | context.putImageData(imgData,0,0); 49 | } 50 | 51 | window.onresize = function(){ 52 | canvasSetSize(); 53 | } 54 | } 55 | 56 | // 监听用户鼠标事件 57 | function listenToUser() { 58 | // 定义一个变量初始化画笔状态 59 | let painting = false; 60 | // 记录画笔最后一次的位置 61 | let lastPoint = {x: undefined, y: undefined}; 62 | 63 | if(document.body.ontouchstart !== undefined){ 64 | canvas.ontouchstart = function (e) { 65 | painting = true; 66 | let x1 = e.touches[0].clientX; 67 | let y1 = e.touches[0].clientY; 68 | if(eraserEnabled){//要使用eraser 69 | context.save(); 70 | context.globalCompositeOperation = "destination-out"; 71 | context.beginPath(); 72 | radius = (lWidth/2) > 5? (lWidth/2) : 5; 73 | context.arc(x1,y1,radius,0,2*Math.PI); 74 | context.clip(); 75 | context.clearRect(0,0,canvas.width,canvas.height); 76 | context.restore(); 77 | lastPoint = {'x': x1,'y': y1} 78 | }else{ 79 | lastPoint = {'x': x1,'y': y1} 80 | } 81 | }; 82 | canvas.ontouchmove = function (e) { 83 | let x1 = lastPoint['x']; 84 | let y1 = lastPoint['y']; 85 | let x2 = e.touches[0].clientX; 86 | let y2 = e.touches[0].clientY; 87 | if(!painting){return} 88 | if(eraserEnabled){ 89 | moveHandler(x1,y1,x2,y2); 90 | //记录最后坐标 91 | lastPoint['x'] = x2; 92 | lastPoint['y'] = y2; 93 | }else{ 94 | let newPoint = {'x': x2,'y': y2}; 95 | drawLine(lastPoint.x, lastPoint.y,newPoint.x, newPoint.y); 96 | lastPoint = newPoint; 97 | } 98 | }; 99 | 100 | canvas.ontouchend = function () { 101 | painting = false; 102 | canvasDraw(); 103 | } 104 | }else{ 105 | // 鼠标按下事件 106 | canvas.onmousedown = function(e){ 107 | painting = true; 108 | let x1 = e.clientX; 109 | let y1 = e.clientY; 110 | if(eraserEnabled){//要使用eraser 111 | //鼠标第一次点下的时候擦除一个圆形区域,同时记录第一个坐标点 112 | context.save(); 113 | context.globalCompositeOperation = "destination-out"; 114 | context.beginPath(); 115 | radius = (lWidth/2) > 5? (lWidth/2) : 5; 116 | context.arc(x1,y1,radius,0,2*Math.PI); 117 | context.clip(); 118 | context.clearRect(0,0,canvas.width,canvas.height); 119 | context.restore(); 120 | lastPoint = {'x': x1,'y': y1} 121 | }else{ 122 | lastPoint = {'x': x1,'y': y1} 123 | } 124 | } 125 | 126 | // 鼠标移动事件 127 | canvas.onmousemove = function(e){ 128 | let x1 = lastPoint['x']; 129 | let y1 = lastPoint['y']; 130 | let x2 = e.clientX; 131 | let y2 = e.clientY; 132 | if(!painting){return} 133 | if(eraserEnabled){ 134 | moveHandler(x1,y1,x2,y2); 135 | //记录最后坐标 136 | lastPoint['x'] = x2; 137 | lastPoint['y'] = y2; 138 | }else{ 139 | let newPoint = {'x': x2,'y': y2}; 140 | drawLine(lastPoint.x, lastPoint.y,newPoint.x, newPoint.y); 141 | lastPoint = newPoint; 142 | } 143 | } 144 | 145 | // 鼠标松开事件 146 | canvas.onmouseup = function(){ 147 | painting = false; 148 | canvasDraw(); 149 | } 150 | } 151 | 152 | 153 | 154 | } 155 | 156 | // 157 | function moveHandler(x1,y1,x2,y2){ 158 | //获取两个点之间的剪辑区域四个端点 159 | var asin = radius*Math.sin(Math.atan((y2-y1)/(x2-x1))); 160 | var acos = radius*Math.cos(Math.atan((y2-y1)/(x2-x1))) 161 | var x3 = x1+asin; 162 | var y3 = y1-acos; 163 | var x4 = x1-asin; 164 | var y4 = y1+acos; 165 | var x5 = x2+asin; 166 | var y5 = y2-acos; 167 | var x6 = x2-asin; 168 | var y6 = y2+acos; 169 | 170 |   //保证线条的连贯,所以在矩形一端画圆 171 | context.save() 172 | context.beginPath() 173 | context.globalCompositeOperation = "destination-out"; 174 | radius = (lWidth/2) > 5? (lWidth/2) : 5; 175 | context.arc(x2,y2,radius,0,2*Math.PI); 176 | context.clip() 177 | context.clearRect(0,0,canvas.width,canvas.height); 178 | context.restore(); 179 | 180 |   //清除矩形剪辑区域里的像素 181 | context.save() 182 | context.beginPath() 183 | context.globalCompositeOperation = "destination-out"; 184 | context.moveTo(x3,y3); 185 | context.lineTo(x5,y5); 186 | context.lineTo(x6,y6); 187 | context.lineTo(x4,y4); 188 | context.closePath(); 189 | context.clip(); 190 | context.clearRect(0,0,canvas.width,canvas.height); 191 | context.restore(); 192 | } 193 | 194 | 195 | // 画线函数 196 | function drawLine(x1,y1,x2,y2){ 197 | context.beginPath(); 198 | context.lineWidth = lWidth; 199 | // context.strokeStyle = strokeColor; 200 | // context.globalAlpha = opacity; 201 | // 设置线条末端样式。 202 | context.lineCap = "round"; 203 | // 设定线条与线条间接合处的样式 204 | context.lineJoin = "round"; 205 | context.moveTo(x1,y1); 206 | context.lineTo(x2,y2); 207 | context.stroke(); 208 | context.closePath(); 209 | } 210 | 211 | // 点击橡皮檫 212 | eraser.onclick = function(){ 213 | eraserEnabled = true; 214 | eraser.classList.add('active'); 215 | brush.classList.remove('active'); 216 | } 217 | // 点击画笔 218 | brush.onclick = function(){ 219 | eraserEnabled = false; 220 | brush.classList.add('active'); 221 | eraser.classList.remove('active'); 222 | if(!ifPop){ 223 | // 弹出框 224 | penDetail.classList.add('active'); 225 | }else{ 226 | penDetail.classList.remove('active'); 227 | } 228 | ifPop = !ifPop; 229 | } 230 | 231 | // 实现清屏 232 | reSetCanvas.onclick = function(){ 233 | context.clearRect(0,0,canvas.width,canvas.height); 234 | setCanvasBg('white'); 235 | canvasHistory = []; 236 | undo.classList.remove('active'); 237 | redo.classList.remove('active'); 238 | } 239 | 240 | // 重新设置canvas背景颜色 241 | function setCanvasBg(color) { 242 | context.fillStyle = color; 243 | context.fillRect(0, 0, canvas.width, canvas.height); 244 | } 245 | 246 | // 下载图片 247 | save.onclick = function(){ 248 | let imgUrl = canvas.toDataURL('image/png'); 249 | let saveA = document.createElement('a'); 250 | document.body.appendChild(saveA); 251 | saveA.href = imgUrl; 252 | saveA.download = 'mypic'+(new Date).getTime(); 253 | saveA.target = '_blank'; 254 | saveA.click(); 255 | } 256 | 257 | 258 | // 实现了切换背景颜色 259 | /* for (let i = 0; i < bgcolorBtn.length; i++) { 260 | bgcolorBtn[i].onclick = function (e) { 261 | e.stopPropagation(); 262 | for (let i = 0; i < bgcolorBtn.length; i++) { 263 | bgcolorBtn[i].classList.remove("active"); 264 | this.classList.add("active"); 265 | activeBgColor = this.style.backgroundColor; 266 | setCanvasBg(activeBgColor); 267 | } 268 | 269 | } 270 | } 271 | document.onclick = function(){ 272 | bgGroup.classList.remove('active'); 273 | } 274 | 275 | selectBg.onclick = function(e){ 276 | bgGroup.classList.add('active'); 277 | e.stopPropagation(); 278 | } */ 279 | 280 | // 实现改变画笔粗细的功能 1-20 放大的倍数 1 10 实际大小呢? 2-20 281 | 282 | range1.onchange = function(){ 283 | thickness.style.transform = 'scale('+ (parseInt(range1.value)) +')'; 284 | lWidth = parseInt(range1.value*2); 285 | } 286 | 287 | range2.onchange = function(){ 288 | opacity = 1 - parseInt(this.value)/10; 289 | if(opacity !== 0){ 290 | showOpacity.style.opacity = opacity; 291 | } 292 | } 293 | 294 | // 改变画笔颜色 295 | getColor(); 296 | 297 | function getColor(){ 298 | for (let i = 0; i < aColorBtn.length; i++) { 299 | aColorBtn[i].onclick = function (e) { 300 | // e.stopPropagation(); 301 | for (let i = 0; i < aColorBtn.length; i++) { 302 | aColorBtn[i].classList.remove("active"); 303 | this.classList.add("active"); 304 | activeColor = this.style.backgroundColor; 305 | context.fillStyle = activeColor; 306 | context.strokeStyle = activeColor; 307 | } 308 | penDetail.classList.remove('active'); 309 | ifPop = false; 310 | } 311 | } 312 | } 313 | 314 | // 实现撤销和重做的功能 315 | let canvasHistory = []; 316 | let step = -1; 317 | 318 | // 绘制方法 319 | function canvasDraw(){ 320 | step++; 321 | if(step < canvasHistory.length){ 322 | canvasHistory.length = step; // 截断数组 323 | } 324 | // 添加新的绘制到历史记录 325 | canvasHistory.push(canvas.toDataURL()); 326 | if(step > 0){ 327 | undo.classList.add('active'); 328 | } 329 | } 330 | 331 | // 撤销方法 332 | function canvasUndo(){ 333 | if(step > 0){ 334 | step--; 335 | let canvasPic = new Image(); 336 | canvasPic.src = canvasHistory[step]; 337 | canvasPic.onload = function () { context.drawImage(canvasPic, 0, 0); } 338 | undo.classList.add('active'); 339 | redo.classList.add('active'); 340 | }else{ 341 | undo.classList.remove('active'); 342 | alert('不能再继续撤销了'); 343 | } 344 | } 345 | // 重做方法 346 | function canvasRedo(){ 347 | if(step < canvasHistory.length - 1){ 348 | step++; 349 | let canvasPic = new Image(); 350 | canvasPic.src = canvasHistory[step]; 351 | canvasPic.onload = function () { 352 | context.drawImage(canvasPic, 0, 0); 353 | } 354 | // redo.classList.add('active'); 355 | }else { 356 | redo.classList.remove('active') 357 | alert('已经是最新的记录了'); 358 | } 359 | } 360 | undo.onclick = function(){ 361 | canvasUndo(); 362 | } 363 | redo.onclick = function(){ 364 | canvasRedo(); 365 | } 366 | 367 | 368 | 369 | for (let index = 0; index < closeBtn.length; index++) { 370 | closeBtn[index].onclick = function(e){ 371 | let btnParent = e.target.parentElement; 372 | btnParent.classList.remove('active'); 373 | } 374 | 375 | } 376 | 377 | window.onbeforeunload = function(){ 378 | return "Reload site?"; 379 | }; 380 | --------------------------------------------------------------------------------