《canvas 彩虹画板记录》

1,790 阅读7分钟

今天完成了一个在线的画板,现在来总结一下。

一、项目名称:

彩虹画板

二、用途

支持在pc端和移动端在线绘画和签字功能

三、技术栈

html5 , css3 , javascript

四、功能模块

  • 画笔模块、
  • 选择画笔大小功能、
  • 选择画笔颜色功能、
  • 橡皮擦功能、
  • 清屏功能、
  • 保存图片功能、
  • 撤销功能、
  • 反撤销功能

五、项目展示

源码地址 预览地址

pc端图片

彩虹画板pc.png 移动端图片

彩虹画板移动端.jpg

下面讲讲项目的实现 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() 实现了 橡皮檫。
  • mdnclearRect()方法,可以实现橡皮擦功能,但是橡皮擦的形状是方形,我们习惯了圆形,这是一个踩坑的地方,踩坑部分我会另外讲,网上找了很多方案最后实现了。下面是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画板过程就分享到这里,有需要改进的地方可以留言哦