手把手教你实现一个canvas智绘画板的方法
本文主要介绍:
- 项目介绍
- 项目效果展示
- 一步步实现项目效果
- 踩坑
一、项目介绍
名称:智绘画板
技术栈:html5,css3,javascript,移动端
功能描述:
- 支持pc端和移动端在线绘画功能
- 实现任意选择画笔颜色、调整画笔粗细以及橡皮檫擦除等绘画功能
- 实现在线画板的本地保存功能
- 支持撤销和返回操作
- 自定义背景颜色
二、项目效果展示
预览图
pc端的预览图:
移动端的预览图:
看完上面的预览图和体验过 智绘画板 觉得还可以的,记得点个赞哦,不管你是否十分激动,反正我是挺激动的,毕竟自己实现出现的项目效果,挺自豪的,说了一堆废话,下面就可以动起手来敲代码,实现自己想要的效果!!!
注:下面实现项目效果主要是关于javascript方面的,下面仅仅是提供 实现思路的代码 , 并非全部代码 。
三、一步步实现项目效果
(一)分析页面
通过 用例图 ,我们知道用户进入我们这个网站有哪些功能?
用户可以进行的操作:
- 画画
- 改变画笔的粗细
- 切换画笔的颜色
- 使用橡皮檫擦除不想要的部分
- 清空画板
- 将自己画的东西保存成图片
- 进行撤销和重做操作
- 切换画板背景颜色
- 兼容移动端(支持触摸)
(二)进行html布局
我书写html的同时,引入了css文件和js文件
<!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美化界面
css代码可以根据个人习惯进行美化界面,所以这里就不写css的代码了,大家可以直接看 项目代码 或者从开发者工具中审查元素观看。如果有问题可以私聊我,我觉得问题不大。
(四)使用js实现项目的具体功能
1.准备工作
首先,准备个容器,也就是画板了,前面的html已经书写好这个容器,这里纯属是废话。
<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(){ let pagewidth = document.documentelement.clientwidth; let pageheight = document.documentelement.clientheight; canvas.width = pagewidth; canvas.height = pageheight; } // 在窗口大小改变之后,就会触发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){//要使用eraser context.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.实现改变背景颜色的功能
实现思路:
- 获取相应的元素节点。
- 给每一个class为bgcolor-item的标签添加点击事件,当点击事件触发时,改变背景颜色。
- 点击设置背景颜色的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.实现改变画笔粗细的功能
实现思路:
- 实现让设置画笔的属性的对话框出现。
- 获取相应的元素节点。
- 当input=range的元素发生改变的时候,获取到的值赋值给lwidth。
- 然后设置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.实现改变撤销和重做的功能
实现思路:
- 保存快照:每完成一次绘制操作则保存一份 canvas 快照到
canvashistory
数组(生成快照使用 canvas 的todataurl()
方法,生成的是 base64 的图片); - 撤销和反撤销:把
canvashistory
数组中对应索引的快照使用 canvas 的drawimage()
方法重绘一遍; - 绘制新图像:执行新的绘制操作时,删除当前位置之后的数组记录,然后添加新的快照。
let undo = document.getelementbyid("undo"); let redo = document.getelementbyid("redo"); // ... 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.兼容移动端
实现思路:
- 判断设备是否支持触摸
-
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(); } }
问题2:当绘制线条宽度比较小的时候还好,一旦比较粗就会出现问题
解决办法:看一下文档,得出方法,只需要简单修改一下 绘制线条的代码 就行
// 画线函数 function drawline(x1,y1,x2,y2){ context.beginpath(); context.linewidth = lwidth; //-----加入----- // 设置线条末端样式。 context.linecap = "round"; // 设定线条与线条间接合处的样式 context.linejoin = "round"; //-----加入----- context.moveto(x1,y1); context.lineto(x2,y2); context.stroke(); context.closepath(); }
问题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:出现一个问题就是清空之后,重新画,然后出现原来的画的东西
这个嘛,问题不大,只不过是我漏写context.beginpath(); ,也花了一点时间在上面解决bug,让我想起“代码千万行,注释第一行;编程不规范,同事两行泪 ”,还是按照文档操作规范操作好,真香!!!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 下拉复选框
下一篇: 理想与现实的差距,在女人面前展现的更明显