1

之前学了 canvas,现在线上签字那么流行,就想着自己实现一个玩玩

打算实现一个包含以下功能的写字板

  • 使用鼠标写字
  • 调整画笔粗细
  • 修改画笔颜色
  • 撤销功能
  • 清空画布的功能
  • 生成图片,下载签名

静态结构

静态结构很简单,就直接列出来不做解释了

HTML

<div>
    <div id="settings">
        <div>
            字体大小:
            <select id="fontSize"></select>
        </div>
        <div>
            字体颜色:
            <input type="color" id="fontColor" />
        </div>
        <button id="undo">撤销</button>
        <button id="clear">清空画布</button>
        <button id="download">生成图片</button>
    </div>
    <canvas width="800" height="300" id="tablet"></canvas>
</div>

CSS

#settings {
    display: flex;
    margin: auto;
    margin-top: 100px;
    line-height: 27px;
    justify-content: center;
    gap: 20px;
}

#tablet {
    display: block;
    margin: auto;
    margin-top: 10px;
    background-color: #eee;
}

gap 属性可能有人不认识,可以设置 flex/grad 布局子元素的间隔

静态页面就是这么个样子,字体大小的选项是用 js 生成的

image.png

接下来我们来写 JS

功能实现

初始化 canvas

我们先初始化 canvas 和其 2d 上下文 ctx

设置线宽为 5,线的末端和连接处均为圆角

const tablet = document.getElementById('tablet')
const ctx = tablet.getContext('2d')
ctx.lineWidth = 5
ctx.lineCap = 'round'
ctx.lineJoin = 'round'

字体大小

然后我们生成字体大小的选项,允许 1-16px,默认为 5px

const fontSize = document.getElementById('fontSize')
for (let i = 1; i <= 16; i++) {
    const option = document.createElement('option')
    option.value = i
    option.innerText = i + 'px'
    fontSize.appendChild(option)
    if (i == 5) {
        option.selected = true
    }
}
fontSize.addEventListener('change', (e) => {
    ctx.lineWidth = e.target.value
})

字体颜色

从 input 读取颜色,设置线色

const fontColor = document.getElementById('fontColor')
fontColor.addEventListener('change', (e) => {
    ctx.strokeStyle = e.target.value
})

写字

写字分为三部分

  • 鼠标落下:开始写字,对应 mousedown 事件
  • 鼠标移动:正在写字,对应 mousemove 事件
  • 鼠标抬起:结束写字,对应 mouseup 事件

写字无非就是许多点连成的线,在写字过程中,记录鼠标上一点的位置,将其与当前点连起来

我个人习惯在鼠标按下时注册事件,鼠标松开时清除事件;还有一种实现方式是用一个变量控制 mouseover 是否执行

mouseup 事件绑定在 document 上,又加了一个 mouseenter 事件,使得鼠标离开画布再回来可以继续写。

let prex, prey // 记录上一点的位置
const mousedown = (e) => {
    if (e.button != 0) return // 不是左键直接返回
    prex = e.offsetX
    prey = e.offsetY
    ctx.beginPath() // 开始路径
    ctx.moveTo(prex, prey)
    // 注册事件
    tablet.addEventListener('mousemove', mousemove)
    document.addEventListener('mouseup', mouseup)
    tablet.addEventListener('mouseenter', mouseenter)
}
const mousemove = (e) => {
    ctx.lineTo(e.offsetX, e.offsetY)
    ctx.stroke()
}
const mouseup = () => {
    // 关闭路径
    ctx.closePath()
    // 清除事件
    tablet.removeEventListener('mousemove', mousemove)
    document.removeEventListener('mouseup', mouseup)
    tablet.removeEventListener('mouseenter', mouseenter)
}
const mouseenter = (e) => {
    // 从外部回到画布,改变上一点的位置,继续书写
    prex = e.offsetX
    prey = e.offsetY
    ctx.moveTo(prex, prey)
}
tablet.addEventListener('mousedown', mousedown)

28d6e1b310d04ba5916bbe8fcf19cb5e_tplv-k3u1fbpfcp-watermark.png

撤销

想要实现撤销功能,就要保存之前的状态,可以在开始写字时保存此时的画布状态,在需要撤销时恢复

考虑到 imageData 内容较大,我们这里只实现了一步的撤销功能

