推荐使用 PC、平板等大屏幕访问,以体验完整版功能

    H5 canvas 实现 涂鸦画图


    这篇文章介绍下如何用 H5 的 canvas API 实现涂鸦画图功能,也作为陶笛日记的乐谱涂鸦笔记功能的一份总结。

    关于 canvas 的文档,可以参考 Canvas_API

    H5 的 canvas 标签

    首先,我们在 html 中声明一个 canvas 标签:<canvas> </canvas>,就能在浏览器中得到一块画板。

    然后设置 style,让画板层以透明背景,覆盖在乐谱图片上方:style="background: transparent; position: absolute;"

    与 canvas API 的邂逅

    通过 document.getElementsByTagName('canvas') 之类的操作,我们能得到画板的的 dom 对象,假设对象名为 element 或者 ele。

    要对画板进行操作,首先要新建一个上下文:let ctx = element.getContext('2d')

    接下来就可以设置画笔的一些属性:

    // 线条的渲染方式
    ctx.lineCap = 'round'
    ctx.lineJoin = 'round'
    
    // 宽度
    ctx.lineWidth = 1
    
    // 颜色
    ctx.strokeStyle = '#ff2600'
    

    有了画笔之后,可以通过 ctx.beginPath() 把画笔的笔头靠到画板上。

    ctx.lineTo(X, Y) 把笔头沿直线移动到某个坐标。坐标系以左上角为原点,向左为横轴正方向,向下为纵轴正方向,像素为单位长度。

    ctx.stroke() 在画布上渲染出这条直线。这个操作自然界的笔是不需要的,毕竟自然界的笔也没有撤销操作。

    ctx.closePath() 抬起画笔,让笔头离开画板。

    上面的几个步骤,用自然界的笔做了个过程对比。其实是一个彻底错误的示范、错误地理解画图接口的思考方式。实际上这几个步骤,是绘制 Path 的一个简略过程,因为没有用到全套完整绘制 Path 的功能,所以错误的理解好像也还能说得通。不止是 h5 的 canvas,其他所有与图形相关的编程接口:java的、python的、svg的都有一套相似的设计思维。正确的 api 解释,请一定要点开 Canvas_API 文档看看啊。

    橡皮擦:清除一块矩形区域上的笔迹。ctx.clearRect(x, y, w, h) 四个参数分别表示矩形的 左上角横坐标、左上角纵坐标、宽度、长度。

    上面的接口还不足以支撑完整的画板功能,但是已经足够开始动手写核心 demo 了。下面以鼠标事件、触摸事件为例,给出两个代码示例。

    鼠标事件

    需要用到的事件有:onmousedownonmousemoveonmouseup

    function onMouseDown(ele, event) {
        let ctx = ele.getContext('2d')
    
        if (!isEraser) {
            ctx.lineCap = 'round'
            ctx.lineJoin = 'round'
            ctx.lineWidth = 1
            ctx.strokeStyle = '#ff2600'
            ctx.beginPath()
        }
    
        ele.onmousemove = function(event){
            if (isEraser) {
                ctx.clearRect(event.layerX - eraserSizeHalf,
                             event.layerY - eraserSizeHalf,
                             eraserSize, eraserSize)
            } else {
                ctx.lineTo(event.layerX, event.layerY);
                ctx.stroke();
            }
        }
    
        ele.onmouseup = function(event){
            if (!isEraser) {
                ctx.closePath();
            }
            ele.onmousemove = null;
            ele.onmouseup = null;
        }
    }
    

    触屏事件

    需要用到的事件有:ontouchstartontouchmoveontouchend

    与鼠标不同的主要还有:

    1. 获取笔头当前在画板上的位置的方式
    2. 测试手头上有的各种浏览器的之后,加上了几处 return 语句,我也不知道当时是怎么想的。
    function onTouchStart(ele, event) {
        let ctx = ele.getContext('2d')
    
        if (!isEraser) {
            ctx.lineCap = 'round'
            ctx.lineJoin = 'round'
            ctx.lineWidth = 1
            ctx.strokeStyle = '#ff2600'
            ctx.beginPath()
        }
        
        ele.ontouchmove = function(event){
            if (event.touches.length > 1) {
                ele.ontouchend
                return true
            }
            let x = event.layerX
            let y = event.layerY
            if (!x || !y) {
                let eleBox = ele.getBoundingClientRect()
                x = event.touches[0].clientX - eleBox.left
                y = event.touches[0].clientY - eleBox.top
            }
            if (isEraser) {
                ctx.clearRect(x - eraserSizeHalf,
                              y - eraserSizeHalf,
                              eraserSize, eraserSize)
            } else {
                ctx.lineTo(x, y);
                ctx.stroke();
            }
            return false
        }
    
        ele.ontouchend = function(event){
            if (!isEraser) {
                ctx.closePath();
            }
            ele.ontouchmove = null;
            ele.ontouchend = null;
            return false
        }
        return false
    }
    

    导出/导入 图片

    ele.toDataURL() 可以得到 base64 编码的字符串形式的图片。当然,因为实际不是 base64,是 DataURL,所以带有 data:image/png;base64, 前缀。

    导入图片需要用异步的方式:

    let img = new Image()
    img.onload = function() {
        ctx.drawImage(img, 0, 0, element.width, element.height);
    }
    img.src = 'xxx'
    

    坑1:canvas 元素的宽和高

    前文讲到绘制 Path 的时候,提到 canvas 元素有一个坐标系,以像素为单位长度。但是这个像素是元素内部世界独立的。

    另一个角度讲,不要试图用 CSS 去改变 canvas 元素的宽高,用 CSS 只是在缩放图片,图片内部的所谓单个像素的长短大小也会被缩放。

    应该使用 canvas 标签的 width 和 height 属性来设置画板大小。

    如果需要动态大小怎么办?element.width = 300 可以修改宽度,但是会清空画板。可以尝试在修改前后导入、导出一下图片数据。

    坑2:获取鼠标位置

    搜索引擎的答案五花八门。过去太久,已经记不清示例代码里获取笔头位置的代码是怎么来的了。