这篇文章介绍下如何用 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()
抬起画笔,让笔头离开画板。
橡皮擦:清除一块矩形区域上的笔迹。ctx.clearRect(x, y, w, h)
四个参数分别表示矩形的 左上角横坐标、左上角纵坐标、宽度、长度。
上面的接口还不足以支撑完整的画板功能,但是已经足够开始动手写核心 demo 了。下面以鼠标事件、触摸事件为例,给出两个代码示例。
鼠标事件
需要用到的事件有:onmousedown
、onmousemove
、onmouseup
。
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;
}
}
触屏事件
需要用到的事件有:ontouchstart
、ontouchmove
、ontouchend
。
与鼠标不同的主要还有:
- 获取笔头当前在画板上的位置的方式
- 测试手头上有的各种浏览器的之后,加上了几处 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:获取鼠标位置
搜索引擎的答案五花八门。过去太久,已经记不清示例代码里获取笔头位置的代码是怎么来的了。