手把手教你实现一个canvas智绘画板

下载地址

前言

本文主要介绍:

  • 项目介绍
  • 项目效果展示
  • 一步步实现项目效果
  • 踩坑

一、项目介绍

名称: 智绘画板
技术栈: HTML5,CSS3,JavaScript,移动端
功能描述

  • 支持PC端和移动端在线绘画功能
  • 实现任意选择画笔颜色、调整画笔粗细以及橡皮檫擦除等绘画功能
  • 实现在线画板的本地保存功能
  • 支持撤销和返回操作
  • 自定义背景颜色[这个功能尚未完善好]
    注:本项目仅仅是canvas和JavaScript练手小项目,存在一个问题尚未解决,橡皮擦把背景层都给擦掉了,希望有大神给一些建议给我,谢谢!

二、一步步实现项目效果

(一)分析页面

用户可以进行的操作:

  • 画画
  • 改变画笔的粗细
  • 切换画笔的颜色
  • 使用橡皮檫擦除不想要的部分
  • 清空画板
  • 将自己画的东西保存成图片
  • 进行撤销和重做操作
  • 兼容移动端(支持触摸)

(二)进行HTML布局

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>智绘画板</title><link rel="shortcut icon" href="./image/favicon.png" type="image/x-icon"><link rel="stylesheet" href="./css/style.css">
</head>
<body><canvas id="canvas"></canvas><!-- 自定义背景颜色功能尚未完善好 --><!--<div class="bg-btn"></div><div class="color-group" id="bgGroup"><h3>选择背景颜色:</h3><ul class="clearfix"><li class="bgcolor-item" style="background-color: blue;"></li><li class="bgcolor-item" style="background-color: black;"></li><li class="bgcolor-item" style="background-color: #FF3333;"></li><li class="bgcolor-item" style="background-color: #0066FF;"></li><li class="bgcolor-item" style="background-color: #FFFF33;"></li><li class="bgcolor-item" style="background-color: #33CC66;"></li><li class="bgcolor-item" style="background-color: gray;"></li><li class="bgcolor-item" style="background-color: #F34334;"></li><li class="bgcolor-item" style="background-color: #fff;box-shadow: 0 1px 2px 0 rgba(32,33,36,0.28);"></li><li class="bgcolor-item" style="background-color: #9B27AC;"></li><li class="bgcolor-item" style="background-color: #4CB050;"></li><li class="bgcolor-item" style="background-color: #029688;"></li></ul><i class="closeBtn"></i></div>--><div class="tools"><div class="container"><button class="save"  id="save" title="保存"></button><button class="brush active" id="brush" title="画笔"></button><button class="eraser" id="eraser" title="橡皮擦"></button><button class="clear" id="clear" title="清屏"></button><button class="undo"  id="undo" title="撤销"></button><button class="redo"  id="redo" title="再做"></button></div></div><div class="pen-detail" id="penDetail"><i class="closeBtn"></i><p>笔大小</p><span class="circle-box"><i id="thickness"></i></span> <input type="range" id="range1" min="1" max="10" value="1"><p>笔颜色</p><ul class="pen-color clearfix"><li class="color-item active" style="background-color: black;"></li><li class="color-item" style="background-color: #FF3333;"></li><li class="color-item" style="background-color: #99CC00;"></li><li class="color-item" style="background-color: #0066FF;"></li><li class="color-item" style="background-color: #FFFF33;"></li><li class="color-item" style="background-color: #33CC66;"></li></ul><p>不透明度</p><i class="showOpacity"></i> <input type="range" id="range2" min="1" max="10" value="1"></div><script src="./js/main.js"></script>
</body>
</html>

(三)用CSS美化界面

可根据自己爱好美化

使用JS实现项目的具体功能

1.准备工作

