之前学了 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 生成的
接下来我们来写 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)
撤销
想要实现撤销功能,就要保存之前的状态,可以在开始写字时保存此时的画布状态,在需要撤销时恢复
考虑到 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 标签实现图片下载。
如果喜欢或有所帮助的话,希望能点赞关注,鼓励一下作者。
如果文章有不正确或存疑的地方,欢迎评论指出。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。