当我们开发完自己的小程序后,很想分享给大家让都来关注一下,但是单纯二维码看起来比较单调,不太吸引人!
微信小程序也提供了一系列基于canvas的绘图相关API,所以绘制这样的图片还是比较简单易上手的。下面就开始我们的绘制之旅吧!
那怎么来生成一个分享的图片呢?
step1:绘制背景图片
观察合成的分享图,它的组成主要有以下几个部分:一张大的背景图,一本图书封面,书名“Row,row,row your boat”,“扫码听原版音频”文字,以及一个小程序码图片。 最后的两个button“分享给好友”“保存到相册”,只是做交互作用,不绘制到合成图片中去。
那么我们就先找一张图片来当做背景图,将它画到画布上去,代码大致如下:
ctx.drawImage(this.randomBg, 0, (this.statusBarHeight-44)*scale, 375*scale, bgHeight*scale)
ctx.draw(true)
step2:绘制图书封面图片
let coverDx = (250-this.coverSize.width)/2 + 62.5
let coverDy = this.naviBarBottom + 10 + (250-this.coverSize.height)/2
if (this.isIos) {
ctx.setShadow(0, 16*scale, 16*scale, '#D8D8D8')
}
ctx.drawImage(this.bookCoverFilePath, coverDx*scale, coverDy*scale, this.coverSize.width*scale, this.coverSize.height*scale)
ctx.draw(true)
step3:绘制文字
//绘制文字
let titleW = 250
let titleH = 40
let titleDx = 375/2
let titleDy = coverDy + 250 + 30 + titleH/2
for (let b = 0; b < row.length; b++) {
ctx.fillText(row[b], titleDx*scale, (titleDy + b*titleH)*scale, titleW*scale);
}
ctx.draw(true)
step4:绘制小程序码
const qrDx = 138
const qrDy = this.screenHeight - 57 - 100
ctx.drawImage(this.qrcodeFilePath, qrDx*scale, qrDy*scale, 100*scale, 100*scale)
//等待绘制完毕后输出
ctx.draw(true, ()=>{
wx.canvasToTempFilePath({
canvasId: 'book-img',
x: 0,
y: 0,
width: 375*scale,
height: this.screenHeight*scale,
success:(res)=>{
this.imgOutputPath = res.tempFilePath
},
fail() {
setTimeout(()=>{
toast('生成分享码失败,请退出重试')
},500)
},
complete:()=>{
this.imgDone = true
if (this.shareDone) {
this.loading = false
}
}
})
})
我使用的vue.js+mpvue开发的小程序,我先展示一下这个页面的完整代码,由于业务需求,实现的逻辑可能复杂了点:
<template>
<div class="con" :style="{height: screenHeight+'px'}" @touchmove.stop="disableMove">
<canvas canvas-id="book-img" class="canvas" :style="{height: (screenHeight*4)+'rpx',background: '#ffffff'}" />
<canvas canvas-id="book-share" class="canvas" :style="{height: (screenHeight*4)+'rpx',background: '#ffffff'}" />
<img class="bg" :style="{top: (statusBarHeight-44)*2+'rpx'}" :src="randomBg"/>
<div class="content" :style="{'padding-top': statusBarHeight+'px','padding-bottom': (statusBarHeight >20 ? 54:20)+'px'}">
<div class="header">
<div class="close-btn" @click="onClickClose">
<img class="close-img" src="/static/icon_close.png" />
</div>
<div class="book-cover-con">
<div class="book-cover" :style="{width: coverSize.width*2+'rpx', height: coverSize.height*2+'rpx'}">
<lazy-image :src="book.book_img_thumb" mode="aspectFit" />
</div>
</div>
<div class="book-title line3">{{book.book_name}}</div>
</div>
<div class="footer">
<div class="scan-con">
<img class="scan-music" src="/static/icon_music_node_black.png" />
<div class="scan-tip">扫码收听原版音频</div>
<div class="scan-qrcode">
<lazy-image :src="qrCodeUrl"/>
</div>
</div>
<div class="btn-con">
<div class="btn btn-left">
分享给好友
<button class="btn-share" open-type="share" />
</div>
<div class="btn" @click="onClickSave2Album">保存到相册</div>
</div>
</div>
</div>
</div>
</template>
<script>
import LazyImage from '@/components/base/LazyImage';
import {mapGetters} from 'vuex'
import {
getQuery,
toast,
showLoading,
hideLoading,
} from '@/utils'
import {getQRCode} from '@/api/mini-program'
export default {
components: {LazyImage},
props: {
book: {
type: Object
}
},
data() {
return {
qrCodeUrl: '',
bookCoverFilePath: '',
qrcodeFilePath: '',
imgOutputPath: '',
shareOutputPath: '',
imgDone: false,
shareDone: false,
loading: false,
}
},
onUnload() {
this.qrCodeUrl = ''
this.bookCoverFilePath = ''
this.qrcodeFilePath = ''
this.imgOutputPath = ''
this.shareOutputPath = ''
this.imgDone = false
this.shareDone = false
this.loading = false
},
computed: {
randomBg() {
let index = Math.floor(Math.random()*3)%3
return '/static/book_share_bg' + index + '.png'
},
coverSize() {
let query = getQuery(this.book['book_img_thumb'])
let width = query.w ? query.w: 250, height = query.h ? query.h:250
let maxW = 250, maxH = 250
let size = {}
if (width > height) {
size.width = maxW
size.height = height*maxW/width
}else {
size.height = maxH
size.width = width*maxH/height
}
return size
},
...mapGetters('system', ['screenHeight','statusBarHeight','naviBarBottom','isIos'])
},
methods: {
disableMove() {},
draw() {
if (this.bookCoverFilePath.length === 0 || this.qrcodeFilePath.length === 0) {return}
this.drawImg()
this.drawShare()
},
drawImg() {
const ctx = wx.createCanvasContext('book-img', this)
const scale = 2
//bg
const bgHeight = 812
ctx.drawImage(this.randomBg, 0, (this.statusBarHeight-44)*scale, 375*scale, bgHeight*scale)
ctx.draw(true)
//cover
let coverDx = (250-this.coverSize.width)/2 + 62.5
let coverDy = this.naviBarBottom + 10 + (250-this.coverSize.height)/2
if (this.isIos) {
ctx.setShadow(0, 16*scale, 16*scale, '#D8D8D8')
}
ctx.drawImage(this.bookCoverFilePath, coverDx*scale, coverDy*scale, this.coverSize.width*scale, this.coverSize.height*scale)
ctx.draw(true)
//title
//计算每行文本,需要裁剪时裁剪文本
let title = this.book.book_name
let chars = title.split('')
let temp = ''
let word = ''
let row = []
const maxLine = 3
ctx.setShadow(0, 0, 0, '#000000')
ctx.font = `normal bold ${28*scale}px PingFangSC-Semibold`
ctx.setTextAlign('center')
ctx.setFillStyle('#484848')
ctx.setTextBaseline('middle')
for (let a = 0; a < chars.length; a++) {
let ascii = title.charCodeAt(a)
let isLetter = ascii >= 65 && ascii <= 90 || ascii >= 97 && ascii <= 122
if (isLetter && a !== chars.length-1) {
word += chars[a]
continue
}
else {
if (ctx.measureText(temp+word+chars[a]).width < 250*scale) {
temp += word
temp += chars[a];
}
else {
row.push(temp);
temp = word + chars[a];
}
word = ''
}
}
row.push(temp);
if (row.length > maxLine) {
let rowCut = row.slice(0, maxLine);
let rowPart = rowCut[maxLine-1];
let test = "";
let empty = [];
for (let a = 0; a < rowPart.length; a++) {
if (ctx.measureText(test).width < 220*scale) {
test += rowPart[a];
}
else {
break;
}
}
empty.push(test);
let group = empty[0] + '...'
rowCut.splice(maxLine-1, 1, group);
row = rowCut;
}
//绘制文字
let titleW = 250
let titleH = 40
let titleDx = 375/2
let titleDy = coverDy + 250 + 30 + titleH/2
for (let b = 0; b < row.length; b++) {
ctx.fillText(row[b], titleDx*scale, (titleDy + b*titleH)*scale, titleW*scale);
}
ctx.draw(true)
//tip & qrcode
const iconPath = '/static/icon_music_node_black.png'
const iconDx = 122
const iconDy = this.screenHeight - 20 - 22
ctx.drawImage(iconPath, iconDx*scale, iconDy*scale, 22*scale, 22*scale)
const tipW = 104
const tipH = 18
const tipDx = 148 + tipW/2
const tipDy = this.screenHeight - 22 - 18/2
ctx.setFontSize(13*scale)
ctx.setTextAlign('center')
ctx.setFillStyle('#919191')
ctx.setTextBaseline('middle')
ctx.fillText('扫码收听原版音频', tipDx*scale, tipDy*scale, tipW*scale)
const qrDx = 138
const qrDy = this.screenHeight - 57 - 100
ctx.drawImage(this.qrcodeFilePath, qrDx*scale, qrDy*scale, 100*scale, 100*scale)
//等待绘制完毕后输出
ctx.draw(true, ()=>{
wx.canvasToTempFilePath({
canvasId: 'book-img',
x: 0,
y: 0,
width: 375*scale,
height: this.screenHeight*scale,
success:(res)=>{
this.imgOutputPath = res.tempFilePath
},
fail() {
setTimeout(()=>{
toast('生成分享码失败,请退出重试')
},500)
},
complete:()=>{
this.imgDone = true
if (this.shareDone) {
this.loading = false
}
}
})
})
},
drawShare() {
const ctx = wx.createCanvasContext('book-share', this)
const scale = 2
//bg
const bgHeight = 812
ctx.drawImage(this.randomBg, 0, 0, 375*scale, bgHeight*scale)
ctx.draw(true)
//cover
let coverW = this.coverSize.width/250*196
let coverH = this.coverSize.height/250*196
let coverDx = (196-coverW)/2 + 89
let coverDy = 25 + (196-coverH)/2
let coverBottom = coverDy + coverH
if (this.isIos) {
ctx.setShadow(0, 16*scale, 16*scale, '#D8D8D8')
}
ctx.drawImage(this.bookCoverFilePath, coverDx*scale, coverDy*scale, coverW*scale, coverH*scale)
ctx.draw(true)
//slogan
const tipW = 230
const tipH = 30
const tipDx = 375/2
const tipDy = coverBottom + 25+ 30/2
ctx.setFontSize(21*scale)
ctx.setTextAlign('center')
ctx.setFillStyle('#484848')
ctx.setTextBaseline('middle')
ctx.fillText('扫码收听原版音频', tipDx*scale, tipDy*scale, tipW*scale)
//等待绘制完毕后输出
ctx.draw(true, ()=>{
wx.canvasToTempFilePath({
canvasId: 'book-share',
x: 0,
y: 0,
width: 375*scale,
height: 375/5*4*scale,
success:(res)=>{
this.shareOutputPath = res.tempFilePath
this.$emit('share', {book: this.book, imgUrl: this.shareOutputPath})
},
fail() {
setTimeout(()=>{
toast('生成分享码失败,请退出重试')
},500)
},
complete:()=>{
this.shareDone = true
if (this.imgDone) {
this.loading = false
}
}
})
})
},
onClickSave2Album() {
wx.saveImageToPhotosAlbum({
filePath: this.imgOutputPath,
success(res) {
toast('保存成功')
},
fail(res) {
toast('保存失败,请确认权限是否开启')
setTimeout(()=>wx.openSetting(),2000)
}
})
},
onClickClose() {
this.$emit('close')
},
},
mounted() {
this.loading = true
wx.getImageInfo({
src: this.book.book_img_thumb,
success:(res)=>{
this.bookCoverFilePath = res.path
this.draw()
},
fail:(res)=>{
this.loading = false
setTimeout(()=>{
toast('生成分享码失败,请退出重试')
},500)
}
})
let scene = this.book.is_group === 1 ? `goods_id=${this.book.goods_id}`:`isbn_id=${this.book.isbn_id}`
let page = 'pages/index/main'
getQRCode(scene, page, 100*3).then(
(res)=>{
let {qrcode_url} = res
this.qrCodeUrl = qrcode_url
wx.getImageInfo({
src: this.qrCodeUrl,
success:(res)=>{
this.qrcodeFilePath = res.path
this.draw()
},
fail:(res)=>{
this.loading = false
setTimeout(()=>{
toast('生成分享码失败,请退出重试')
},500)
}
});
}
).catch((err)=>{
this.loading = false
setTimeout(()=>{
toast(err.message)
},500)
})
},
watch: {
loading(newVal) {
newVal ? showLoading():hideLoading()
}
}
}
</script>
<style scoped lang='less'>
@import "../../styles/variable.less";
.con {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: #ffffff;
overflow: hidden;
z-index: 1004;
}
.bg {
position: fixed;
width: 100%;
height: 691px;
left: 0;
}
.content {
position: fixed;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 100%;
top: 0;
left: 0;
box-sizing: border-box;
}
.close-btn {
box-sizing: border-box;
width: 44px;
height: 44px;
margin-left: 10px;
padding-top: 12px;
padding-left: 12px;
}
.close-img {
width: 20px;
height:20px;
}
.book-cover-con {
display: flex;
justify-content: center;
align-items: center;
width: 250px;
height: 250px;
margin-top: 10px;
margin-left: 62.5px;
}
.book-cover {
box-shadow:0px 16px 16px 0px rgba(0,0,0,0.14);
}
.book-title {
color: #484848;
font-size:28px;
font-weight: bold;
line-height:40px;
text-align: center;
height: 120px;
margin: 30px 62.5px 0;
}
.scan-con {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
height: 75px;
}
.scan-music {
width: 22px;
height: 22px;
margin-left: 136px;
}
.scan-tip {
color: #919191;
font-size:13px;
line-height:18px;
margin-left: 4px;
}
.scan-qrcode {
margin-left: 10px;
width: 75px;
height: 75px;
}
.btn-con {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 20px;
width: 100%;
height: 50px;
}
.btn {
width: 145px;
height: 50px;
font-size: 18px;
line-height:50px;
color: #484848;
border-radius:4px;
border:1px solid rgba(72,72,72,1);
text-align: center;
&:active {
opacity: 0.5;
}
}
.btn-left {
position: relative;
margin-right: 15px;
}
.btn-share {
position: absolute;
opacity: 0;
color: #00000000;
background: #00000000;
background-color: #00000000;
width: 145px;
height: 50px;
border-width: 0px;
left: 0;
top: 0;
padding: 0;
}
/*canvas*/
.canvas {
position: fixed;
width: 750px;
top: -4000px;
left: 0;
}
</style>