微信小程序使用canvas生成分享海报功能复盘
创始人
2024-05-21 12:55:34
0

前言

近期需要开发一个微信小程序生成海报分享的功能。在h5一般都会直接采用 html2canvas 或者 dom2image 之类的库直接处理。但是由于小程序不具备传统意义的dom元素,所以也没有办法采用此类工具。
所以就只能一笔一笔的用 canvas 画出来了,下面对实现这个功能中遇到的问题做一个简单的复盘。

制作要求:

  • 主题切换。
  • 图片弹框展示,适应不同的手机尺寸。
  • 图片上层有弹出框展示保存图片按钮。
  • 海报内容,
    • 标题部分根据实际内容展示,可能为一行也可能为两行
    • 描述部分,最多展示四行,超出的显示成…
    • 圆角图片展示
    • 圆角虚线框

基本方案流程

  1. 预先加载好所有需要的图片。
  2. 在偏离视窗显示区域使用 canvas 绘制海报,并生成临时文件。
  3. 弹窗的图片使用 生成的临时图片。
  4. 设置图片的宽度为适应屏幕的,可通过定位或者flex来实现,图片高度根据宽度自动缩放。超出的内容滚动显示。

效果图如下:
在这里插入图片描述
在这里插入图片描述

微信canvas组件的相关问题

canvas 属于微信客户端创建的原生组件,所以需要注意一些原生组件的限制

  • 原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。
    • 后插入的原生组件可以覆盖之前的原生组件。
  • 原生组件还无法在 picker-view 中使用
  • 部分 CSS 样式无法应用于原生组件
    • 无法对原生组件设置 CSS 动画
    • 无法定义原生组件为 position: fixed
    • 不能在父级节点使用 overflow: hidden 来裁剪原生组件的显示区域

所以无法使用 canvas 绘制的图片直接用于显示。会遇到层级以及尺寸的问题。

预加载图片资源

在绘制之前我们需要先加载好图片资源并保存。

