关于小程序使用Canvas画布生成分享海报,网上有很多的教程,但是结合到自己的业务上面就BUG百出了 接下来分享一下我的经历
微信出了新的api
微信公众平台目前更新不是太即使,查问题好多帖子都是一两年都没有回应(像授权获取用户的信息,说是马上要废弃,但新的api又不是太方便,用老的又不安心)... 关于画布我们就按照文档上最新的来写
WXML
直接用内置标签 平面海报 type 就是 2d 样式给上尺寸 把它定位移出 可视范围 虽然是用JS在画布范围内 作画的 但最终是调用api 生成图片 在行内有就可以 不用显示
<canvas style="z-index: 99;position: fixed; left: -9990px;top: 0px;width: 750rpx;height: 1200rpx;background: #fff;" id="myCanvas" type="2d" />
JS
初始化画布 找到 WXML中的元素 设置大小 获得分辨率 可以适配不同的机型
// 具体画布实现
createNewImg(){
var that = this;
wx.createSelectorQuery()
.select('#myCanvas') // 在 WXML 中填入的 id
.fields({ node: true, size: true })
.exec((res) => {
// Canvas 对象
const canvas = res[0].node
// Canvas 画布的实际绘制宽高
const renderWidth = res[0].width
const renderHeight = res[0].height
// Canvas 绘制上下文
const context = canvas.getContext('2d')
// 初始化画布大小 适配不同屏幕的分辨率
const dpr = wx.getWindowInfo().pixelRatio
canvas.width = renderWidth * dpr
canvas.height = renderHeight * dpr
context.scale(dpr, dpr)
const ratio = wx.getSystemInfoSync()?.windowWidth / 750; // 各机型的比例
// 绘制前清空画布
context.clearRect(0, 0, canvas.width, canvas.height)
// 设置背景为白色
context.fillStyle = "#fff";
context.fillRect(0, 0, canvas.width, canvas.height)
// 作画顺序当中 加载图片是异步的 想让加载顺序按照自己书写的顺序来 就用Promise 写成链式调用
that.canvasAvatar(canvas,context,ratio).then(res => {
return that.canvasCover(canvas,context,ratio)
}).then(cov => {
return that.canvasCode(canvas,context,ratio)
}).then(cod =>{
that.canvasImg(canvas)
})
})
},
作画时确保需要的数据已经就绪
常见的分享推广海报元素中有 微信 昵称 头像 封面图 小程序码 当在画布中插入动态文字或者图片时保证 数据已经请求到 或者 || 表示备用数据 不然就会导致 海报生成失败 并且不报错 也不好排查错误
在我的开发环境中 用户授权登录后 头像和昵称储存在 wx.StorageSync中 封面图和小程序码是又发起的网络请求 所以画布的开始作画时间选择 在了初始化数据请求(自行封装网络请求)成功之后
// 获取数据
initData(wsg){
let data = {
method : 'POST',
url:'orderCase/getCaseInfo',
data:{id:wsg}
}
let that = this
httpUtils.request(data).then(res=>{
if(res.data.data){ // 确定成功获取到了数据
that.createNewImg() // 开启作画
// 设置标题
wx.setNavigationBarTitle({
title: res.data.data.caseName
})
that.setData({
footerNavList:res.data.data, // 赋值列表数据
})
...
作画时异步插入图片的顺序和堵塞
以上都梳理好之后 生成海报可以成功 但是会出现 缺漏项 少了标题? 少了小程序码? 少了封面图?等等
解决方案 : 作画顺序当中 加载图片是异步的 想让加载顺序按照自己书写的顺序来 就用Promise 写成链式调用 并且每个阶段要连接步骤
context.save() context.beginPath() 使用这两个api
设置头像 如果 没有 ave 就用默认的logo -- 使用裁剪
// 设置 头像 设置头像 (裁剪圆形)
canvasAvatar(canvas,context,ratio) {
return new Promise(res => {
const image = canvas.createImage()
image.src = wx.getStorageSync('ave') || wx.getStorageSync('storeData').logo
image.onload = () => {
context.save()
context.beginPath()
context.arc(125 *ratio, 90*ratio, 50*ratio, 0, Math.PI * 2)
// 裁剪一个圆形 圆心的位置 圆的半径 0 2派 就是弧度完整的 圆
context.clip(); // 裁剪
context.drawImage(image,75*ratio,40*ratio,100*ratio,100*ratio)
// 向画布内写入图片 图片对象 x轴的位置 y 轴的位置 宽 高
context.restore()
console.log('头像完成')
res(true)
}
})
},
设置封面图和文字 -- 封面图有三种尺寸 尺寸不同 文字布局也要发生改变
// 设置封面图
canvasCover(canvas,context,ratio){
let that = this;
return new Promise(cov => {
// 增加用户名 提示语 案例名
context.font = '20px WenQuanYi Micro Hei'
context.fillStyle = 'black';
context.fillText(that.data.userInfo.nickName, 204*ratio, 108*ratio);
context.font = '12px WenQuanYi Micro Hei'
context.fillStyle = '#444';
context.fillText('为您分享', 566*ratio, 108*ratio);
// 封面尺寸是长图的 文字布局
if(wx.getStorageSync('nowCaseCover') === 1 || wx.getStorageSync('nowCaseCover') === 3){
context.font = '18px WenQuanYi Micro Hei'
context.fillStyle = 'black';
context.fillText(that.data.footerNavList.caseName, 400*ratio, 1060*ratio);
context.font = '13px WenQuanYi Micro Hei'
context.fillStyle = 'black';
context.fillText('长按识别,阅览全文', 400*ratio, 1104*ratio);
// 封面尺寸是横图 或者方图的 文字布局
}else{
context.font = '18px WenQuanYi Micro Hei'
context.fillStyle = 'black';
context.textAlign = 'center';
context.fillText(that.data.footerNavList.caseName, 375*ratio, 860*ratio);
context.font = '13px WenQuanYi Micro Hei'
context.fillStyle = 'black';
context.textAlign = 'center';
context.fillText('长按识别,阅览全文', 375*ratio, 1128*ratio);
}
const image3 = canvas.createImage()
image3.src = that.data.footerNavList.microCoverUrl
image3.onload = () => {
setTimeout(() => {
context.save()
context.beginPath()
// nowCaseCover 1 3 长图 2 横图 4 方图
if(wx.getStorageSync('nowCaseCover') === 4){
context.drawImage(image3,75*ratio,180*ratio,600*ratio,600*ratio)
}else if(wx.getStorageSync('nowCaseCover') === 1 || wx.getStorageSync('nowCaseCover') === 3){
context.drawImage(image3,135*ratio,180*ratio,480*ratio,750*ratio)
}else if(wx.getStorageSync('nowCaseCover') === 2){
context.drawImage(image3,-45*ratio,220*ratio,860*ratio,540*ratio)
}
context.restore()
console.log('封面图完成')
cov(true)
}, 250);
}
})
},
最后请求小程序码 异步请求
// 设置小程序码
canvasCode(canvas,context,ratio){
let that = this
return new Promise(cod => {
const image5 = canvas.createImage()
let data = {
method : 'POST',
url:'weChat/getQrCode',
data:{
storeId:wx.getStorageSync('storeData').id,
appid:wx.getStorageSync('appId'),
path:'pages/homeDetail/index',
clerkId:wx.getStorageSync('storeData').clerkId,
caseId:that.data.id
}
}
httpUtils.request(data).then(res=>{
image5.src = res.data.data
})
image5.onload = () => {
setTimeout(() => {
context.save()
context.beginPath()
// 小程序码的布局 随封面图尺寸 变化
if(wx.getStorageSync('nowCaseCover') === 1 || wx.getStorageSync('nowCaseCover') === 3){
context.drawImage(image5,75*ratio,980*ratio,150*ratio,150*ratio)
}else{
context.drawImage(image5,300*ratio,900*ratio,150*ratio,150*ratio)
}
context.restore()
console.log('小程序码完成')
cod(true)
}, 250);
}
})
},
绘制完成 生成图片的 临时路径并 展示
//canvas生成图片
canvasImg(canvas) {
let that = this;
wx.canvasToTempFilePath({
canvas,
success: function (res) {
that.setData({
imagePath: res.tempFilePath, // 临时路径储存起来
prowerS:true // 表示分享海报 已经生成完成
});
},
fail: function (err) {
console.log(err);
}
});
},
一些其他优化
1.开始作画时间 刚一进入页面就开始 作画(数据请求过来之后) 等到用户主动分型生成的时候 也画的差不多了
2.主动生成的时候 依据 prowerS:true 判断(轮询)完成情况 节省内存 当前页面只需 生成一次
主动生成的回调:
// 显示分享海报
drawPoster(e){
// 保险 先关闭之前的
this.setData({
maskHidden: false,
});
wx.showLoading({
title: '生成海报中',
})
let that = this
// 300ms 轮询一次 生成完成后 清除定时器
let wsg = setInterval(() =>{
if(this.data.prowerS){
wx.hideLoading()
that.setData({
maskHidden: true,
})
that.dynamic({
behaviorType: '2',
behaviorDescription: '分享了微官网的案例'
})
clearInterval(wsg)
}
},300)
},
页面展示 show-menu-by-longpress="true" 开启图片长按菜单
<view class='imagePathBox' hidden="{
{maskHidden == false}}" catchtap="close" catchtouchmove='stopScroll'>
<image src="{
{imagePath}}" class='shengcheng' show-menu-by-longpress="true" mode="aspectFill"></image>
</view>