//准备个容器,也就是画板了
<canvas id="canvas"></canvas>//然后初始化js
let canvas = document.getElementById('canvas');
let context = canvas.getContext('2d');//把画板做成全屏的,所以接下来设置一下canvas的宽高
let pageWidth = document.documentElement.clientWidth;
let pageHeight = document.documentElement.clientHeight;canvas.width = pageWidth;
canvas.height = pageHeight;//由于部分IE不支持canvas,如果要兼容IE,我们可以创建一个canvas,然后使用excanvas初始化,针对IE加上exCanvas.js,这里我们明确不考虑IE。// 记得要执行autoSetSize这个函数哦
function autoSetSize(){canvasSetSize();// 当执行这个函数的时候,会先设置canvas的宽高function canvasSetSize(){// 把变化之前的画布内容copy一份,然后重新画到画布上let imgData = context.getImageData(0,0,canvas.width,canvas.height);let pageWidth = document.documentElement.clientWidth;let pageHeight = document.documentElement.clientHeight;canvas.width = pageWidth;canvas.height = pageHeight;context.putImageData(imgData,0,0);}// 在窗口大小改变之后,就会触发resize事件,重新设置canvas的宽高window.onresize = function(){canvasSetSize();}
}

2.实现画画的功能
实现思路:监听鼠标事件, 用drawLine()方法把记录的数据画出来。

  • 初始化当前画板的画笔状态,painting = false。
  • 当鼠标按下时(mousedown),把painting设为true,表示正在画,鼠标没松开。把鼠标点记录下来。
  • 当按下鼠标的时候,鼠标移动(mousemove)就把点记录下来并画出来。
  • 如果鼠标移动过快,浏览器跟不上绘画速度,点与点之间会出现间隙,所以我们需要将画出的点用线连起来(lineTo())。
  • 鼠标松开的时候(mouseup),把painting设为false。

注:drawCircle这个方法其实可以不用书写,这个只是为了让大家能够理解开始点击的位置在哪里?

function listenToUser() {// 定义一个变量初始化画笔状态let painting = false;// 记录画笔最后一次的位置let lastPoint = {x: undefined, y: undefined};// 鼠标按下事件canvas.onmousedown = function(e){painting = true;let x = e.clientX;let y = e.clientY;lastPoint = {'x':x,'y':y};drawCircle(x,y,5);}// 鼠标移动事件canvas.onmousemove = function(e){if(painting){let x = e.clientX;let y = e.clientY;let newPoint = {'x':x,'y':y};drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y);lastPoint = newPoint;}}// 鼠标松开事件canvas.onmouseup = function(){painting = false;}
}// 画点函数
function drawCircle(x,y,radius){// 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。context.beginPath();// 画一个以(x,y)为圆心的以radius为半径的圆弧(圆),// 从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。context.arc(x,y,radius,0,Math.PI*2);// 通过填充路径的内容区域生成实心的图形context.fill();// 闭合路径之后图形绘制命令又重新指向到上下文中。context.closePath();
}function drawLine(x1,y1,x2,y2){// 设置线条宽度context.lineWidth = 10;// 设置线条末端样式。context.lineCap = "round";// 设定线条与线条间接合处的样式context.lineJoin = "round";// moveTo(x,y)将笔触移动到指定的坐标x以及y上context.moveTo(x1,y1);// lineTo(x, y) 绘制一条从当前位置到指定x以及y位置的直线context.lineTo(x2,y2);// 通过线条来绘制图形轮廓context.stroke();context.closePath();
}

3.实现橡皮擦功能

  • 实现思路: 获取橡皮擦元素
  • 设置橡皮擦初始状态,eraserEnabled = false。
  • 监听橡皮擦click事件,点击橡皮擦,改变橡皮擦状态,eraserEnabled = true,并且切换class,实现被激活的效果。
  • eraserEnabled为true,移动鼠标用context.clearRect()实现了橡皮檫。

但是我发现canvas的API中,可以清除像素的就是clearRect方法,但是clearRect方法的清除区域矩形,毕竟大部分人的习惯中的橡皮擦都是圆形的,所以就引入了剪辑区域这个强大的功能,也就是clip方法。下面的代码是使用context.clearRect()实现了 橡皮檫。请看踩坑部分,了解如何更好的实现橡皮檫。

