Table of Contents
最近有一个关于小程序的需求——集卡。这个需求有两个比较难搞定的地方:1. 卡牌翻转动画。2. 生成海报。
小程序基础库:2.9.0
卡牌翻转
这个实现的逻辑很简单,一个容器,里面两个容器代表正反,通过 backface-visibility 设置背面可见性。卡牌制好后添加动画(JavaScript 和 css 都可以)就可以实现卡牌翻转效果了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="divport" content="width=device-width, initial-scale=1.0">
<title>backface-visibility</title>
<style>
.card-wrapper {
width: 100px;
height: 130px;
position: relative;
transform-style: preserve-3d;
}
.cover-image,
.back-image {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
backface-visibility: hidden;
}
.cover-image {
transform: rotateY(180deg);
transform-origin: center;
}
.card-image {
width: 100%;
height: 100%;
}
.card-text {
display: block;
width: 100%;
line-height: 1;
margin-top: -50px;
text-align: center;
color: white;
font-size: 36rpx;
text-shadow:1px 2px 4px rgba(27,95,168,0.6);
}
.back-image .card-text {
opacity: 0.4;
margin-top: -58px;
}
</style>
</head>
<body>
<div class="card-wrapper">
<div class="cover-image">
<img class="card-image" src="xxx1.png" />
<span class="card-text">分类</span>
</div>
<div class="back-image">
<img class="card-image" src="xxx2.png"></img>
<span class="card-text">分类</span>
</div>
</div>
</body>
</html>
以上代码在 chrome 里运行良好。如果等效的小程序代码也运行良好我就没必要写“卡牌翻转”这一节了。(在小程序里我用的 this.animate 动画函数。)
在小程序里碰到的第一个问题是,翻转过来的卡牌背面字体也会显示(即正反面文字都会显示)。

网上搜了一下也没找到解决方案,可能因为这类需求太少了。后来,只能对文字条件渲染。即在翻转动画的前半程和后半程各渲染一面的文字。
翻转动画的第二个问题是 iOS 有黑影。

