需求点
点某条打开日记的右上角,选择生成长图,到长图预览界面,可以切换背景图。 长图内容根据动态日记内容生成。
长图背景图原定,按比例缩放后在y轴上重复填充背景;后续考虑版权问题换成了背景给渐变色。 背景是图片的也会贴出来
效果图
代码片段
// 保存图片
saveImg() {
wx.showLoading({
title: '图片生成中',
})
this.createSelectorQuery()
.select('#posterCanvas')
.fields({
node: true,
size: true
})
.exec(this.drawPoster.bind(this));
},
// 获取图片信息
getImgInfo(imgURL) {
return new Promise((reslove) => {
wx.getImageInfo({
src: imgURL,
success(res) {
reslove(res);
}
})
})
},
// 绘制海报
async drawPoster(res) {
// 获取到 Canvas 对象
if (!res?.[0]?.node) {
wx.hideLoading();
return;
}
const canvas = res[0].node;
// 渲染上下文
const ctx = canvas.getContext('2d');
// 获取设备像素比
let winInfo = wx.getWindowInfo();
let dpr = winInfo.pixelRatio
const sysInfo = wx.getDeviceInfo();
// 避免安卓机挂掉 安卓机dpr最大为2
if (sysInfo.platform === 'android' && dpr > 2) {
dpr = 2;
}
let dvwidth = winInfo.windowWidth;
let dvheight = winInfo.windowHeight;
// 准备要渲染的元素信息 === start
let detail = this.data.dataSource; // 动态数据
// 日期时间
let createdTime_info = null;
if (detail?.diary_info?.dateStr) {
createdTime_info = this.getCreatedTimeRender(ctx, detail?.diary_info.dateStr, dvwidth);
}
// 星期几
let daysOfWeek_info = null;
if (detail?.diary_info?.daysOfWeek) {
daysOfWeek_info = this.getDaysOfWeekRender(ctx, detail?.diary_info.daysOfWeek, dvwidth, createdTime_info);
}
// 训练营名字
let campmissionname_info = null;
if (detail?.camp_name) {
campmissionname_info = this.getCampmissionNameRender(ctx, detail?.camp_name, dvwidth, createdTime_info);
}
// 第几次打开
let diaryCount_info = null;
// 渲染打卡次数时,该位置上面已绘制的最大y坐标
let diaryCount_top_y = Math.max((campmissionname_info?.y || 0) + (campmissionname_info?.height || 0) - (campmissionname_info?.lineHeight || 0), daysOfWeek_info?.y || 0);
if (detail?.diary_seq) {
diaryCount_info = this.getDiaryCountRender(ctx, detail?.diary_seq, diaryCount_top_y);
}
// 日记标题
let diaryTitle_info = null;
let diaryTitle_top_y = Math.max(diaryCount_top_y || 0, (diaryCount_info?.y || 0) + (diaryCount_info?.height || 0) - (diaryCount_info?.lineHeight || 0));
if (detail?.diary_info?.title) {
diaryTitle_info = this.getDiaryTitleRender(ctx, detail?.diary_info?.title, dvwidth, diaryTitle_top_y);
}
// 日记内容
let diaryContent_info = null;
let diaryContent_top_y = Math.max(diaryTitle_top_y || 0, (diaryTitle_info?.y || 0) + (diaryTitle_info?.height || 0) - (diaryTitle_info?.lineHeight || 0));
if (detail?.diary_info?.content) {
diaryContent_info = this.getDiaryContentRender(ctx, detail?.diary_info?.content, dvwidth, diaryContent_top_y);
}
// 日记图片=== 九宫格展示,固定显示区域宽高,实现image的aspectFill模式
let diaryImg_info = null;
let diaryImg_top_y = Math.max(diaryContent_top_y || 0, (diaryContent_info?.y || 0) + (diaryContent_info?.height || 0) - (diaryContent_info?.lineHeight || 0));
if (detail?.picture_url_list?.length) {
diaryImg_info = await this.getDiaryImgRender(canvas, detail?.picture_url_list, dvwidth, diaryImg_top_y);
}
// 赞和评论
let zanComment_top_y = Math.max(diaryImg_top_y || 0, (diaryImg_info?.y || 0) + (diaryImg_info?.height || 0));
let zanComment_info = this.getZanCommentRender(ctx, canvas, {comment_list: detail?.comment_list, like_user_list: detail?.like_user_list}, dvwidth, zanComment_top_y);
// 打卡人
// 星期几
let userName_info = null;
let userNametop_y = Math.max(zanComment_top_y || 0, (zanComment_info?.y || 0) + (zanComment_info?.height || 0));
if (detail?.user_name) {
userName_info = this.getUserNameRender(ctx, '——' + detail?.user_name, dvwidth, userNametop_y);
}
// 准备要渲染的元素信息 === end
// 初始化画布大小
let calcHeight = userName_info?.y ? userName_info?.y * 1 + 30 : dvheight;
calcHeight = Math.max(calcHeight, dvheight + 100);
if (calcHeight * dpr < this.data.canvasMaxHeight) {
// ====高清 ===start
canvas.width = dvwidth * dpr;
canvas.height = calcHeight * dpr;
ctx.scale(dpr, dpr);
// ====高清 ===end
} else {
// 普通 --- start
canvas.width = dvwidth;
canvas.height = calcHeight;
// 普通 --- end
}
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制画布底色
// const gradient = ctx.createRadialGradient(canvas.width/2, canvas.height/2, canvas.width/2, canvas.width/2, canvas.height/2, canvas.height/2);
let gradient = ctx.createLinearGradient(0, 0, 0, calcHeight <= dvheight ? calcHeight: canvas.height);
// 添加三个色标
gradient.addColorStop(0, "white");
gradient.addColorStop(0.2, this.data.curBgUrl);
gradient.addColorStop(0.8, this.data.curBgUrl);
gradient.addColorStop(1, "white");
// 设置填充样式并绘制矩形
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 开始绘制
// 第一步--绘制背景图
// await this.drawerBgImg(ctx, canvas, dvwidth)
if (campmissionname_info) {
let fontInfo= {
fontColor: campmissionname_info?.fontColor,
fontSize: campmissionname_info?.fontSize
}
this.drawerMutiText(ctx, campmissionname_info?.content, campmissionname_info?.x, campmissionname_info?.y, fontInfo, campmissionname_info?.lineHeight);
}
if (createdTime_info) {
ctx.fillStyle = createdTime_info?.fontColor || '#333';
ctx.font = `${createdTime_info?.fontSize} ${this.data.fontFamily}`;
ctx.fillText(createdTime_info?.content, createdTime_info?.x, createdTime_info?.y);
}
if (daysOfWeek_info) {
ctx.fillStyle = daysOfWeek_info?.fontColor || '#333';
ctx.font = `${daysOfWeek_info?.fontSize} ${this.data.fontFamily}`;
ctx.fillText(daysOfWeek_info?.content, daysOfWeek_info?.x, daysOfWeek_info?.y);
}
if (diaryCount_info) {
ctx.fillStyle = diaryCount_info?.fontColor || '#333';
ctx.font = `${diaryCount_info?.fontSize} ${this.data.fontFamily}`;
ctx.fillText(diaryCount_info?.content1, diaryCount_info?.x1, diaryCount_info?.y);
ctx.font = `bold ${diaryCount_info?.fontSize2} ${this.data.fontFamily}`;
ctx.fillText(diaryCount_info?.content2, diaryCount_info?.x2, diaryCount_info?.y);
ctx.font = `${diaryCount_info?.fontSize} ${this.data.fontFamily}`;
ctx.fillText(diaryCount_info?.content3, diaryCount_info?.x3, diaryCount_info?.y);
}
if (diaryTitle_info) {
let fontInfo= {
fontColor: diaryTitle_info?.fontColor,
fontSize: diaryTitle_info?.fontSize,
fontWeight: diaryTitle_info?.fontWeight
}
this.drawerMutiText(ctx, diaryTitle_info?.content, diaryTitle_info?.x, diaryTitle_info?.y, fontInfo, diaryTitle_info?.lineHeight);
}
if (diaryContent_info) {
let fontInfo= {
fontColor: diaryContent_info?.fontColor,
fontSize: diaryContent_info?.fontSize
}
this.drawerMutiText(ctx, diaryContent_info?.content, diaryContent_info?.x, diaryContent_info?.y, fontInfo, diaryContent_info?.lineHeight);
}
if (diaryImg_info && diaryImg_info?.content) {
let images1 = diaryImg_info?.content || [];;
// 所有图片都加载完毕,此时可以进行绘制操作
images1.forEach((image, index) => {
let dheight = diaryImg_info?.imgh || 0; // 单个图品固定高
let dwidth = diaryImg_info?.imgw || 0; // 单个图品固定宽
let ya = diaryImg_info?.y || 0; // 在canvas上的y坐标
if (index >= 3 && index < 6) {
ya += dheight;
} else if (index >= 6 && index < 9) {
ya += dheight * 2;
}
let xa = (diaryImg_info?.x || 0) + (index % 3) * dwidth; // 在canvas上的x坐标
let originW = image.width;
let originH = image.height;
let scale = Math.max(dwidth / originW, dheight / originH);
let dw = dwidth / scale;
let dh = dheight / scale;
ctx.drawImage(image, (originW - dw) / 2,(originH - dh) / 2, dw, dw, xa, ya, dwidth, dheight);
})
}
if (zanComment_info) {
let images2 = await Promise.all(zanComment_info?.icons);
images2.forEach((image, index) => {
if (index == 0) {
ctx.drawImage(image, zanComment_info?.x1, zanComment_info?.y, zanComment_info?.iconsize, zanComment_info?.iconsize)
} else if (index == 1) {
ctx.drawImage(image, zanComment_info?.x3, zanComment_info?.y, zanComment_info?.iconsize, zanComment_info?.iconsize)
}
})
ctx.fillStyle = zanComment_info?.fontColor || '#333';
ctx.font = `${zanComment_info?.fontSize} ${this.data.fontFamily}`;
ctx.fillText(zanComment_info?.content2, zanComment_info?.x2, zanComment_info?.yTxt);
ctx.fillText(zanComment_info?.content4, zanComment_info?.x4, zanComment_info?.yTxt);
}
if (userName_info) {
ctx.fillStyle = userName_info?.fontColor || '#333';
ctx.font = `${userName_info?.fontSize} ${this.data.fontFamily}`;
ctx.fillText(userName_info?.content, userName_info?.x, userName_info?.y);
}
// 绘制完成
setTimeout(()=>{
this.canvasToImage(canvas);
}, 500);
},
// canvas生成图片
canvasToImage(canvas) {
let that = this;
wx.canvasToTempFilePath({
canvas,
canvasId: 'posterCanvas',
success: tplRes => {
wx.hideLoading();
if (tplRes.tempFilePath) {
wx.saveImageToPhotosAlbum({
filePath: tplRes.tempFilePath,
success(res) {
that.onCancel();
wx.showToast({
title: '已保存到相册',
icon: 'success',
duration: 3000
})
},
fail(error) {
// console.log('===asdas', error)
}
})
}
},
fail: error => {
console.log('==error=', error)
}
})
},
// 绘制背景图,按比例缩放后y轴重复
async drawerBgImg(ctx, canvas, dvwidth) {
// 解析活动背景图片
let posterImgURL = this.data.curBgUrl;
// 获取源图片宽高
let posterImgInfo = await this.getImgInfo(posterImgURL)
// 计算源图片的宽高比
let posterImgRate = posterImgInfo.width / posterImgInfo.height
// 计算出新的图片宽高
let bgnewWidth = dvwidth;
let bgnewHeight = bgnewWidth / posterImgRate;
let repeatNCount = Math.ceil(canvas.height / bgnewHeight); //背景图重复次数,向上取整
let bgPromiseList = [];
for (let j = 0; j < repeatNCount; j++) {
let prs = new Promise((resolve, reject) => {
let tempimg = canvas.createImage();
tempimg.src = posterImgURL;
tempimg.onload = function() {
resolve(tempimg)
}
})
bgPromiseList.push(prs);
}
let bgimages = await Promise.all(bgPromiseList);
// 所有图片都加载完毕,此时可以进行绘制操作
bgimages.forEach((image, index) => {
ctx.drawImage(image, 0, 0 + index * bgnewHeight, bgnewWidth, bgnewHeight)
})
},
// 打卡创建日期
getCreatedTimeRender(ctx, text, dvwidth) {
let fontSize = '15px';
let fontColor = '#333';
ctx.fillStyle = fontColor;
ctx.font = `${fontSize} ${this.data.fontFamily}`;
let width = ctx.measureText(text).width || 0; // 文本自己宽度
let obj = {
x: dvwidth - this.data.canvasLrPadding - width,
y: this.data.canvasTpPadding + 15, // 20--指字体大小是20px时文本大概高度
content: text,
width,
height: 20,
fontColor, // 后面改成根据背景色取
fontSize
}
return obj;
},
// 打卡星期几
getDaysOfWeekRender(ctx, text, dvwidth, createdTime_info) {
let fontSize = '15px';
let fontColor = '#333';
ctx.fillStyle = fontColor;
ctx.font = `${fontSize} ${this.data.fontFamily}`;
let width = ctx.measureText(text).width || 0; // 文本自己宽度
let height = 20; // 文本自己高度---不准确
let createdTime_w = createdTime_info?.width || 0;
let createdTime_y = createdTime_info?.y || 0;
let obj = {
x: dvwidth - this.data.canvasLrPadding - width - (createdTime_w - width) / 2,
y: createdTime_y + 10 + height, // 20--指字日期和星期几上下间距
content: text,
width,
height,
fontColor, // 后面改成根据背景色取
fontSize
}
return obj;
},
// 训练营名字
getCampmissionNameRender(ctx, text, dvwidth, createdTime_info) {
let rgtW = createdTime_info?.width || 0;
let w = dvwidth - this.data.canvasLrPadding * 2 - rgtW - 25; // 20是训练营名字很多时-和日期之间水平间距
let lineHeight = 25;
let fontObj = {
fontColor: '#333', // 后面改成根据背景色取
fontSize: '20px',
lineHeight,
}
let rows = this.getMutiTextRows(ctx, text, w, fontObj);
let obj = {
x: this.data.canvasLrPadding,
y: this.data.canvasTpPadding + 15, // 20--指字体大小是20px时文本大概高度
content: rows,
width: w,
height: rows.length * lineHeight,
...fontObj
}
return obj;
},
// 获取多行文本:切成行数组
getMutiTextRows(ctx, text,w,fontObj, rownum) {
if (!text) return [];
//自动换行介绍
let temp = '';
let rows = [];
let chartArr = text.split('');
ctx.fillStyle = fontObj?.fontColor || '#333';
ctx.font = `${fontObj?.fontWeight ? (fontObj?.fontWeight + ' ') : ''}${fontObj?.fontSize} ${this.data.fontFamily}`;
for (var a = 0; a < chartArr.length; a++) {
if (ctx.measureText(temp).width < w) {
} else {
rows.push(temp);
temp = '';
}
temp += chartArr[a];
}
rows.push(temp);
if (rownum && rows.length > rownum) {
rows = rows.slice(0, rownum);
let lastChart = rows[rownum - 1];
rows[rownum - 1] = lastChart?.substring(0, lastChart - 2) + '...'
}
return rows
},
// 获取 第几次打卡的渲染信息
getDiaryCountRender(ctx, count, hasy) {
let x1 = this.data.canvasLrPadding;
let fontSize = '18px';
let fontSize2 = '28px';
ctx.font = `${fontSize} ${this.data.fontFamily}`;
let x1w = ctx.measureText('第').width;
let x2 = x1 + x1w;
ctx.font = `bold ${fontSize2} ${this.data.fontFamily}`;
let x2w = ctx.measureText(count).width || 0;
let x3 = x2 + x2w;
ctx.font = `${fontSize} ${this.data.fontFamily}`;
let x3w = ctx.measureText('次打卡').width;
let width = x1w + x2w + x3w;
let lineHeight = 20;
return {
x1,
y: hasy + 20 + lineHeight, // 25---指的是:第几次打卡和上面最近一个元素的垂直间距
x2,
x3,
width,
height: lineHeight,
lineHeight,
content1: '第',
content2: count,
content3: '次打卡',
fontColor: '#333', // 后面改成根据背景色取
fontSize,
fontSize2,
fontWeight2: 'bold'
}
},
// 日记标题
getDiaryTitleRender(ctx, text, dvwidth, hasy) {
let lineHeight = 25;
let x = this.data.canvasLrPadding;
let y = hasy + 30; // 30-日记标题距离上面最近一个元素的垂直间距
let w = dvwidth - this.data.canvasLrPadding * 2; // 20是训练营名字很多时-和日期之间水平间距
let fontObj = {
fontColor: '#333', // 后面改成根据背景色取
fontSize: '20px',
fontWeight: 'bold'
}
let rows = this.getMutiTextRows(ctx, text, w, fontObj);
let height = rows.length * lineHeight;
return {
x,
y,
content: rows,
width: w,
height,
lineHeight,
...fontObj
}
},
// 日记内容
getDiaryContentRender(ctx, text, dvwidth, hasy) {
let w = dvwidth - this.data.canvasLrPadding * 2; // 20是训练营名字很多时-和日期之间水平间距
let lineHeight = 20;
let fontObj = {
fontColor: '#333', // 后面改成根据背景色取
fontSize: '16px',
lineHeight
}
let rows = [];
let lines = text?.split('\n') || [];
lines.forEach(el => {
let ite = this.getMutiTextRows(ctx, el, w, fontObj);
ite.length && rows.push(...ite);
; })
let height = rows.length * lineHeight;
return {
x: this.data.canvasLrPadding,
y: hasy + 30, // 20--日记内容和标题垂直间距
content: rows,
width: w,
height,
...fontObj
}
},
// 日记图片
async getDiaryImgRender(canvas, imageUrls, dvwidth, hasy) {
let that = this;
let imgw = 98;
let imgh = 98;
const promises = imageUrls.map(url => {
return new Promise((resolve, reject) => {
let tempimg = canvas.createImage();
tempimg.src = url;
tempimg.onload = function() {
resolve(tempimg)
}
})
})
let images1 = await Promise.all(promises);
return {
x: this.data.canvasLrPadding + 20, // 20-图片的左边距比整体多
y: hasy + 20, // 20--日记内容和标题垂直间距;整体图片区域的 y坐标
content: images1,
width: dvwidth - this.data.canvasLrPadding * 2 - 20,
height: Math.ceil(imageUrls.length / 3) * imgh,
fontColor: '#222', // 后面改成根据背景色取
fontSize: '16px',
imgw,
imgh
}
},
// 打卡人
getUserNameRender(ctx, text, dvwidth, hasy) {
let fontSize = '16px';
let fontColor = '#333';
ctx.fillStyle = fontColor;
ctx.font = `${fontSize} ${this.data.fontFamily}`;
let width = ctx.measureText(text).width;
let lineHeight = 20;
return {
x: dvwidth - this.data.canvasLrPadding - width,
y: hasy + 20, // 20--垂直间距
content: text,
width: width,
height: lineHeight,
lineHeight,
fontSize,
fontColor
}
},
// 多行文本绘制
drawerMutiText(ctx, rows, x, y, fontObj, lineHeight) {
ctx.fillStyle = fontObj?.fontColor || '#333';
ctx.font = `${fontObj?.fontWeight ? (fontObj?.fontWeight + ' ') : ''}${fontObj?.fontSize} ${this.data.fontFamily}`;
for (let i = 0; i < rows.length; i++) {
let rtext = rows[i];
ctx.fillText( rtext|| '', x, y + i* lineHeight);
}
},
// 赞和评论数
getZanCommentRender(ctx, canvas, data, dvwidth, hasy) {
let fontSize = '14px';
let fontColor = '#333';
ctx.fillStyle = fontColor;
ctx.font = `${fontSize} ${this.data.fontFamily}`;
let iconsize = 25;
let x1 = this.data.canvasLrPadding + 20; // 20-图片的左边距比整体多
let x1w = iconsize;
let x2 = x1 + x1w + 2;
let x2text = data?.like_user_list?.length ;
let x2w = ctx.measureText(x2text)?.width || 0;
let x3 = x2 + x2w + 30;
let x3w = iconsize;
let x4 = x3 + x3w + 4;
let x4text = data?.comment_list?.length;
let x4w = ctx.measureText(x2text)?.width || 0;
let icons = this.data.zancommenticons.map(url => {
return new Promise((resolve, reject) => {
let tempimg = canvas.createImage();
tempimg.src = url;
tempimg.onload = function() {
resolve(tempimg)
}
})
})
return {
x1,
x2,
x3,
x4,
iconsize,
icons,
height: iconsize,
y: hasy + 20,
yTxt: hasy + 20 + 10 + 10,
content2: x2text,
content4: x4text,
fontColor,
fontSize
}
}