function create(){const img1 = preLoadImg("https:xxxx.img1", 'img1')const img2 = preLoadImg("https:xxxx.img2", 'img2')const img3 = preLoadImg("https:xxxx.img3", 'img3')Promise.all([img1, img2, img3]).then(res=>{// 开始绘制canvas})
}function preLoadImg(url, taskId) {if(this.imageTempPath[taskId]) return Promise.resolve();if (!url) return Promise.resolve();url = /^https/.test(url) ? url : `https:${url}`;return wx.getImageInfo({src: url}).then((res)=>{this.imageTempPath[taskId] = res.path;})
}

文本处理

计算不同长度的文本绘制高度

对于不同的文本长度,可能存在占一行或者多行的情况,这个时候对于文本以下的内容绘制的 y 轴坐标会造成影响。

解决方案:先定义好每一个元素在标准情况下的坐标位置,然后对于存在可能有占据空间改变的文本,通过测量其文本宽度,计算出实际占据行数,然后出多出的 y 轴位置(diff),并在后续的元素绘制上加上这个差值。

基本思路:

  1. 测量出文本的实际绘制需要的总长度
  2. 计算出实际绘制多少行
  3. 计算实际绘制行数与默认行数的高度差

计算方法如下:

function getWordDiffDistance(ctx,        // canvas 上下文text,       // 要计算的文本baseline,   // 默认显示行数lineHeight, // 行高fontSize,   // 字号textIndent, // 首行缩进字符maxWidth,   // 每一行绘制的最大宽度maxLine     // 最大允许显示行数
) {// 设置上下文的字号ctx.setFontSize(fontSize);// 首行缩进的宽度const textIndentWidth = fontSize * textIndent;//实际总共能分多少行let allRow = Math.ceil((ctx.measureText(text).width + textIndentWidth) / maxWidth);allRow = Math.min(allRow, maxLine);return (allRow - baseline) * lineHeight;
}

ctx.measureText() 要先设置好文本属性。

文本超出指定行数后显示 …

基本思路:

  1. 设置好 canvas 上下文的文字样式
  2. 通过 measureText 计算出当前文本需要绘制多少行
  3. 如果是首行且设置了首行缩进,绘制的 x 要加上缩进的宽度
  4. 然后计算出每一行要绘制的文字并进行绘制,并记录最后的截取位置
  5. 如果最后一行的实际绘制宽度大于设置的最大宽度,添加… 否则正常绘制
dealWords(options) {const {ctx,fontSize,word,maxWidth,x,y,maxLine,lineHeight,style,textIndent = 0,} = options;ctx.font = style || "normal 12px PingFangSC-Regular";//设置字体大小ctx.setFontSize(fontSize);// 首行缩进的宽度const textIndentWidth = fontSize * textIndent;//实际总共能分多少行let allRow = Math.ceil((ctx.measureText(word).width + textIndentWidth) / maxWidth);//实际能分多少行与设置的最大显示行数比,谁小就用谁做循环次数let count = allRow >= maxLine ? maxLine : allRow;//当前字符串的截断点let endPos = 0;for (let j = 0; j < count; j++) {let startWidth = 0;if (j == 0 && textIndent) startWidth = textIndentWidth;let rowRealMaxWidth = maxWidth - startWidth;//当前剩余的字符串let nowStr = word.slice(endPos);//每一行当前宽度let rowWid = 0;if (ctx.measureText(nowStr).width > rowRealMaxWidth) {//如果当前的字符串宽度大于最大宽度,然后开始截取for (let m = 0; m < nowStr.length; m++) {//当前字符串总宽度rowWid += ctx.measureText(nowStr[m]).width;if (rowWid > rowRealMaxWidth) {if (j === maxLine - 1) {//如果是最后一行ctx.fillText(nowStr.slice(0, m - 1) + "...",x + startWidth,y + (j + 1) * lineHeight); //(j+1)*18这是每一行的高度} else {ctx.fillText(nowStr.slice(0, m),x + startWidth,y + (j + 1) * lineHeight);}endPos += m; //下次截断点break;}}} else {//如果当前的字符串宽度小于最大宽度就直接输出ctx.fillText(nowStr.slice(0), x, y + (j + 1) * lineHeight);}}
}

绘制多行文本计算行宽的时候,空白字符可能会对最终的计算结果造成一定影响,所以可以先对其空白字符进行过滤。

图文对齐

微信小程序中通过 setTextBaseline 设置文本竖直对齐方式。可选值有 top,bottom,middle,normal;

在这里插入图片描述

图片的坐标基点为左上角坐标,所以在绘制的时候要注意 y 的起始坐标。如果有修改 文本的对齐方式,在结束的时候最好将文本竖直对齐方式设置为 normal,避免影响后续的绘制。

形状处理

绘制圆角矩形路径

使用arc()方式绘制弧线
在这里插入图片描述

// 按照canvas的弧度从 0 - 2PI 开始顺时针绘制
function drawRoundRectPathWithArc(ctx, x, y, width, height, radius) {ctx.beginPath();// 从右下角顺时针绘制,弧度从0到1/2PIctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2);// 矩形下边线ctx.lineTo(x + radius, y + height);// 左下角圆弧,弧度从1/2PI到PIctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI);// 矩形左边线ctx.lineTo(x, y + radius);// 左上角圆弧,弧度从PI到3/2PIctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);// 上边线ctx.lineTo(x + width - radius, y);//右上角圆弧ctx.arc(x + width - radius,y + radius, radius, (Math.PI * 3) / 2, Math.PI * 2);//右边线ctx.lineTo(x + width, y + height - radius);ctx.closePath();
}

使用arcTo()方式绘制弧线

function drawRoundRectPathWithArcTo(ctx, x, y, width, height, radius) {ctx.beginPath();// 上边线ctx.lineTo(x + width - radius, y);// 右上弧线ctx.arcTo(x + width, y, x + width, y + radius, radius)//右边线ctx.lineTo(x + width, y + height - radius);// 从右下角顺时针绘制,弧度从0到1/2PIctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)// 矩形下边线ctx.lineTo(x + radius, y + height);// 左下角圆弧,弧度从1/2PI到PIctx.arcTo(x, y + height, x, y +height -radius, radius)// 矩形左边线ctx.lineTo(x, y + radius);// 左上角圆弧,弧度从PI到3/2PIctx.arcTo(x,y, x+ radius, y, radius)ctx.closePath();
}

背景色填充

function fillRoundRectPath(ctx, x, y, width, height, radius, color){ctx.save();this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);ctx.setFillStyle(color);ctx.fill();ctx.restore();
}

图片填充

function drawRoundRectImg(ctx, x, y, width, height, radius, img) {if(!img) returnctx.save();this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);// 剪切  原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内ctx.clip();ctx.drawImage(img, x, y, width, height);ctx.restore();
}

虚线框

function strokeRoundRectPath(ctx, x, y, width, height, radius) {this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);ctx.strokeStyle = "#DDDDDD";ctx.lineWidth = 0.5;ctx.setLineDash([6, 5]);ctx.stroke();
}

生成临时图片

wx.canvasToTempFilePath(Object object, Object this)

把当前画布指定区域的内容导出生成指定大小的图片。在 draw() 回调里调用该方法才能保证图片导出成功。