let eraser = document.getElementById("eraser");
let eraserEnabled = false;// 记得要执行listenToUser这个函数哦
function listenToUser() {// ... 代表省略了之前写的代码// ...// 鼠标按下事件canvas.onmousedown = function(e){// ...if(eraserEnabled){//要使用erasercontext.clearRect(x-5,y-5,10,10)}else{lastPoint = {'x':x,'y':y}}}// 鼠标移动事件canvas.onmousemove = function(e){let x = e.clientX;let y = e.clientY;if(!painting){return}if(eraserEnabled){context.clearRect(x-5,y-5,10,10);}else{var newPoint = {'x':x,'y':y};drawLine(lastPoint.x, lastPoint.y,newPoint.x, newPoint.y);lastPoint = newPoint;}  }// ...
}// 点击橡皮檫
eraser.onclick = function(){eraserEnabled = true;eraser.classList.add('active');brush.classList.remove('active');
}

4.实现清屏功能
实现思路:

  • 获取元素节点。
  • 点击清空按钮清空canvas画布。
let reSetCanvas = document.getElementById("clear");// 实现清屏
reSetCanvas.onclick = function(){ctx.clearRect(0,0,canvas.width,canvas.height);setCanvasBg('white');
}// 重新设置canvas背景颜色
function setCanvasBg(color) {ctx.fillStyle = color;ctx.fillRect(0, 0, canvas.width, canvas.height);
}

5.实现保存成图片功能
实现思路:

  • 获取canvas.toDateURL
  • 在页面里创建并插入一个a标签
  • a标签href等于canvas.toDateURL,并添加download属性
  • 点击保存按钮,a标签触发click事件
let save = document.getElementById("save");// 下载图片
save.onclick = function(){let imgUrl = canvas.toDataURL('image/png');let saveA = document.createElement('a');document.body.appendChild(saveA);saveA.href = imgUrl;saveA.download = 'mypic'+(new Date).getTime();saveA.target = '_blank';saveA.click();
}

6.实现改变背景颜色的功能

实现思路:

  1. 获取相应的元素节点。
  2. 给每一个class为bgcolor-item的标签添加点击事件,当点击事件触发时,改变背景颜色。
  3. 点击设置背景颜色的div之外的地方,实现隐藏那个div。
