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}`;
- 二维码绘制不出来或者海报一直绘制不出来的问题
暂且推断为网络原因。并无解决方法。