今天完成了一个在线的画板,现在来总结一下。
一、项目名称:
彩虹画板
二、用途
支持在pc端和移动端在线绘画和签字功能
三、技术栈
html5 , css3 , javascript
四、功能模块
- 画笔模块、
- 选择画笔大小功能、
- 选择画笔颜色功能、
- 橡皮擦功能、
- 清屏功能、
- 保存图片功能、
- 撤销功能、
- 反撤销功能
五、项目展示
pc端图片
移动端图片
下面讲讲项目的实现 html和css部分我就忽略一下,主要总结一下javascript思路
六、代码实现
上面的第四功能模块,就是画板的基本需求。本来我刚刚做的是一个画笔,只能实现在上面画画,签字的基本需求,后面还是完善了这个项目,尽管一波三折。
1、html代码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>彩虹画板</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas"></canvas>
<div class="tools">
<ul class="container">
<li><button class="save" id="save" title="保存"></button></li>
<li><button class="brush active" id="brush" title="画笔"></button></li>
<li><button class="eraser" id="eraser" title="橡皮擦"></button></li>
<li><button class="clear" id="clear" title="清屏"></button></li>
<li><button class="revocation" id="revocation" title="撤回"></button></li>
<li><button class="back_revocation" id="back_revocation" title="取消撤回"></button></li>
</ul>
</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>
</div>
<script src="main.js"></script>
</body>
</html>
2、css部分代码实现
input[type=range]{
-webkit-appearance: none;/*去除系统默认滑动条样式*/
width: 130px;
height: 24px;
outline: none;
}
input[type='range']::-webkit-slider-runnable-track{ /*自定义滑动控件轨道*/
background-color: #DBDBDB;
height: 4px;
border-radius: 5px;
}
input[type='range']::-webkit-slider-thumb { /*自定义滑动控件滑块*/
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #FF4081;
cursor: pointer;
margin-top: -4px;
}
type="range" 直接显示一个滑块控件,可拖动。其他的css样式,我就不多赘述了,可以看源码。
3、js代码具体实现
- 将画板做成全屏,需要获取文本的长宽,不能获取body的,body的高是靠内容撑起的
- 当我们添加
button
的时候,会占用我们屏幕的位置,而画笔会点击的时,内容会出现在下方,这是踩坑的地方,会在踩坑部分分享。
let pageWidth = document.documentElement.clientWidth;
let pageHeight = document.documentElement.clientHeight;
- 接下来我要在画板上画上图案,比如点,我看了mdn上面有个基本用法
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
ctx.fillStyle = "rgb(0,0,0)";
ctx.fillRect (10, 10, 55, 50);
- 结果是个方形,那怎么变圆呢?
ctx.beginPath();
ctx.arc(20, 20, 10, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
我们应该画在屏幕上,而不是让它自己生成。
(1)实现画点
- 画点实现思路:监听鼠标,当我们点击时可以画点,
onmousedown
- 并获取屏幕坐标
canvas.onmousedown = (e) => {
//画方
//ctx.fillRect(e.clientX -5,e.clientY -5,10,10);
//画实圆
ctx.beginPath();
ctx.arc(e.clientX, e.clientY, 10, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
}
(2)实现画线
- 画线实现思路:监听鼠标, 用
drawLine()
方法把记录的数据画出来。 - 初始化当前画板的画笔状态,
draw = false
。 - 当鼠标按下时(
mousedown
),把draw
设为true
,表示正在画。把鼠标点记录下来。 - 当按下鼠标的时候,鼠标移动(
mousemove
)就 把点记录 下来并画出来。 如果鼠标移动过快,浏览器跟不上绘画速度,点与点之间会出现间隙,需要将每次移动坐标存储在lastPlace
,将lastPlace
坐标作为画线的起点坐标,所以我们需要将画出的点用线连起来(moveTo
)(lineTo()
)。 - 鼠标松开的时候(
mouseup
),把draw
设为false
。
//颜色
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
//长度
ctx.lineWidth = 10;
ctx.lineCap = "round";
let painting = false;
let lastPlace;
//手机触摸
let isTouchDevice = "ontouchstart" in document.documentElement;
if (isTouchDevice) {
//获取手机触屏第一次的坐标
let x = e.touches[0].clientX;
let y = e.touches[0].clientY;
// 手指点击
canvas.ontouchmove = (e) => {
//画圆
drawCircle(x,y,radius)
lastPlace =[x,y];
}
//手指移动
canvas.ontouchmove = (e) => {
if (draw===true) {
//画线
drawLine(last[0], last[1], x, y);
lastPlace = [x, y];
}
}
//手指离开
canvas.ontouchend = (e) => {
draw = false;
}
} else {
//鼠标放下为ture,开始画点
let x = e.clientX;
let y = e.clientY;
canvas.onmousedown = (e) => {
draw = true;
drawCircle(x,y,radius)
//鼠标第一次点击的位置
lastPlace = [x, y];
}
canvas.onmousemove = (e) => {
if (draw===true) {
//调用函数 上一点连接下一点
drawLine(last[0], last[1],x, y);
//将这次位置确定为下次的起点
lastPlace = [x, y];
}
}
//鼠标离开 停止画画
canvas.onmouseup = () => {
painting = false;
}
}
//画线
function drawLine(x1, y1, x2, y2) {
ctx.beginPath();
// 设置线条末端样式。
context.lineCap = "round";
// 设定线条与线条间接合处的样式
context.lineJoin = "round";
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.closePath();
}
//画点函数
function drawCircle(x,y,radius){
// 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
ctx.beginPath();
// 画一个以(x,y)为圆心的以radius为半径的圆弧(圆),
// 从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。
ctx.arc(x,y,lWidth/2,0,Math.PI*2);
// 通过填充路径的内容区域生成实心的图形
ctx.fill();
// 闭合路径之后图形绘制命令又重新指向到上下文中。
ctx.closePath();
}
(3)实现橡皮檫的功能
- 获取橡皮擦元素
- 设置橡皮擦初始状态,
iseEraser = false
。 - 监听橡皮擦
click
事件,点击橡皮擦,改变橡皮擦状态,iseEraser = true
,并且切换class,实现 被激活 的效果。 iseEraser
为true
时,移动鼠标用ctx.clearRect()
实现了 橡皮檫。- mdn
clearRect()
方法,可以实现橡皮擦功能,但是橡皮擦的形状是方形,我们习惯了圆形,这是一个踩坑的地方,踩坑部分我会另外讲,网上找了很多方案最后实现了。下面是mdn的方法,橡皮擦是方形,且滑动不连贯。
let eraser = document.getElementById("eraser");
let iseEraser = false;
monitorToUser();一定要调用函数
//监听鼠标 手机触屏事件 函数
function monitorToUser() {
//....前面代码我就省略了,可以在源码上看
//适配手机触摸
let isTouchDevice = "ontouchstart" in document.documentElement;
if (isTouchDevice) {
//...
}else{
//PC
//鼠标放下为ture
canvas.onmousedown = (e) => {
let x = e.clientX;
let y = e.clientY;
draw = true;
if (iseEraser) {//要使用eraser
ctx.clearRect(x - lWidth/2, y - lWidth/2, lWidth, lWidth);
}else{
drawCircle(x,y,radius);
lastPlace =[x, y];
}
}
canvas.onmousemove = (e) => {
let x = e.clientX;
let y = e.clientY;
if (!draw) { return }
if (iseEraser) {
ctx.clearRect(x - lWidth/2, y - lWidth/2, lWidth, lWidth);
} else {
let newPlace = [x, y];
drawLine(lastPlace[0], lastPlace[1], x, y);
lastPlace =newPlace;//这次作为上次的位置
}
}
//鼠标松开
canvas.onmouseup = (e) => {
draw = false;
}
}
}
// 橡皮檫功能
eraser.onclick = function(){
iseEraser = true;
eraser.classList.add('active');
brush.classList.remove('active');
}
(4)实现清屏
- 获取元素节点
- 点击清空按钮清空canvas画布
- 原理就是
ctx.clearRect()
let reSetCanvas = document.getElementById("clear");
// 实现清屏
reSetCanvas.onclick = function(){
ctx.clearRect(0,0,canvas.width,canvas.height);
}
(5)实现保存下载图片
- 获取元素节点
- 点击按钮保存下载图片
- 获取canvas.toDataURL并设置类型'image/png'
- 创建一个a标签,并插入页面
- a标签href等于canvas.toDateURL,并添加download属性
- target另外打开页面
- 点击保存按钮,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)实现改变画笔粗细
- 获取相应的元素节点
- 实现让设置画笔的属性的对话框出现。
- 当
iseEraser = false;
禁用橡皮擦功能, - 默认画笔
isPenDetail = false;
初始化画笔粗细let lWidth = 5
- 点击画笔移除橡皮擦样式,添加选中画笔出现画笔属性
- 滑动input=range的元素发生改变的时候,获取到的值赋值给lWidth。
- 相应的span获取到的值,使用
scale()
展现相应的缩放 - 然后设置ctx.lineWidth = lWidth。
let brush = document.getElementById('brush');
let range1 = document.getElementById('range1');
let thickness = document.getElementById("thickness");
//初始化画笔
let isPenDetail = false;
//初始画笔粗细
let lWidth = 5;
//点击画笔
brush.onclick = function () {
iseEraser = false;
eraser.classList.remove('active');
brush.classList.add('active');
if (!isPenDetail) {
penDetail.classList.add('active');
} else {
penDetail.classList.remove('active');
}
isPenDetail = !isPenDetail;
}
// 画线函数
function drawLine(x1,y1,x2,y2){
// ...
ctx.lineWidth = lWidth;
// ...
}
//改变画笔粗细
range1.onchange = function () {
thickness.style.transform = 'scale('+(parseInt(range1.value))+')';
lWidth = parseInt(range1.value*2);
}
(7).实现改变画笔颜色
- 获取相应的元素节点
- 给每一个class为
color-item
的标签添加点击事件,当点击事件触发时,改变背景颜色。 - 点击设置背景颜色的div之外的地方,实现隐藏那个div。
let ColorPen = document.getElementsByClassName("color-item");
changePenColor();
//改变画笔颜色
function changePenColor() {
for (var i = 0; i < ColorPen.length; i++) {
ColorPen[i].onclick = function () {
for (var j = 0; j < ColorPen.length;j++) {
ColorPen[j].classList.remove('active');
this.classList.add('active');
activeColor = this.style.backgroundColor;
ctx.fillStyle = activeColor;
ctx.strokeStyle = activeColor;
}
}
}
}
(8)实现改变撤销和反撤销
- 获取相应的元素节点
- 定义一个
canvasHistory
记录每次画笔和橡皮的操作 step
记录鼠标松开每一步的步数- 保存快照:每完成一次绘制操作则保存一份 canvas 快照到
canvasHistory
数组(生成快照使用 canvas 的toDataURL()
方法,生成的是 base64 的图片) - 点击撤回按钮,
new Image()
新建画布,相应的canvasPic.src
会得到canvasHistory[step]
需要回索引那个快照 - 执行新的绘制操作时,删除当前位置之后的数组记录,然后添加新的快照。
ctx.drawImage
会重新绘画 - 反撤回同理
let revocation = document.getElementById("revocation");
let back_revocation = document.getElementById("back_revocation");
//监听鼠标 手机触屏事件 函数
function monitorToUser() {
//......省略代码
//鼠标松开
canvas.onmouseup = (e) => {
draw = false;
record_operation();
}
}
// 实现撤销的功能
let canvasHistory = [];
let step = -1;
//记录每一步画画的操作函数
function record_operation(){
step++;
if(step < canvasHistory.length){
canvasHistory.length = step;
}
// 添加新的绘制记录到历史记录
canvasHistory.push(canvas.toDataURL());
if(step > -1){
revocation.classList.add('active');
}
}
//撤回方法
function canvasRevocation(){
if(step > 0){
step--;
let canvasPic = new Image();
canvasPic.src = canvasHistory[step];
canvasPic.onload = ()=> {
ctx.drawImage(canvasPic, 0, 0);
}
revocation.classList.add('active');
back_revocation.classList.add('active');
}else{
revocation.classList.remove('active');
alert('已经无法撤回');
}
}
//取消撤回方法
function canvas_back_revocation(){
if(step < canvasHistory.length - 1){
step++;
let canvasPic = new Image();
canvasPic.src = canvasHistory[step];
canvasPic.onload = function () {
ctx.drawImage(canvasPic, 0, 0);
}
}else {
back_revocation.classList.remove('active')
alert('已经是最新的记录了');
}
}
revocation.onclick = ()=>{
canvasRevocation();
}
back_revocation.onclick=()=>{
canvas_back_revocation();
}
(9)close关闭画笔属性
获取相应的元素节点
let closeBtn = document.getElementsByClassName('closeBtn');
//close功能
for (let i = 0; i < closeBtn.length; i++) {
closeBtn[i].onclick = function (e) {
let btnParent = e.target.parentElement;
btnParent.classList.remove('active');
}
}
(10)兼容移动端
- 判断设备是否支持触摸
true
,则使用touch
事件;false
,则使用mouse
事件
// ...
let isTouchDevice = "ontouchstart" in document.documentElement;
if (isTouchDevice) {
// 使用touch事件
anvas.ontouchstart = function (e) {
// 开始触摸
}
canvas.ontouchmove = function (e) {
// 开始滑动
}
canvas.ontouchend = function () {
// 滑动结束
}
}else{
// 使用mouse事件
// ...
}
// ...
7、踩坑
问题1:在电脑上对浏览器的窗口进行改变,添加别的组件,画板不会自适应
解决办法:
onresize响应事件处理中,获取到的页面尺寸参数是变更后的参数 。
当窗口大小发生改变之后,重新设置canvas的宽高,简单来说,就是窗口改变之后,给canvas.width和canvas.height重新赋值。
function autoSetSize(){
canvasSetSize();
function canvasSetSize(){
let pageWidth = document.documentElement.clientWidth;
let pageHeight = document.documentElement.clientHeight;
canvas.width = pageWidth;
canvas.height = pageHeight;
}
window.onresize = function(){
canvasSetSize();
}
}
问题2:当绘制线条宽度比较小的时候还好,一旦比较粗就会出现问题
解决办法:添加绘制线条的代码
// 设置线条末端样式。
context.lineCap = "round";
// 设定线条与线条间接合处的样式
context.lineJoin = "round";
问题3:如何实现圆形的橡皮檫?
解决办法:入了剪辑区域这个强大的功能,也就是clip()方法
//橡皮圆点
function clearCircle(x, y, radius) {
ctx.save()
ctx.beginPath()
ctx.arc(x,y,lWidth/2,0,2*Math.PI);
ctx.clip()
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.restore();
ctx.closePath();
}
问题4:如何实现圆形移动擦除呢?
- 上面那段代码就实现了圆形区域的擦除,移动擦除不连贯怎么办?
- 如果是实现画图功能的话,就可以直接通过lineTo把两点之间连接起来再绘制,但是擦除效果中的剪辑区域要求要是闭合路径,如果是单纯的把两个点连起来就无法形成剪辑区域了。
- 然后网上找到了计算的方法,算出两个擦除区域中的矩形四个端点坐标来实现,也就是下图中的红色矩形;
- 因为可以知道两个剪辑区域连线两个端点的坐标,又知道我们要多宽的线条,矩形的四个端点坐标就变得容易求了
- x1,y1,x2,y2就是2个端点从而求出了四个端点的坐标。这样一来,剪辑区域就是圈加矩形
function moveHandler(x1,y1,x2,y2){
//获取两个点之间的剪辑区域四个端点
let asin = lWidth/2*Math.sin(Math.atan((y2-y1)/(x2-x1)));
let acos = lWidth/2*Math.cos(Math.atan((y2-y1)/(x2-x1)))
let x3 = x1+asin;
let y3 = y1-acos;
let x4 = x1-asin;
let y4 = y1+acos;
let x5 = x2+asin;
let y5 = y2-acos;
let x6 = x2-asin;
let y6 = y2+acos;
//保证线条的连贯,所以在矩形一端画圆
clearCircle(x2, y2, radius)
//清除矩形剪辑区域里的像素
ctx.save()
ctx.beginPath()
ctx.moveTo(x3,y3);
ctx.lineTo(x5,y5);
ctx.lineTo(x6,y6);
ctx.lineTo(x4,y4);
ctx.closePath();
ctx.clip();
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.restore();
ctx.closePath();
}
问题5:如何兼容移动端?
(1)添加meta标签
因为浏览器初始会将页面现在手机端显示时进行缩放,因此我们可以在meta标签中设置meta viewport属性,告诉浏览器不将页面进行缩放,页面宽度=用户设备屏幕宽度
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
(2)在移动端几乎使用的都是touch事件,与PC端不同
由于移动端是触摸事件,所以要用到H5的属性touchstart/touchmove/touchend,但是PC端只支持鼠标事件
let x = e.touches[0].clientX;
let y = e.touches[0].clientY;
问题6:出现一个问题就是清空之后,重新画,然后出现原来的画的东西
解决方法:
// 实现清屏
reSetCanvas.onclick = function(){
ctx.clearRect(0,0,canvas.width,canvas.height);
canvasHistory=[];
}
但是操作清屏之后,不能撤销清屏这个动作
问题7:出现一个问题就是撤销的时候,橡皮擦动作和步数可以执行,画笔不能撤销
解决过程:
- 经过log调试大法,发现我没有设置canvas背景色,保存下来的图片是黑色。
- 撤销是的快照已经实现了,但是没有重画。
解决办法:
重新设置canvas背景颜色
setCanvasBg('white');
// 实现清屏
reSetCanvas.onclick = function(){
ctx.clearRect(0,0,canvas.width,canvas.height);
setCanvasBg('white');
canvasHistory=[];
}
// 重新设置canvas背景颜色
function setCanvasBg(color) {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
}
关于canvas画板过程就分享到这里,有需要改进的地方可以留言哦