canvas2d绘图踩坑
需求样式
需求是这个样子的:

要求绘制的canvas的大小是370*70(上下男女分成2个canvas,其中一个大小为370*70)
现有的素材是,ui给出了不同大小、颜色的小人的图片,包括灰色和蓝色的男性小人,与灰色和红色的女性小人。
大小分别是22*59、44*108、66*167

问题分析
首先确定构图要素为ui提供的图片,和绘制文字。
绘制文字的api较为简单,基本没有坑,不赘述。
通过查canvas的api可知:
void drawImage(image,dx,dy);image支持的类型很多,包括HTMLImageElement,SVGImageElement等等,这里我选择使用HTMLImageElement。dx,dy指image绘制时,在canvas上绘制的起点坐标。但是2个参数是不够用的,绘制时使用下面2种api。void drawImage(image,dx,dy,dw,dh);同上面形式,指定了图片在canvas上绘制的宽度和高度,宽高由dw和dh传入。void drawImage(image,sx,sy,sw,sh,dx,dy,dw,dh);这个是最复杂,最灵活的使用形式,第一参数是待绘制的图片元素,第二个到第五个参数,指定了原图片上的坐标和宽高,这部分区域将会被绘制到canvas中,而其他区域将忽略,最后四个参数跟形式二一样,指定了canvas目标中的坐标和宽高。

MDN上api的示意图
可见,只要使用第二种4参数的api就能绘制出完整的小人。
问题在于如何绘制一个半灰半有颜色的小人。利用第三种8参数的api可以截取灰色小人图片的一部分,然后在覆盖上一个已经在canvas上画好的完整的有颜色的小人上。这样就能画出一个半灰半有颜色的小人。
开始绘制
问题一:canvas图片加载
canvas需要图片加载完成后才能绘制,即需要监听图片的load事件。当有多个图片(比如现在有4种不同的小人图片)如何确定所有图片都加载完成后,再调用canvas绘图的api呢?
可以创建一个picDetail对象
let PicDetail={count:0}
将其传入preLoad函数,当给img.src赋值后,令count+1,当触发回调。其中reCallback返回一个回调函数,每触发一次回调,就令count--,然后判断count是否为0,当为0时,就说明所有图片加载完成,可以开始绘制了。
function PreLoad(ctx,picDetail,dpr){
//创建img1元素
...
picDetail.count++
...
//给img1加一个事件监听
Img1.addEventListener(
'load',reCallback(ctx,picDetail,dpr),false
);
//创建img2元素
...
picDetail.count++
...
//给img2加一个事件监听
Img2.addEventListener(
'load',reCallback(ctx,picDetail,dpr),false
);
reCallback函数中
function reCallback(ctx,picDetail,dpr){
return ()=>{
picDetail.count--
if(picDetail.count!==0){
return 0 //回调终止,说明有图片还没加载完
} else{
//开始绘制
}
}
}
可能存在的问题:会不会出现count+1后,先count-1,此时count就为0了,再count+1,这样就会出现部分图片还未加载,就触发了回调。
应该不存在这种问题,因为count++的过程是在一个js代码段里完成的,js执行完成后,浏览器才会开始加载img。加载完成后再将回调加入到任务队列中,等待事件循环时执行。
问题二:canvas图片模糊
解决了图片加载问题,可以进入绘制的代码段了,可是发现绘制图片总是非常的模糊,这是为什么呢?一方面是css的放缩,另一方面是dpr,还要一方面是canvas的api放缩。
我们一一解决这些问题
还记得ui提供的图片有三种大小吗?分别是22*59、44*108、66*167,而根据需求的要求,一个性别的canvas大小为370*70。
也就是说图片绘制的css实际大小只会选22*59,因此一开始就确定了dWidth, dHeight的值为22和59。
img自然是选择越大的越好,因为原图分辨率越大,越清晰,这是很合理的想法。因此选择了66*167作为image,而dWidth, dHeight的值为22和59。
ctx.drawImage(image`66*167`, dx, dy, dWidth`22`, dHeight`59`);
结果图片很不清晰,问题可能出在图片缩小上,不应该使用api去强行缩小图片。这样使得绘制的canvas本身的像素只有22和59
合理的解决办法是使用css配合canvas进行缩放。canvas可以绘制的很大,canvas的宽高决定了canvas有多少像素,而canvas的css的宽高决定了canvas展示的大小,只要保证展示大小符合需求,而将canvas根据不同的dpr使用不同的图片,从而绘制出不同大小的canvas,最后统一放进固定大小的css大小中。
首先获得当前设备的dpr,因为只有满足dpr=1,2,3的图片,所以根据dpr的值令dpr只能为1,2,3,这就变成了缩放值。
// dpr初始化
let dpr;
if(window && window.devicePixelRatio){
if(Object.prototype.toString.call(window.devicePixelRatio)==='[object Number]'){
dpr = window.devicePixelRatio
} else{
dpr = 1
}
} else{
dpr = 1
}
// dpr整数化,实际为缩放值
if(dpr>0 && dpr <=1){
dpr = 1
} else if(dpr>1 && dpr <=2){
dpr = 2
} else if(dpr>2 && dpr <=3){
dpr = 3
} else {
dpr = 3
}
开始缩放canvas的大小:
// canvas 初始化
const manCanvas = document.getElementById('manCanvas');
//这里的height和width指canvas拥有的像素的宽和高,而不是css里的展示大小
manCanvas.height = 70 * dpr
manCanvas.width = 400 * dpr
const manCtx = manCanvas.getContext('2d');
<canvas id="manCanvas" class="canvasBox"></canvas>
.canvasBox{
height: 70px;
width: 400px;
}
同理可以根据dpr来选择使用不同的图片
womanRedImg.src = dprToPic(dpr,'woman','red') ;
可以看到图片的绘制也是根据dpr形成的
ctx.drawImage(picDetail.manGrayImg,(82+31*i)*dpr,5*dpr,22*dpr,59*dpr);
其中picDetail.manGrayImg的大小就是(22*dpr)*(59*dpr),避免了通过api压缩图片。
经过测试,在dpr=1上的pc上,dpr=2的mac屏幕上都能清晰的显示。
其他问题:js数值计算不准
和canvas无关,但和js特性有关
后端只会返回男女人数,前端要计算百分比,并且根据之前的理论,必须知道杂色小人中,灰色的比例.然而js浮点数不准确,比如0.7/0.1=6.999,这时候使用Math.floor()计算杂色小人中灰色的比例就会出错。
解决办法就是全部变成整数计算,因为7000/1000=7是相对准确的
绘制重叠杂色小人
代码中先绘制一个蓝色小人,然后用manPercent决定截取灰色小人的宽度,最后覆盖上去。
ctx.drawImage(picDetail.manBlueImg,(82+31*i)*dpr,5*dpr,22*dpr,59*dpr);
ctx.drawImage(picDetail.manGrayImg,0,0,22*dpr*manPercent,59*dpr,(82+31*i)*dpr,5*dpr,22*dpr*manPercent,59*dpr);
大功告成
解决了上述种种问题后最终实现了需求的界面,canvas平时接触很少,此次实现确实学习到了很多,特此记录。