引言
最近做了一个关于海报分享的功能,没有用相关的海报插件实现,我将详细用代码展示如何实现这一功能。 如果你们的海报也是前端实现的,希望能帮助到你~
注意事项
- 绘画时要注意不同手机展示的效果情况,建议可以默认用苹果6的大小来适配所有手机;
- canvas画布是2倍,所以后面绘画坐标的时候,有些会*2;
- 要注意绘画的顺序,所以建议绘画时涉及到异步的,都return promise;
- 针对文案过长时,需要做一步截取分行处理;
- 生成临时图片路径时,因为绘画需要点时间,建议可以加点loading过度下哦。
1.先定义canvas组件及相关数据
<!-- canvas海报 -->
<canvas id="poster" type="2d" class="cavas-wrapper" style="width: {{postWidth}}px; height: {{postHeight}}px"></canvas>
// 画布大小
const postWidth = 375;
const postHeight = 615;
let dpr = 1; // 分辨率;
// 默认样式
let defaultSyle = {
avatarSize: 126,
codeSize: 100,
top: 136,
nameSize: 36,
decSize: 30,
bgUrl: '/bg-default.png',
avatarUrl: '/default-avatar.png'
};
let description = '邀请你付费升级会员,尊享更多权益!';
let decWidth = 0; // 描述文案宽度
let nameWidth = 0; // 名字宽度
let customerName = ''; // 会员名字
let fixWidth = 275 * 2; // 文案之间的最大固定宽度,用于文案分行
/*如果想要看到canvas的画出来的实际效果,可以把此css注释*/
.cavas-wrapper {
position: absolute;
top: -10000rpx;
}
2.我这边主要绘画的有:背景图,头像,文案,太阳码
onLoad(options) {
this.handleCanvas();
},
//获取canvas实例
async handleCanvas() {
const query = wx.createSelectorQuery();
query.select('#poster')
.fields({ node: true, size: true })
.exec(async (res) => {
canvas = res[0].node;
ctx = canvas.getContext('2d');
// 初始化画布大小
canvas.width = postWidth * dpr;
canvas.height = postHeight * dpr;
ctx.scale(dpr / 2, dpr / 2);
let { userInfo, posterDetail } = this.data;
let { decSize, nameSize } = defaultSyle;
// 文案宽度
ctx.font = `${decSize}px sans-serif`;
decWidth = ctx.measureText(description).width;
console.log('decWidth===', decWidth);
// 会员名字宽度
ctx.font = `bold ${nameSize}px sans-serif`;
customerName = userInfo.customer_name;
nameWidth = ctx.measureText(customerName).width;
// 绘画背景图
await this.handleDrawBg();
// 绘制太阳码
await this.handleDrawCode();
// 绘制头像
await this.handleAvatar();
// 绘制姓名
this.handleCustomerName();
// 绘制宣传文案
this.handleDec();
}
});
},
// 绘制背景图
handleDrawBg() {
console.log('开始绘画背景-------')
const image = canvas.createImage();
image.src = defaultSyle.bgUrl;
return new Promise((resolve, reject) => {
// 图片加载完成回调
image.onload = () => {
// 开始处理背景图实现cover效果,这是避免如果用户没有按照一定的比例上传图片,背景图不会铺满
let sx, sy, sw, sh, imgRatio, canvasRatio;
canvasRatio = postWidth / postHeight
imgRatio = image.width / image.height;
// 图片宽高比 <= 画布宽高比时
if (imgRatio <= canvasRatio) {
sw = image.width
sh = sw / canvasRatio
sx = 0
sy = (image.height - sh) / 2
}
// 当图片宽高比 >= 画布宽高比时
else {
sh = image.height
sw = sh * canvasRatio
sx = (image.width - sw) / 2
sy = 0
};
// // 绘制背景图,因为画布他是默认2倍的,所以画在画布时,都要*2
ctx.drawImage(image, sx, sy, sw, sh, 0, 0, postWidth * 2, postHeight * 2)
console.log('背景画图完毕');
resolve();
};
})
},
// 绘制头像
handleAvatar() {
console.log('开始绘画头像-------');
const image = canvas.createImage();
let { userInfo } = this.data;
image.src = userInfo.head_image;
let avatarSize = defaultSyle.avatarSize; // 头像大小
// 计算圆的绘画位置,这里大家可以自行去调试,我这边的数据基本都是以居中为主。
let x1 = postWidth;
let y1 = defaultSyle.top + (avatarSize / 2) + 1;
// 计算头像的绘画位置
let x2 = x1 - (avatarSize / 2);
let y2 = y1 - (avatarSize / 2);
ctx.save();
ctx.beginPath();
ctx.arc(x1, y1, avatarSize / 2, 0, 2 * Math.PI); // 绘画圆形
ctx.clip();
return new Promise((resolve, reject) => {
// 图片加载完成回调
image.onload = () => {
ctx.drawImage(image, x2, y2, avatarSize, avatarSize);
ctx.restore();
console.log('头像画图完毕');
resolve();
};
})
},
// 绘制会员姓名
handleCustomerName() {
let { nameSize } = defaultSyle;
ctx.font = `bold ${nameSize}px sans-serif`;
ctx.fillStyle = '#222';
let textLen = 0; // 记录字符长度
if (nameWidth > fixWidth) {
console.log('名字超字符===');
// 开始处理字符截取
let overWidth = nameWidth - fixWidth;
textLen = Math.ceil(overWidth / nameSize);
let line1 = customerName.slice(0, customerName.length - textLen - 1);
let x = postWidth - (ctx.measureText(line1).width / 2);
let y = defaultSyle.top + defaultSyle.avatarSize + 70;
ctx.fillText(`${line1}...`, x, y);
} else {
console.log('名字不超字符===');
let x = postWidth - (nameWidth / 2);
let y = defaultSyle.top + defaultSyle.avatarSize + 70;
ctx.fillText(customerName, x, y);
}
},
// 绘制宣传文案
handleDec() {
let { decSize } = defaultSyle;
ctx.font = `${decSize}px sans-serif`;
ctx.fillStyle = '#6F6F6F';
let str = description;
let textLen = 0; // 记录字符长度
if (decWidth > fixWidth) {
console.log('宣传超出字符=====');
// 开始处理字符截取
let overWidth = decWidth - fixWidth;
textLen = Math.ceil(overWidth / decSize);
let line1 = str.slice(0, str.length - textLen);
let line2 = str.slice(str.length - textLen);
// 计算第一行文字的绘画位置
let x1 = postWidth - (ctx.measureText(line1).width / 2);
let y1 = defaultSyle.top + defaultSyle.avatarSize + 60 + 36 + 30;
// 计算第二行文字的绘画位置
let x2 = postWidth - (ctx.measureText(line2).width / 2);
let y2 = y1 + 40;
ctx.fillText(line1, x1, y1);
ctx.fillText(line2, x2, y2);
} else {
console.log('宣传不超出字符');
let x = postWidth - (decWidth / 2);
let y = defaultSyle.top + defaultSyle.avatarSize + 60 + 36 + 30;
ctx.fillText(str, x, y);
}
},
// 绘制太阳码
handleDrawCode() {
console.log('开始绘画码-------');
let {codeSize} = defaultSyle;
let codeUrl = ‘/code.png’;
const image = canvas.createImage();
image.src = codeUrl;
// 计算圆的绘画位置
let x1 = postWidth + (codeSize * 2)
let y1 = postHeight - 60 + (codeSize * 2);
// 计算头像绘画的位置
let x2 = x1 - codeSize;
let y2 = y1 - codeSize;
ctx.save();
ctx.beginPath();
ctx.fillStyle = '#fff';
ctx.arc(x1, y1, codeSize, 0, Math.PI * 2, false); // 绘画圆形
ctx.clip();
ctx.fill();
return new Promise((resolve, reject) => {
// 图片加载完成回调
image.onload = () => {
ctx.drawImage(image, x2, y2, codeSize * 2, codeSize * 2);
console.log('码画图完毕');
ctx.restore();
resolve();
};
})
},
3.获取canvas生成的临时图片路径
// 获取临时图片路径
hanldeGetCanvasTempFilePath(cb) {
wx.showLoading({
title: '图片生成中',
mask: true,
});
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: postWidth,
height: postHeight,
destWidth: canvas.width,
destHeight: canvas.height,
canvas,
success: (res) => {
console.log('res.tempFilePath=====', res.tempFilePath)
setTimeout(() => {
wx.hideLoading();
}, 1000)
},
fail: (err) => {
console.log('canvas生成图片失败===', err);
wx.showToast({
title: '图片生成失败',
icon: 'none',
duration: 1500,
})
},
});
},
End~