奥利奥生成器与Canvas绘制图片不显示问题

最近尝试在 Vue 中实现奥利奥生成器的效果,遇到使用 Canvas 的 drawImage 方法绘制图片不显示的问题,在此记录下解决过程。

前言

欢迎体验

奥利奥生成器

实现思路

奥利奥的原理,就是根据不同的字符串组合,对应不同的图片,然后按顺序将图片绘制到 canvas 上,最终生成图片。

canvas 的 drawImage 有几种用法。

  • 在画布上定位图像: ctx.drawImage(img,x,y);
  • 在画布上定位图像,并规定图像的宽度和高度: ctx.drawImage(img,x,y,width,height);
  • 剪切图像,并在画布上定位被剪切的部分: ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

我们这里只需要使用第二种方法,控制下图片的大小即可。

参数值

参数 描述
img 规定要使用的图像、画布或视频。
sx 可选。 开始剪切的 x 坐标位置。
sy 可选。 开始剪切的 y 坐标位置。
swidth 可选。 被剪切图像的宽度。
sheight 可选。 被剪切图像的高度。
x 在画布上放置图像的 x 坐标位置。
y 在画布上放置图像的 y 坐标位置。
width 可选。 要使用的图像的宽度(伸展或缩小图像)。
height 可选。 要使用的图像的高度(伸展或缩小图像)。

一般情况下,都按照下面的示例来使用。把绘制写到 img 的 onload 事件里,是因为如果图片还没有加载完成,drawImage 是不生效的,这样能够保证图片已经加载完成。不过因为我们需要多次调用同一张图片,绘制多次,这样的方法就不是很方便。

1
2
3
4
5
6
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var img = document.getElementById("scream");
img.onload = function () {
ctx.drawImage(img, 10, 10, 150, 180);
};

于是尝试将加载的图片缓存一下,方便调用,使用的是下面的方法。sources 中是图片名称和图片地址的键值对,使用 Image 对象设置 src 的方式加载图片;在 onload 事件中,记录加载完成的次数;当全部加载完成时,将存储了这些 Image 对象的 cacheImages 对象存储下来以便调用。

在原版 oreooo 的页面中,我可以看到这个方法是有效的。但是我在 Vue 3 + Vite 2 的环境下,虽然 cacheImages 成功存储了,也能在 Networks 里面看到网络请求,但是 canvas 绘制出来的图形一直是空白的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
loadImages: function (sources, callback) {
var cacheImages = {};
var index = 0;
var attCount = Object.getOwnPropertyNames(sources).length;
for (imgItem in sources) {
cacheImages[imgItem] = new Image();
cacheImages[imgItem].onload = function () {
index++;
if (index == attCount) {
images = cacheImages;
if (typeof callback === "function") {
callback();
}
}
}
cacheImages[imgItem].src = sources[imgItem];
}
}

或许是需要实际存在的 img 标签才有用,我如果页面上实际就有这个图片,我测试了下,确实就能获取到图片绘制出来了。于是我换了种写法,通过在页面实际加载图片,然后在绘制时获取图片 DOM 的 image,只要图片已经加载完成了,绘制时就能正常出图了。下面是在 Vue 里的一个大概示例。

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
<template>
<div class="imgContainer">
<img :src="assets.O" ref="O" />
<img :src="assets.R" ref="R" />
<img :src="assets.Ob" ref="Ob" />
</div>
<div class="output-image">
<canvas ref="oreo_canvas" width="240" height="500">
您的浏览器不支持 HTML5 canvas 标签。
</canvas>
</div>
</template>
<script>
import O from "./assets/image/O.png";
import R from "./assets/image/R.png";
import Ob from "./assets/image/Ob.png";

export default {
data: () => ({
assets: {
O,
R,
Ob,
},
}),
methods: {
generateImage() {
const canvas = this.$refs.oreo_canvas;
const ctx = canvas.getContext("2d");
ctx.drawImage(this.$refs.O.image, 0, 0, 240, 160);
},
},
};
</script>
<style scoped lang="scss">
.imgContainer {
display: none;
}
</style>

总结

Canvas 的 drawImage 在图片还未加载完成时是不会生效的,由于这个特性,需要保证在调用 drawImage 时图片已经加载完成,否则就会有不显示的现象。解决的方案有下面两种。

  • 将 drawImage 事件写在图片的 onload 事件中,这样可以保证图片已经加载。
  • 可以在页面上写上 img 标签实际加载图片,然后 drawImage 时调用 img 的 DOM 下的 image 属性进行绘制。