ctx.draw(false, async () => {// canvas画布转成图片并返回图片地址const { tempFilePath } = await wx.canvasToTempFilePath({x: 0,       // 指定的画布区域的左上角横坐标	y: 0,       // 指定的画布区域的左上角纵坐标width: posterImg_width,     // 指定的画布区域的宽度	height: posterImg_height,   // 指定的画布区域的高度destWidth: posterImg_width * pixelRatio, // 输出的图片的宽度 导出大小为 canvas 的 pixelRatio 倍destHeight: posterImg_height * pixelRatio, // 输出的图片的高度 canvasId: "posterCanvas",},this);this.posterTempFilePath = tempFilePath;
});

不同像素手机的显示适配问题

由于只是一张图片的展示,所以显示适配的问题久很好解决。

  • 设置图片父层容器的侧边距,使容器自动撑开。
  • 图片宽度设置为 width:100%, 设置 mode="widthFix"让图片自动缩放。

微信本地保存临时图片

function savePoster(tempFilePath) {wx.saveImageToPhotosAlbum({filePath: tempFilePath,}).then(()=> {wx.showToast({title: "保存成功", // 提示的内容,icon: "success", // 图标,duration: 2000, // 延迟时间,mask: true, // 显示透明蒙层,防止触摸穿透,});},(err) => {wx.showToast({title: "保存失败", // 提示的内容,icon: "none", // 图标,duration: 2000, // 延迟时间,mask: true, // 显示透明蒙层,防止触摸穿透,});},);
}

主题切换

通过替换不同的背景图片来切换不同的主题。

参考文章

说说如何使用 Canvas 绘制弧线与曲线

canvas生成分享海报

相关内容

热门资讯

毕业典礼主持词 毕业典礼主持词(合集15篇)  主持人在台上表演的灵魂就表现在主持词中。在当今不断发展的世界,主持成...
新婚回门的宴会主持词 新婚回门的宴会主持词尊敬的各位来宾:  大家好!  今天是20XX年2月13日,农历:乙卯年正月十一...
春季订货会的主持词 春季订货会的主持词各位领导、各位经理、来宾、朋友们:  今天我们相聚在这里,共同迎来了四海商贸公司的...
电影《画壁》经典台词 电影《画壁》经典台词  1、牡丹:如果我愿意跟你一起走,你会和我一生一世吗?  2、牡丹:没有爱 会...
中秋对客户的感谢词 中秋对客户的感谢词亲爱的客户朋友们:  金秋九月,硕果累累,又是一年一度的中秋佳节,又是一年一度的月...
致新生的欢迎词 致新生的欢迎词亲爱的新同学:你们好!秋风送爽,丹桂飘香,又是一个流金岁月,又是一个收获季节。踏着习习...
追悼会家属答谢词 追悼会家属答谢词尊敬的各位领导,各位亲朋好友,感谢各位今天出席亡母的追悼会。在母亲生病住院期间,承蒙...
重阳节活动主持词开场白 重阳节活动主持词开场白  在这金秋送爽,硕果累累的时节,我们迎来了又一个九九重阳节。下面是小编精心为...
郭德纲相声小段子台词 郭德纲相声小段子台词  相声,一种民间说唱曲艺。它以说,学,逗,唱为形式,突出其特点。下面是小编整理...
《将夜》经典台词 《将夜》经典台词  1.这片海洋,当时这里还有日出,在阳光的照射下,这片海洋是透明的',看上去就像是...
晚会主持词 【实用】晚会主持词(精选17篇)  主持词是主持人在节目进行过程中用于串联节目的串联词。在如今这个时...
春节晚会主持词 给力春节晚会主持词(通用3篇)  主持词的写作需要将主题贯穿于所有节目之中。在现今人们越来越重视活动...
婚礼上领导致辞 婚礼上领导致辞(通用7篇)  在日复一日的学习、工作或生活中,大家都经常接触到致辞吧,致辞要求风格的...
晚会结束语 晚会结束语(通用13篇)  闭幕词是一些大型会议结束时由有关领导人或德高望重者向会议所作的讲话。具有...
介绍毕业典礼舞蹈追梦的主持词 介绍毕业典礼舞蹈追梦的主持词(精选6篇)  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观...
员工誓师大会主持词 员工誓师大会主持词  誓师大会,又名 造势大会,两者皆可以称为“誓师会”,“造势会”,不过如此公共关...
教职工运动会入场式解说词 教职工运动会入场式解说词  在快速变化和不断变革的今天,我们可以使用解说词的机会越来越多,解说词让观...
央视春晚小品的经典台词 央视春晚小品的经典台词  小品《快乐老爸》  为了狗大点事,你还要得学门外语呀?  他是用泪水洗刷自...
《遇见王沥川》的经典台词 《遇见王沥川》的经典台词  1、爱情是干渴的,除非你遇上一个像沥川那样的.男人。  2、爱情是进行时...
回门宴主持词开场白   回门宴主持词开场白  亲爱的各位来宾,各位亲朋好友,先生们,女士们大家上午好! 玉兔奔月去,祥龙...