这个问题我百思不得其解,至今未解决。
生成海报
我需要用 canvas 将后台给的海报(一个链接)和活动二维码(即时生成)合并到一起,然后显示并可以保存到相册。
目前官网上关于 canvas 的文档都是新版的(竟然没给旧版文档入口),我也就按照新版的来写了。然后,各种意想不到的问题就来了。
首先,querySelector 获取不到 canvas 元素。我的海报生成工作是在组件里做的。根据网上找到的答案,在组件里需要加上 in :const query = wx.createSelectorQuery().in(this)。然而,我还是没能获取到 canvas。后来找到一篇解答说 canvas 元素需要添加 type=”2d”。试了下,真的解决了。总结一下:in 解决的是组件内选择元素的问题。type=”2d” 解决的是选择 canvas 的问题。
然后,就可以绘制了。对于网络图片,需要先下载下来。在 web 端,可以使用 new Image() 实现。但是,小程序没有这个构造函数。百度后发现,可以先通过相关 api 把图片下载下来。
// 于是有了这段代码
wx.getImageInfo({
src: this.properties.posterUrl,
success: (res) => {
console.log(res)
this.setData({
tempPosterWidth: res.width,
tempPosterHeight: res.height,
posterImage: res.path
})
},
}
获取到本地路径后就可以绘制了。但是,调用 ctx.drawImage 失败了。原来新版 canvas 的 drawImage 已经不支持本地临时路径了。没办法,只能再将临时路径加载成图片。
const image = canvas.createImage()
image.src = this.data.posterImage
// 以下两行代码手机端报错,因为 style 为 undefined。直接设置 width 和 height 也报错,因为 width 只有 getter 属性——只读。
// image.style.width = TPW + 'px'
// image.style.height = TPH + 'px'
image.onload = () => {
……
}
经过这一番折腾,可以绘制海报主体了。接着就是把二维码加上。
生成小程序二维码的接口只能服务端调用,并且 wxacode.getUnlimited 这个接口的 scene 有长度限制(32位),而我的参数是肯定超过 32 位的。没办法,只能让后端接收参数后先保存起来,生成 uuid 作为 scene,扫码进入页面后我再通过 uuid 请求保存的参数,然后再进入正常流程(真的想放声高歌,声嘶力竭的那种)。
不管怎样,总算可以拿到二维码了。为了方便处理(我以为),我让后端先转成 base64 再给我(I am too young~~)。我百度了下小程序的 canvas 怎么绘制 base64,惊奇地发现需要先保存成本地图片,然后才能绘制。没办法,从网上复制了一段保存 basa64 为本地图片的代码。
const fsm = wx.getFileSystemManager();
const FILE_BASE_NAME = 'tmp_base64src';
export const base64src = function(base64data) {
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'));
}
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
const buffer = wx.base64ToArrayBuffer(bodyData);
fsm.writeFile({
filePath,
data: buffer,
encoding: 'binary',
success() {
resolve(filePath);
},
fail() {
reject(new Error('ERROR_BASE64SRC_WRITE'));
},
});
});
};
// 获取二维码
this.getWxacode()
.then(res => {
base64src('data:image/png;base64,' + res)
.then(path => {
this.setData({
qrcodeImage: path
})
setTimeout(() => {
this.generatePoster()
}, 1000)
})
})
用和海报主体一样的方法,将二维码绘制到 canvas 上。
保存海报需要先通过 wx.canvasToTempFilePath 将海报转成临时图片,然后,再通过 wx.saveImageToPhotosAlbum 保存到相册。这里会有文件名太长并无法自定义的问题。不过这无法解决。
以上的流程在开发者工具和我的安卓手机是可以的,但是在 iOS 上就不可以。iOS 微信小程序的 vConsole 里看不出错误,但就是不绘制。至于真机调试,嗯~~ type=’2d’ 和 type=’webgl’ 的 canvas 不支持真机调试。
因为实在不知道为什么无法绘制,只能不使用新版 canvas(去掉 type=’2d’)。
使用旧版 canvas 碰到的第一个问题是 querySelector 无法获取 canvas 元素。具体原因未知,应该是不支持旧版 canvas。旧版 canvas 获取绘制上下文是通过 wx.createCanvasContext 实现的。
const ctx = wx.createCanvasContext('share-canvas', this)
ctx.beginPath()
ctx.drawImage(this.data.posterImage, 0, 0, TPW, TPH)
let tempWidth = Math.floor(TPW / 3)
let tempHeight = Math.floor(TPH / 3)
tempWidth = tempWidth < tempHeight ? tempWidth : tempHeight
ctx.drawImage(this.data.qrcodeImage, Math.floor(TPW / 2) - Math.floor(tempWidth / 2), Math.floor(TPH / 2 - tempWidth / 2), tempWidth, tempWidth)
ctx.draw()
this.setData({
showLoading: false,
posterWidth: TPW,
posterHeight: TPH,
})
旧版 canvas 在开发者工具、安卓、iOS 三端都可以成功绘制图片(谢天谢地,总算可以了)。但是保存图片后发现还有一个问题:图片模糊。
移动端存在 dpr 问题。在 web 开发中,可以通过 ctx.scale 解决问题。可在小程序里,也许新版可以通过 ctx.scale 解决,但旧版就无能为力了。并且旧版 canvas 不支持设置 ctx 的宽高(在网上有一段设置 ctx 宽高的代码)。要生成高清的海报,需要绘制大图,然后通过 css 样式 zoom 缩放显示。如果三端都支持 zoom 的话这篇文章就可以到此结束了。
由于安卓不支持 zoom,所以就图片模糊问题只能换种解决方法:canvas 定位到屏外,大图绘制。绘制结束后保存成临时图片,然后通过 image 显示。
<!-- 显示的海报 -->
<image
wx:if="{{ !showLoading }}"
style="width: {{ posterWidth / systemInfo.pixelRatio }}px; height: {{ posterHeight / systemInfo.pixelRatio }}px;"
src="{{ posterShowUrl }}"
></image>
<!-- 实际绘制的 canvas -->
<canvas
id="share-canvas"
class="share-canvas"
canvas-id="share-canvas"
style="width: {{ posterWidth }}px; height: {{ posterHeight }}px;"
/>
.share-canvas {
position: fixed;
left: 2000%;
top: 2000%;
}
由于 ctx.draw 是异步的,setData 后的渲染也是异步的,所以将 canvas 保存成临时图片的操作需要在 ctx.draw 的回调里完成。
ctx.draw(true, () => {
setTimeout(this.handleSavePoster.bind(this), 300)
})
至此,绘制海报算是完成了。其中坎坷一言难尽。
最后,祝愿小程序早日倒闭。
注意点
- 不同二维码临时保存后绘制出的结果相同
在开发者工具上,不同二维码的 base64 临时保存为图片后进行绘制,得到的结果二维码是不同的。但在手机上,出现了不同二维码绘制结果相同的现象。暂且推测为手机系统缓存问题。解决方法是每次保存时指定不同的路径(文件名)const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}${Date.now()}.${format}`; - 二维码绘制不出来或者海报一直绘制不出来的问题
暂且推断为网络原因。并无解决方法。