• 【分享】用Canvas实现画板功能


      

    前言

      PC端测试:QQ浏览器全屏绘画完成、缩小时内容会被清空,切换背景颜色内容会被重置,其他暂无发现;

    手机端测试:微信内置浏览器不通过;Safari 浏览器使用画笔时没固定页面会有抖动效果,使用橡皮擦功能 能绘制出点线(黑人问号脸出现),保存成图片时需要手动保存(能理解),撤销操作?(em 黑人问号再次出现);

    手机机型系统:iphone 7p , ios 12

      写的有意思,就搬来了重要内容供参考

      原文地址: https://juejin.im/post/5c7bf106e51d454b47558882

      补充了一些自己的想法在其中o(* ̄▽ ̄*)ブ~~~

      如果有什么错误地方请指出,感谢思密达~~

    一、介绍

      名称: 智绘画板

      技术栈: HTML5,CSS3,JavaScript,移动端

      功能描述:

      • 支持PC端和移动端在线绘画功能

      • 实现任意选择画笔颜色、调整画笔粗细以及橡皮檫擦除等绘画功能

      • 实现在线画板的本地保存功能

      • 支持撤销和返回操作

      • 自定义背景颜色

    二、项目效果展示

       项目地址

       预览地址

      注:下面实现项目效果主要是关于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美化界面

        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(){
            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,实现被激活的效果。
        • eraserEnabledtrue,移动鼠标用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,让我想起“代码千万行,注释第一行;编程不规范,同事两行泪 ”,还是按照文档操作规范操作好,

        真香!!!

  • 相关阅读:
    【荐】说说CSS Hack 和向后兼容
    【阮一峰】深入研究URL编码问题及JavaScript相应的解决方案
    什么是H标签?H1,H2,H3标签?以及和strong标签使用的方法及重要性
    实用框架(iframe)代码
    数据库(SQLITE3函数总结): sqlite3_open, sqlite3_exec, slite3_close,sqlite3_prepare_v2,sqlite3_column_text,
    BZOJ 3110 ZJOI 2013 K大数查询 树套树(权值线段树套区间线段树)
    c++中基本的语法问题
    RIP协议两个版本号对不连续子网的支持情况实验
    getChars的使用方法
    ios8中百度推送接收不到
  • 原文地址:https://www.cnblogs.com/anniey/p/10470966.html
Copyright © 2020-2023  润新知