let imageData // 图片数据
const mousedown = (e) => {
    imageData = ctx.getImageData(0, 0, tablet.width, tablet.height)
    ……
}

const undo = document.getElementById('undo')
undo.addEventListener('click', () => {
    ctx.putImageData(imageData, 0, 0)
})

清空画布

清空画布很简单,clearRect 就好了

const clear = document.getElementById('clear')
clear.addEventListener('click', () => {
    ctx.clearRect(0, 0, tablet.width, tablet.height)
})

下载图片

canvas 生成 DateURL,然后利用 a 标签下载

let a
const download = document.getElementById('download')
download.addEventListener('click', () => {
    if (!a) {
        a = document.createElement('a')
    }
    a.href = tablet.toDataURL('image/png')
    a.download = '签名.png'
    a.click()
})

完整代码

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style type="text/css">
        #settings {
            display: flex;
            margin: auto;
            margin-top: 100px;
            line-height: 27px;
            justify-content: center;
            gap: 20px;
        }

        #tablet {
            display: block;
            margin: auto;
            margin-top: 10px;
            background-color: #eee;
        }
    </style>
</head>

<body>
    <div>
        <div id="settings">
            <div>
                字体大小:
                <select id="fontSize"></select>
            </div>
            <div>
                字体颜色:
                <input type="color" id="fontColor" />
            </div>
            <button id="undo">撤销</button>
            <button id="clear">清空画布</button>
            <button id="download">生成图片</button>
        </div>
        <canvas width="800" height="300" id="tablet"></canvas>
    </div>

    <script>
        const tablet = document.getElementById('tablet')
        const ctx = tablet.getContext('2d')
        ctx.lineCap = 'round'
        ctx.lineJoin = 'round'
        ctx.lineWidth = 5
        ctx.strokeStyle = '#000'

        // 生成字体大小选项
        const fontSize = document.getElementById('fontSize')
        for (let i = 1; i <= 16; i++) {
            const option = document.createElement('option')
            option.value = i
            option.innerText = i + 'px'
            if (i == 5) {
                option.selected = true
            }
            fontSize.appendChild(option)
        }
        fontSize.addEventListener('change', (e) => {
            ctx.lineWidth = e.target.value
        })

        const fontColor = document.getElementById('fontColor')
        fontColor.addEventListener('change', (e) => {
            ctx.strokeStyle = e.target.value
        })
        let imageData
        let prex, prey
        const mousedown = (e) => {
            if (e.button != 0) return // 不是左键直接返回
            imageData = ctx.getImageData(0, 0, tablet.width, tablet.height)
            prex = e.offsetX
            prey = e.offsetY
            ctx.beginPath()
            ctx.moveTo(prex, prey)
            tablet.addEventListener('mousemove', mousemove)
            document.addEventListener('mouseup', mouseup)
            tablet.addEventListener('mouseenter', mouseenter)
        }
        const mousemove = (e) => {
            ctx.lineTo(e.offsetX, e.offsetY)
            ctx.stroke()
        }
        const mouseup = () => {
            tablet.removeEventListener('mousemove', mousemove)
            document.removeEventListener('mouseup', mouseup)
            tablet.removeEventListener('mouseenter', mouseenter)
        }
        const mouseenter = (e) => {
            prex = e.offsetX
            prey = e.offsetY
            ctx.moveTo(prex, prey)
        }
        tablet.addEventListener('mousedown', mousedown)

        const undo = document.getElementById('undo')
        undo.addEventListener('click', () => {
            ctx.putImageData(imageData, 0, 0)
        })

        const clear = document.getElementById('clear')
        clear.addEventListener('click', () => {
            ctx.clearRect(0, 0, tablet.width, tablet.height)
        })

        let a
        const download = document.getElementById('download')
        download.addEventListener('click', () => {
            if (!a) {
                a = document.createElement('a')
            }
            a.href = tablet.toDataURL()
            a.download = '签名.png'
            a.click()
        })
    </script>
</body>

</html>

结语

至此写字板就完成了,都是 canvas 的知识,最后借助了 a 标签实现图片下载。

如果喜欢或有所帮助的话,希望能点赞关注,鼓励一下作者。

如果文章有不正确或存疑的地方,欢迎评论指出。


清隆
29 声望2 粉丝

学完某项技能一定要写写文章,用的时候都是照搬代码,写出来才能深入理解!