前言
在微信小程序的开发中,可能会涉及到一个功能,就是卡片分享,卡片分享的主要技术点就是canvas(画布),这篇文章就是记录一下如何用canvas制作一个卡片分享。
开发准备
本篇文章中用到的小程序基础库版本为2.22.0,因此,在canvas的使用上与低版本基础库有所区别,具体如下:
-
从基础库 2.9.0 开始,停止维护wx.createCanvasContext,改为使用canvas.getContext(‘2d’);
-
ctx.drawImage方法第一个参数不能直接使用图片地址,需要使用canvas.createImage()进行创建;
-
2.9.0 起支持一套新 Canvas 2D 接口(需指定 type 属性);
-
从基础库 1.9.90 开始,舍弃了部分属性方法,需要使用新版方法。
开始开发
先来看一下效果图:
1、变量定义
data: {
name: '', //植物名称
imageUrl: '', //图片
localImageUrl: '', //本地临时路径
codeUrl: '../../images/code.jpg', //小程序码
appName: '植晓', //应用名称
desc: '长按识别小程序码 / 带你认识植物',// 说明
date: f.formatYmd(util.formatDate(new Date())),// 格式化日期
_width: 0, //canvas区域的宽
_height: 0, //canvas区域的高
imgHeigth: 0, //原图片高度
imgWidth: 0, //原图片宽度
loadImagePath: '', //保存图片本地路径
len: {
"9": 280,
"10": 302,
"11": 327,
"12": 352
}, //计算日期的位置
winWidth: app.globalData.systemInfo.windowWidth,
winHeight: app.globalData.systemInfo.windowHeight,
blc: 750, //比例,用来自适应图片和文字
}
以上就是canvas绘制时需要用到的变量。
2、布局代码
<view class="index-container">
<view class="index-daily-info">
<canvas canvas-id="myCanvas" id="myCanvas" style="width:100%;height:100%;border-radius: 5px;" type="2d"></canvas>
</view>
<view class="index-operation">
<view class="operation-btn">
<button class="btn btn-share" open-type='share' style="width:280rpx;">分享给好友</button>
<button class="btn btn-save" bindtap='saveImg' style="width:320rpx;">
<image src="../../images/download.png" mode="aspectFill" style="width:40rpx;height:40rpx;margin-right:10rpx;">
</image>保存植物美图
</button>
</view>
</view>
</view>
注意:canvas上必须有id和type,type的值为2d。
3、功能代码
利用canvas进行图片绘制,需要注意的一个问题就是图片的适配,要保证绘制出来的图片不变形,因此,在绘制图片之前,我们需要进行一些必要计算。
首先,需要计算我们的卡片在每个机型上的宽和高,这里就需要用到节点选择wx.createSelectorQuery(),代码如下:
var query = wx.createSelectorQuery();
query.select('.index-daily-info').boundingClientRect(function (rect) {
_this.setData({
_width: rect.width.toFixed(0),// canvas的宽
_height: rect.height.toFixed(2)// canvas的高
})
// 获取图片的信息
wx.getImageInfo({
src: _this.data.imageUrl, //服务器返回的带参数的小程序码地址
success: function (res) {
//res.path是网络图片的本地地址
_this.setData({
localImageUrl: res.path,
imgHeigth: res.height,
imgWidth: res.width
})
// 创建canvas图片
_this.canvasImg()
},
fail: function (res) {
//失败回调
}
});
}).exec();
然后,就是正式开始绘制,由于文字要适配各种机型,所以,需要计算一个缩放比例:
let scale = _this.data.winWidth / _this.data.blc //缩放比例
从效果图上可以看出,图片只占了卡片的一半高度,因此,需要将图片的高度固定在canvas一半高度左右:
let imgH = _height * 0.54 // 要显示的图片高度
除此之外,还需要计算图片与canvas的宽高比:
let dw = _width / imgWidth, //canvas与图片的宽高比
dh = imgH / imgHeight
效果图的上左和右有圆角的效果,代码如下:
// 图片的x坐标
let bg_x = 0
// 图片的y坐标
let bg_y = 0
// 图片圆角
let bg_r = 5
// 图片宽度
let bg_w = _width
// 图片高度
let bg_h = _height
//绘制上左和上右圆角
ctx.arc(bg_x + bg_r, bg_y + bg_r, bg_r, Math.PI, Math.PI * 1.5)
ctx.arc(bg_x + bg_w - bg_r, bg_y + bg_r, bg_r, Math.PI * 1.5, Math.PI * 2)
ctx.arc(bg_x + bg_w - bg_r, bg_y + bg_h - bg_r, bg_r, 0, Math.PI * 0.5)
ctx.arc(bg_x + bg_r, bg_y + bg_h - bg_r, bg_r, Math.PI * 0.5, Math.PI)
ctx.clip()
接下来就是绘制图片了,这里在绘制时,需要计算以保证图片不会变形,代码如下:
// 这里需要先创建一个图片实例才能在canvas上使用
const img = canvas.createImage()
await new Promise(resolve => {
img.onload = resolve
img.src = imgurl
})
//绘制图片
if (imgWidth > _width && imgHeight > imgH || imgWidth < _width && imgHeight < imgH) {
if (dw > dh) {
ctx.drawImage(img, 0, (imgHeight - imgH / dw) / 2, imgWidth, imgH / dw, 0, 0, _width - 0.1, imgH)
} else {
ctx.drawImage(img, (imgWidth - _width / dh) / 2, 0, _width / dh, imgHeight, 0, 0, _width - 0.1, imgH)
}
} else {
if (imgWidth < _width) {
ctx.drawImage(img, 0, (imgHeight - imgH / dw) / 2, imgWidth, imgH / dw, 0, 0, _width - 0.1, imgH)
} else {
ctx.drawImage(img, (imgWidth - _width / dh) / 2, 0, _width / dh, imgHeight, 0, 0, _width - 0.1, imgH)
}
}
绘制完图片后需要恢复之前保存的绘图上下文,避免接下来绘制文字影响图片。
ctx.restore()
接下来就是绘制文字了,需要绘制的文字有标题、小程序介绍、竖排的日期和小程序名称:
绘制标题:
// 字体适配,rpx换算px原理 1rpx = 屏幕宽度/750
ctx.font = parseInt(36 * scale)
ctx.fillStyle = '#000000'
ctx.fillText(_this.data.name.split('').join(' / '), parseInt(56 * scale), imgH + parseInt(74 * scale));
绘制小程序说明:
//绘制说明
ctx.font = parseInt(20 * scale)
ctx.fillStyle = '#000000'
ctx.textAlign = 'center'
ctx.fillText(_this.data.desc, _width / 2, _height - parseInt(74 * scale));
绘制竖排的日期和小程序名称:
//绘制日期
ctx.font = parseInt(20 * scale)
ctx.fillStyle = '#999'
ctx.textAlign = 'right'
c.drawTextVertical(ctx, _this.data.date, _width - parseInt(56 * scale), _height - parseInt(_this.data.len[_this.data.date.length] * scale))
c.drawTextVertical(ctx, _this.data.appName, _width - parseInt(92 * scale), _height - parseInt(98 * scale))
在上述代码中用到了drawTextVertical,这个是自定义的绘制竖排文字的方法,文件命名canvas.js,存放位置可以自己定义,这里是放在了pages同级目录下的utils目录中,代码如下:
const app = getApp()
var winWidth = app.globalData.systemInfo.windowWidth
var scale = winWidth / 750
function drawTextVertical(context, text, x, y) {
var arrText = text.split('');
var arrWidth = arrText.map(function (letter) {
return parseInt(26 * scale);
});
var align = context.textAlign;
var baseline = context.textBaseline;
if (align == 'left') {
x = x + Math.max.apply(null, arrWidth) / 2;
} else if (align == 'right') {
x = x - Math.max.apply(null, arrWidth) / 2;
}
if (baseline == 'bottom' || baseline == 'alphabetic' || baseline == 'ideographic') {
y = y - arrWidth[0] / 2;
} else if (baseline == 'top' || baseline == 'hanging') {
y = y + arrWidth[0] / 2;
}
context.textAlign = 'center';
context.textBaseline = 'middle';
// 开始逐字绘制
arrText.forEach(function (letter, index) {
// 确定下一个字符的纵坐标位置
var letterWidth = arrWidth[index];
// 是否需要旋转判断
var code = letter.charCodeAt(0);
if (code <= 256) {
context.translate(x, y);
// 英文字符,旋转90°
context.rotate(90 * Math.PI / 180);
context.translate(-x, -y);
} else if (index > 0 && text.charCodeAt(index - 1) < 256) {
// y修正
y = y + arrWidth[index - 1] / 2;
}
context.fillText(letter, x, y);
// 旋转坐标系还原成初始态
context.setTransform(1, 0, 0, 1, 0, 0);
// 确定下一个字符的纵坐标位置
var letterWidth = arrWidth[index];
y = y + letterWidth;
});
// 水平垂直对齐方式还原
context.textAlign = align;
context.textBaseline = baseline;
}
module.exports = {
drawTextVertical: drawTextVertical
}
使用时需要在js文件顶部进行引入:
const app = getApp()
const c = require('../../utils/canvas')
const util = require('../../utils/util')
const f = require('../../utils/format')
var _this;
前面提到了竖排的日期,从效果图中可以看出日期大写的格式,这个也是用自定义的方法进行转换得来,也就是上面代码中的const f = require(’…/…/utils/format’):
function formatYmd(dateStr) {
var dict = {
"0": "零",
"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": "三十一"
};
var date = dateStr.split('-'),
yy = date[0],
mm = date[1],
dd = date[2];
var yearStr = dict[yy[0]] + dict[yy[1]] + dict[yy[2]] + dict[yy[3]] + '年',
monthStr = dict['' + Number(mm)] + '月',
dayStr = dict['' + Number(dd)] + '日';
return yearStr + monthStr + dayStr
}
module.exports = {
formatYmd: formatYmd
}
在使用时,传入日期即可,日期格式为yyyy-mm-dd,在前面变量定义中有提到。
最后,从效果图中可以看出,有一圈浅灰色的虚线,也很简单:
//绘制虚线边框
ctx.strokeStyle = '#ccc';
ctx.beginPath();
ctx.lineWidth = 1
ctx.setLineDash([2, 4]);
//顶部
ctx.moveTo(15, 15);
ctx.lineTo(_width - 15, 15);
//底部
ctx.moveTo(15, _height - 15);
ctx.lineTo(_width - 15, _height - 15);
//左边
ctx.moveTo(15, 15);
ctx.lineTo(15, _height - 15);
//右边
ctx.moveTo(_width - 15, 15);
ctx.lineTo(_width - 15, _height - 15);
ctx.stroke();
至此,一个使用canvas绘制卡片的功能就完成了。
下面给出绘制部分的全部代码:
canvasImg: function () {
let _width = _this.data._width,
_height = _this.data._height //宽与高
let imgHeight = _this.data.imgHeight,
imgWidth = _this.data.imgWidth,
scale = _this.data.winWidth / _this.data.blc //缩放比例
let imgH = _height * 0.54 // 要显示的图片高度
let dw = _width / imgWidth, //canvas与图片的宽高比
dh = imgH / imgHeight
let imgurl = _this.data.localImageUrl
// 图片的x坐标
let bg_x = 0
// 图片的y坐标
let bg_y = 0
// 图片圆角
let bg_r = 5
// 图片宽度
let bg_w = _width
// 图片高度
let bg_h = _height
wx.createSelectorQuery()
.select('#myCanvas')
.fields({
node: true,
size: true
})
.exec(async (res) => {
var canvas = res[0].node
canvas.width = _width
canvas.height = _height
const ctx = canvas.getContext('2d')
// let ctx = wx.createCanvasContext('myCanvas')
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, _width, _height);
ctx.save()
ctx.beginPath()
//绘制上左和上右圆角
ctx.arc(bg_x + bg_r, bg_y + bg_r, bg_r, Math.PI, Math.PI * 1.5)
ctx.arc(bg_x + bg_w - bg_r, bg_y + bg_r, bg_r, Math.PI * 1.5, Math.PI * 2)
ctx.arc(bg_x + bg_w - bg_r, bg_y + bg_h - bg_r, bg_r, 0, Math.PI * 0.5)
ctx.arc(bg_x + bg_r, bg_y + bg_h - bg_r, bg_r, Math.PI * 0.5, Math.PI)
ctx.clip()
const img = canvas.createImage()
await new Promise(resolve => {
img.onload = resolve
img.src = imgurl
})
//绘制图片
if (imgWidth > _width && imgHeight > imgH || imgWidth < _width && imgHeight < imgH) {
if (dw > dh) {
ctx.drawImage(img, 0, (imgHeight - imgH / dw) / 2, imgWidth, imgH / dw, 0, 0, _width - 0.1, imgH)
} else {
ctx.drawImage(img, (imgWidth - _width / dh) / 2, 0, _width / dh, imgHeight, 0, 0, _width - 0.1, imgH)
}
} else {
if (imgWidth < _width) {
ctx.drawImage(img, 0, (imgHeight - imgH / dw) / 2, imgWidth, imgH / dw, 0, 0, _width - 0.1, imgH)
} else {
ctx.drawImage(img, (imgWidth - _width / dh) / 2, 0, _width / dh, imgHeight, 0, 0, _width - 0.1, imgH)
}
}
ctx.restore()
//绘制标题
// 字体适配,rpx换算px原理 1rpx = 屏幕宽度/750
ctx.font = parseInt(36 * scale)
ctx.fillStyle = '#000000'
ctx.fillText(_this.data.name.split('').join(' / '), parseInt(56 * scale), imgH + parseInt(74 * scale));
//绘制小程序码
const codeImg = canvas.createImage()
await new Promise(resolve => {
codeImg.onload = resolve
codeImg.src = _this.data.codeUrl
})
ctx.drawImage(codeImg, _width / 2 - (_width * 0.25 / 2), _height - parseInt(262 * scale), _width * 0.25, _width * 0.25)
//绘制说明
ctx.font = parseInt(20 * scale)
ctx.fillStyle = '#000000'
ctx.textAlign = 'center'
ctx.fillText(_this.data.desc, _width / 2, _height - parseInt(74 * scale));
//绘制日期
ctx.font = parseInt(20 * scale)
ctx.fillStyle = '#999'
ctx.textAlign = 'right'
c.drawTextVertical(ctx, _this.data.date, _width - parseInt(56 * scale), _height - parseInt(_this.data.len[_this.data.date.length] * scale))
c.drawTextVertical(ctx, _this.data.appName, _width - parseInt(92 * scale), _height - parseInt(98 * scale))
//绘制虚线边框
ctx.strokeStyle = '#ccc';
ctx.beginPath();
ctx.lineWidth = 1
ctx.setLineDash([2, 4]);
//顶部
ctx.moveTo(15, 15);
ctx.lineTo(_width - 15, 15);
//底部
ctx.moveTo(15, _height - 15);
ctx.lineTo(_width - 15, _height - 15);
//左边
ctx.moveTo(15, 15);
ctx.lineTo(15, _height - 15);
//右边
ctx.moveTo(_width - 15, 15);
ctx.lineTo(_width - 15, _height - 15);
ctx.stroke();
// 显示绘制
// ctx.draw()
//将生成好的图片保存到本地,需要延迟一会,绘制期间耗时
setTimeout(function () {
wx.canvasToTempFilePath({
canvas: canvas,
success: function (res) {
var tempFilePath = res.tempFilePath;
_this.setData({
loadImagePath: tempFilePath,
});
},
fail: function (res) {
console.log(res);
}
});
}, 500);
})
}
最后,再次给出效果图,扫描图片小程序码即可体验 ! ! !