canvas2d画小人

1,048 阅读6分钟

canvas2d绘图踩坑

需求样式

需求是这个样子的:

要求绘制的canvas的大小是370*70(上下男女分成2个canvas,其中一个大小为370*70

现有的素材是,ui给出了不同大小、颜色的小人的图片,包括灰色和蓝色的男性小人,与灰色和红色的女性小人。

大小分别是22*5944*10866*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的示意图

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*5944*10866*167,而根据需求的要求,一个性别的canvas大小为370*70

也就是说图片绘制的css实际大小只会选22*59,因此一开始就确定了dWidth, dHeight的值为2259

img自然是选择越大的越好,因为原图分辨率越大,越清晰,这是很合理的想法。因此选择了66*167作为image,而dWidth, dHeight的值为2259

ctx.drawImage(image`66*167`, dx, dy, dWidth`22`, dHeight`59`);

结果图片很不清晰,问题可能出在图片缩小上,不应该使用api去强行缩小图片。这样使得绘制的canvas本身的像素只有2259

合理的解决办法是使用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平时接触很少,此次实现确实学习到了很多,特此记录。