canvas实践笔记

对canvas使用有过好几次了,曾经用基于canvas的createjs写过一个俄罗斯方块的游戏,毕业设计做的是多人在线绘图网站,自然也是canvas实现的。
最近工作需要,需要在h5端调用canvas API实现生成一张带有数据图的图片,“传送给native端”,所以对canvas的使用做一个总结。[2016-08-27更正:生成图片需要走http传输协议,app内与native通信走file传输协议,协议不同,不能直接通信,所以不能完成直接传送给native,只能传送给server端。]
由简到难,本文首先简单介绍canvas几个常用API,然后给出图片预加载的代码,最后会列举绘制过程中的多个坑。

SVG绘制图形是通过构建一棵XML树来实现,canvas来绘制图形是通过调用它提供的方法。所以在canvas中移除相应的元素需要先将当前的相应元素擦除,然后重新绘制。

业务实现效果图,制作过程3.5天,踩了不少坑,下面进行详细讲解。
jk-img

图形绘制一些简单的API

canvas标签在html文档流中存放,标签内的区域都是画布,所以可以在标签上直接设置画布的长宽。

1
<canvas id="mycanvas" width=100 height=100></canvas>

前面已经说过,canvas需要调用API来绘制图形,大部分API需要调用一个上下文对象来实现,所以需要getContext('2d')方法(传参2d)来获取这个CanvasRenderingContext2D对象,使用这个对象来实现在画布上绘制二维图形。
注意每个canvas元素只有一个上下文对象

1
2
var canvas = document.getElementById('mycanvas');
var context = canvas.getContext('2d');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 绘制路径;
* beginPath开始,closePath结束,绘制比较粗糙;
* 手动lineTo结束位置到起点位置,绘制比较精细;
* 完成一条路径后重新开始另一条路径,必须有beginPath
*/
context.beginPath();
context.moveTo(100,100);
context.lineTo(200,200);
context.lineTo(100,200);
context.closePath();

/**
* 画圆-(0,360)的弧线
* context.arc(left, right, radius, 0, Math.PI*2);
*/
context.strokeStyle = '#999';
context.beginPath();
context.arc(30, 60, 30, 0, Math.PI*2);
context.closePath();
context.stroke();
/**
* 画矩形
* rect(x, y, width, height)
*/
context.rect(10, 10, 100, 100);
context.fill();
/**
* 填充路径区域;
* 如果路径没有闭合,则默认起点与终点连接;
* 可以通过先设置类属性来控制外观;
*/
context.fillStyle = '#ccc';
context.fill();
/**
* 描边;
* 可以通过先设置类属性来控制外观;
*/
context.strokeStyle = '#008';
context.stroke();
/**
* 设置字体和大小
*/
context.font = "40pt Calibri";
context.fillText("Hello World!", x, y);

在线demo

实践过程中发现context.fill()有个坑,特别是在给矩形填充颜色的时候,context的指向会出现问题,可能会将之前已经填充的内容重新填充。
简易使用context.fillRect()这个属性来填充矩形。
如果不可避免需要使用context.fill()这个属性,尽量提前使用,不要影响到后面的内容。

插入图片
1
2
3
4
5
function DrawPic(imgParam,left,top){
this.left = left || 0;
this.top = top || 0;
context.drawImage(imgParam,this.left,this.top);
}

[业务需求]:将切图展示在画布的指定位置,设置图片大小
canvas绘图阻塞浏览器,按顺序执行,画图之前需要图片全部加载成功,作者编写了图片预加载功能,在完全加载完成后进行绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function batchProcessingImgs(imgList,newImgList){
imgList.reduce(function(pre,cur){
var img = createNewImg(cur)
img.onload = function(){
newImgList.push(img);
}
},{})
return newImgList;
}

window.onload = function(){
if(imgList.length == newImgList.length){
//do something
}
}

imgList存储图片源文件,newImgList存储加载完成的图片,只有当两个list长度相等时,图片完全加载完成,可以进行画布绘制,另外加载图片的过程是浏览器的异步行为,newImgList中的图片顺序和imgList不一定相同,所以需要根据名字查询图片信息。(在附录的链接中作者给出了完整的实现)

缩小图片并剪裁成圆形

[业务需求]:展示用户的圆形小头像,后端返回的是一定尺寸的大头像,所以我需要先等比例缩放,然后进行圆形裁剪。
需要注意的是,arc+clip的方法只对绘制的图形产生效果,对图片不起作用。
所以只能用填充的方法,设置一个填充pattern(ctx.createPattern(image, repetition)),但是填充的效果是图片的原像素尺寸,如果原图是一张超大图,很有可能填充的只是黑色的一个小角,所以在填充之前需要对原图进行等比例缩小。
有个CanvasPattern.setTransform(matrix)的方法,matrix是SVG的缩放方式,OMG!难道写canvas还要和SVG合用吗?好吧,尝试之后遇到了更难过的结果,连亲爱的chrome都不支持!那就别说其他浏览器了。
换种思路,能不能用别的方式缩小图片呢?还是老大厉害,鼓励我尝试了先将图片绘制在另一个canvas上下文中,然后将新的canvas调整到我所需要的尺寸,然后将新的canvas作为填充pattern填充到老canvas的fill中,尝试结果,完美实现填充!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 按比例缩小图像并裁剪
* left,top:左上角
* radius:裁剪后的圆半径,注意是半径
* 比较宽和高,小的设置为圆的直径长,大的裁剪为scaleBorder
*/
function scaleAndClipImageToRound(context,img,left,top,radius){
var scaleBord,
width = img.width,
height = img.height;
context.save();
var newCanvans = document.createElement('canvas');
newCanvans.width = img.width;
newCanvans.height = img.height;
var newCtx = newCanvans.getContext("2d");

scaleBord = (width >= height ? 2*radius/height : 2*radius/width);
newCtx.scale(scaleBord,scaleBord);
newCtx.drawImage(img,0,0);

var p = ctx.createPattern(newCanvans,"no-repeat");
context.fillStyle = p;
context.translate(left, top);
context.beginPath();
context.arc(radius, radius, radius, 0, Math.PI*2);
context.fill();
context.closePath();
context.restore();
}
状态堆栈

画布API允许保存当前画布的状态,采用的是堆栈的方式,采用save和restore方法,相当于push和pop,恢复堆栈中的状态。也可以理解为PS的一个图层,作者实践过程中在使用context.scale()这个属性的时候一定需要调用pop和restore。

1
2
context.save();
context.restore();
toDataURL时间上的坑

业务需求需要先向后端请求数据,然后进行绘制,我在做这个需求的时候遇到两个坑。
第一个坑:toDataURL后发现ajax请求的数据还没有返回,所以canvas没有画完。因为ajax请求是异步的,无法判断精确data的返回时间,所以只能在请求成功的回调中进行绘制方法的调用。(附录代码段中作者封装了多个请求执行完毕后处理绘图)
第二个坑:需要在图片预加载结束后画图,然后toDataURL,本地运行时图片加载时间可以忽略不计,而生产环境时加载图片是需要一定时间的,所以判断加载完成的js语句根本没有执行。解决方法是每加载一次图片时进行图片加载判断,即有100张图片就需要执行100次判断,直到第100次判断图片加载完全执行完毕,调用绘图方法进行绘制。

附录
我的纯函数式实现canvas绘制设计稿代码
我的对象方法整理版(包括处理多个请求)代码