前言
PC端测试:QQ浏览器全屏绘画完成、缩小时内容会被清空,切换背景颜色内容会被重置,其他暂无发现;
手机端测试:微信内置浏览器不通过;Safari 浏览器使用画笔时没固定页面会有抖动效果,使用橡皮擦功能 能绘制出点线(黑人问号脸出现),保存成图片时需要手动保存(能理解),撤销操作?(em 黑人问号再次出现);
手机机型系统:iphone 7p , ios 12
写的有意思,就搬来了重要内容供参考
原文地址: https://juejin.im/post/5c7bf106e51d454b47558882
补充了一些自己的想法在其中o(* ̄▽ ̄*)ブ~~~
如果有什么错误地方请指出,感谢思密达~~
一、介绍
名称: 智绘画板
技术栈: HTML5,CSS3,JavaScript,移动端
功能描述:
-
-
支持PC端和移动端在线绘画功能
-
实现任意选择画笔颜色、调整画笔粗细以及橡皮檫擦除等绘画功能
-
实现在线画板的本地保存功能
-
支持撤销和返回操作
-
自定义背景颜色
-
二、项目效果展示
注:下面实现项目效果主要是关于JavaScript方面的,下面仅仅是提供实现思路的代码,并非全部代码。
三、一步步实现项目效果
(一)分析页面
通过用例图,我们知道用户进入我们这个网站有哪些功能?
用户可以进行的操作:
-
-
-
画画
-
改变画笔的粗细
-
切换画笔的颜色
-
使用橡皮檫擦除不想要的部分
-
清空画板
-
将自己画的东西保存成图片
-
进行撤销和重做操作
-
切换画板背景颜色
-
兼容移动端(支持触摸)
-
-
(二)进行HTML布局
智绘画板 选择背景颜色:
笔大小
笔颜色
不透明度
(三)用CSS美化界面
css代码优化界面
(四)使用JS实现项目的具体功能
1.准备工作
首先,准备个容器。
<canvas id="canvas"></canvas>
然后初始化jslet 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加上,这里我们明确不考虑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属性,告诉浏览器不将页面进行缩放,页面宽度=用户设备屏幕宽度
/*页面宽度=移动宽度 :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,让我想起“代码千万行,注释第一行;编程不规范,同事两行泪 ”,还是按照文档操作规范操作好,
真香!!!