let selectBg = document.querySelector('.bg-btn');
let bgGroup = document.querySelector('.color-group');
let bgcolorBtn = document.querySelectorAll('.bgcolor-item');
let penDetail = document.getElementById("penDetail");
let activeBgColor = '#fff';// 实现了切换背景颜色
for (let i = 0; i < bgcolorBtn.length; i++) {bgcolorBtn[i].onclick = function (e) {// 阻止冒泡e.stopPropagation();for (let i = 0; i < bgcolorBtn.length; i++) {bgcolorBtn[i].classList.remove("active");this.classList.add("active");activeBgColor = this.style.backgroundColor;setCanvasBg(activeBgColor);}}
}document.onclick = function(){bgGroup.classList.remove('active');
}selectBg.onclick = function(e){bgGroup.classList.add('active');e.stopPropagation();
}

7.实现改变画笔粗细的功能

实现思路:

  1. 实现让设置画笔的属性的对话框出现。
  2. 获取相应的元素节点。
  3. 当input=range的元素发生改变的时候,获取到的值赋值给lWidth。
  4. 然后设置context.lineWidth = lWidth。
let range1 = document.getElementById('range1');
let lWidth = 2;
let ifPop = false;range1.onchange = function(){console.log(range1.value);console.log(typeof range1.value)thickness.style.transform = 'scale('+ (parseInt(range1.value)) +')';console.log(thickness.style.transform )lWidth = parseInt(range1.value*2);
}// 画线函数
function drawLine(x1,y1,x2,y2){// ...context.lineWidth = lWidth;// ...
}// 点击画笔
brush.onclick = function(){eraserEnabled = false;brush.classList.add('active');eraser.classList.remove('active');if(!ifPop){// 弹出框console.log('弹一弹')penDetail.classList.add('active');}else{penDetail.classList.remove('active');}ifPop = !ifPop;
}

8.实现改变画笔颜色的功能

实现思路跟改变画板背景颜色的思路类似。

let aColorBtn = document.getElementsByClassName("color-item");getColor();function getColor(){for (let i = 0; i < aColorBtn.length; i++) {aColorBtn[i].onclick = function () {for (let i = 0; i < aColorBtn.length; i++) {aColorBtn[i].classList.remove("active");this.classList.add("active");activeColor = this.style.backgroundColor;ctx.fillStyle = activeColor;ctx.strokeStyle = activeColor;}}}
}

9.实现改变撤销和重做的功能

实现思路:

  1. 保存快照:每完成一次绘制操作则保存一份 canvas 快照到 canvasHistory 数组(生成快照使用 canvas 的 toDataURL() 方法,生成的是 base64 的图片);
  2. 撤销和反撤销:把 canvasHistory 数组中对应索引的快照使用 canvas 的 drawImage() 方法重绘一遍;
  3. 绘制新图像:执行新的绘制操作时,删除当前位置之后的数组记录,然后添加新的快照。
let undo = document.getElementById("undo");
let redo = document.getElementById("redo");// ...
canvas.ontouchend = function () {painting = false;canvasDraw();
}// ...
canvas.onmouseup = function(){painting = false;canvasDraw();
}let canvasHistory = [];
let step = -1;// 绘制方法
function canvasDraw(){step++;if(step < canvasHistory.length){canvasHistory.length = step;  // 截断数组}// 添加新的绘制到历史记录canvasHistory.push(canvas.toDataURL());
}// 撤销方法
function canvasUndo(){if(step > 0){step--;// ctx.clearRect(0,0,canvas.width,canvas.height);let canvasPic = new Image();canvasPic.src = canvasHistory[step];canvasPic.onload = function () { ctx.drawImage(canvasPic, 0, 0); }undo.classList.add('active');}else{undo.classList.remove('active');alert('不能再继续撤销了');}
}
// 重做方法
function canvasRedo(){if(step < canvasHistory.length - 1){step++;let canvasPic = new Image();canvasPic.src = canvasHistory[step];canvasPic.onload = function () { // ctx.clearRect(0,0,canvas.width,canvas.height);ctx.drawImage(canvasPic, 0, 0);}redo.classList.add('active');}else {redo.classList.remove('active')alert('已经是最新的记录了');}
}
undo.onclick = function(){canvasUndo();
}
redo.onclick = function(){canvasRedo();
}

10.兼容移动端

实现思路:

  1. 判断设备是否支持触摸
  2. true,则使用touch事件;false,则使用mouse事件
// ...
if (document.body.ontouchstart !== undefined) {// 使用touch事件anvas.ontouchstart = function (e) {// 开始触摸}canvas.ontouchmove = function (e) {// 开始滑动}canvas.ontouchend = function () {// 滑动结束}
}else{// 使用mouse事件// ...
}
// ...

三、踩坑

问题1:在电脑上对浏览器的窗口进行改变,画板不会自适应

解决办法:

onresize响应事件处理中,获取到的页面尺寸参数是变更后的参数 。

当窗口大小发生改变之后,重新设置canvas的宽高,简单来说,就是窗口改变之后,给canvas.width和canvas.height重新赋值。

// 记得要执行autoSetSize这个函数哦
function autoSetSize(){canvasSetSize();// 当执行这个函数的时候,会先设置canvas的宽高function canvasSetSize(){let pageWidth = document.documentElement.clientWidth;let pageHeight = document.documentElement.clientHeight;canvas.width = pageWidth;canvas.height = pageHeight;}// 在窗口大小改变之后,就会触发resize事件,重新设置canvas的宽高window.onresize = function(){canvasSetSize();}
}

问题3:如何实现圆形的橡皮檫?

解决办法:

canvas的API中,可以清除像素的就是clearRect方法,但是clearRect方法的清除区域矩形,毕竟大部分人的习惯中的橡皮擦都是圆形的,所以就引入了剪辑区域这个强大的功能,也就是clip方法。用法很简单:

 // 画线函数
ctx.save()
ctx.beginPath()
ctx.arc(x2,y2,a,0,2*Math.PI);
ctx.clip()
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.restore();

上面那段代码就实现了圆形区域的擦除,也就是先实现一个圆形路径,然后把这个路径作为剪辑区域,再清除像素就行了。有个注意点就是需要先保存绘图环境,清除完像素后要重置绘图环境,如果不重置的话以后的绘图都是会被限制在那个剪辑区域中。

问题4:如何兼容移动端?

1.添加meta标签

因为浏览器初始会将页面现在手机端显示时进行缩放,因此我们可以在meta标签中设置meta viewport属性,告诉浏览器不将页面进行缩放,页面宽度=用户设备屏幕宽度

<meta name="viewport" content="width=device-width,
initial-scale=1,user-scalable=no,
maximum-scale=1.0,minimum-scale=1.0"/>
/*
页面宽度=移动宽度 :width=device-width
用户不可以缩放:user-scalable=no
缩放比例:initial-scale=1
最大缩放比例:maximum-scale=1.0
最小缩放比例:minimum-scale=1.0
*/

2.在移动端几乎使用的都是touch事件,与PC端不同
由于移动端是触摸事件,所以要用到H5的属性touchstart/touchmove/touchend,但是PC端只支持鼠标事件,所以要进行特性检测。

在touch事件里,是通过.touches[0].clientX和.touches[0].clientY来获取坐标的,这点要和mouse事件区别开。

问题5:当浏览器大小变化时,画布被清空

解决办法1:http://js.jirengu.com/dafic/2/edit

解决办法2:http://js.jirengu.com/worus/2/edit

参考链接:canvas长宽变化时,画布内容消失

问题6:当橡皮擦移动很快时会变成圆点

参考链接: HTML5 实现橡皮擦的擦除效果

问题7:橡皮擦把背景层都给擦掉了,橡皮擦需要优化

嗯嗯,这个问题尚未解决,所以我就先把自定义背景颜色的功能取消掉,但是并没有用,还是存在橡皮檫会把背景层给擦掉,希望有大神看到这篇文章,给一点建议和方法,谢谢!

问题8:出现一个问题就是清空之后,重新画,然后出现原来的画的东西

这个嘛,问题不大,只不过是我漏写context.beginPath(); ,也花了一点时间在上面解决bug,让我想起“代码千万行,注释第一行;编程不规范,同事两行泪 ”,还是按照文档操作规范操作好,真香!!!

手把手教你实现一个canvas智绘画板相关推荐

  1. python手机版做小游戏代码大全-Python大牛手把手教你做一个小游戏,萌新福利!...

    原标题:Python大牛手把手教你做一个小游戏,萌新福利! 引言 最近python语言大火,除了在科学计算领域python有用武之地之外,在游戏.后台等方面,python也大放异彩,本篇博文将按照正规 ...

  2. 自定义view学习-手把手教你制作一个可扩展日历控件

    来看看效果图先,手把手教你实现一个简易,但高扩展度的日历控件,可自由扩展成签到,单选,多选日期. 首先我们来分析实现思路.对于上图的效果,很明显是一个6x7的表格. 我们可以两个for循环控制绘制每个 ...

  3. 物联网全栈教程--手把手教你开发一个智能浇花器

    下面来说一下课程的安排,本教程可以分为三大章节,30个小章节,在1-10章节,手把手讲解了如何实现一个定时/实时控制的浇花器,可以对浇花器电量进行显示,可以进行定时设置,以及实时控制,低功耗模式等等, ...

  4. 手把手教你写一个生成对抗网络

    成对抗网络代码全解析, 详细代码解析(TensorFlow, numpy, matplotlib, scipy) 那么,什么是 GANs? 用 Ian Goodfellow 自己的话来说: " ...

  5. python k线合成_手把手教你写一个Python版的K线合成函数

    手把手教你写一个Python版的K线合成函数 在编写.使用策略时,经常会使用一些不常用的K线周期数据.然而交易所.数据源又没有提供这些周期的数据.只能通过使用已有周期的数据进行合成.合成算法已经有一个 ...

  6. 第五十八期:从0到1 手把手教你建一个区块链

    近期的区块链重回热点,如果你想深入了解区块链,那就来看一下本文,手把手教你构建一个自己的区块链. 作者:Captain编译 近期的区块链重回热点,如果你想深入了解区块链,那就来看一下本文,手把手教你构 ...

  7. 手把手教你写一个spring IOC容器

    本文分享自华为云社区<手把手教你写一个spring IOC容器>,原文作者:技术火炬手. spring框架的基础核心和起点毫无疑问就是IOC,IOC作为spring容器提供的核心技术,成功 ...

  8. 手把手教你撸一个Web汇率计算器

    手把手教你撸一个Web汇率计算器 前言 前段时间刚接触到前端网页开发,但是对于刚入门的小白而言,像flask.Django等这类稍大型的框架确实不太适合,今天这个Dash是集众家之长于一体的轻量化We ...

  9. 手把手教你写一个Matlab App(一)

    对于传统工科的学生用的最多的编程软件应该就是matlab,其集成度高,计算能力强,容易上手,颇受大众青睐.今天挖的这个新坑,主要是分享用matlab app designer设计GUI界面的一些方法和 ...

最新文章

  1. 不用地图如何导航?DeepMind提出新型双路径强化学习「智能体」架构
  2. IOS第四天-新浪微博 -存储优化OAuth授权账号信息,下拉刷新,字典转模型
  3. 3dContactPointAnnotationTool开发日志(二二)
  4. VS2010实现opencv基于DCT的图像压缩
  5. (Spring)概述及IOC
  6. Oracle创建pfile spfile 文件及其恢复
  7. 设置按钮中的图片的旋转,并且旋转之后不变形
  8. 电路结构原理_精密半波、全波整流电路结构原理图解
  9. 【6.13-6.27推荐享大礼】华为云·云享专家推荐有礼活动火热进行中……
  10. BN、LN、IN、GN和SN
  11. jpeglib的jpeg_finish_compress函数疑似越界
  12. 安卓玩机教程---全机型安卓4----安卓12 框架xp edx lsp安装方法
  13. 树莓派控制舵机和步进电机
  14. python的文本编辑器atom_python的各种编辑器-PyScripter、pycharm 、atom、vscode、Sublime Text等等...
  15. java批量添加注解到所有业务接口
  16. 关于某normal大学数据库登录的一个尝试
  17. 只有迷信的人,没有迷信的知识
  18. C语言中正弦函数定义域,三角函数定义域和值域
  19. 【历史上的今天】4 月 14 日:Ruby 之父诞生;GDPR 首次颁布;Lindows 更名为 Linspire
  20. imx6 添加matrix keypad

热门文章

  1. 基于JS+HTML实现的纯前端天气预报实时查询系统
  2. 【教程】在UEFI启动方式下,通过GRUB2引导,直接从硬盘ISO文件安装Windows10和Ubuntu双系统
  3. 文字排版--字体/--字号、颜色
  4. Vue-Quill-Editor 从新设置字体和字号,解决回显时不显示空格
  5. Appium自动化测试框架的原理、安装以及使用
  6. 2022icpc沈阳站感想
  7. 《应用非线性控制》第二章——相平面分析 2.1 相平面分析的概念
  8. 超表面案例仿真(1)——自旋霍尔效应 超表面 fdtd仿真
  9. 数据结构--顺序表的使用
  10. webpack初始化vue项目 配置整理(一)