共计 4370 个字符,预计需要花费 11 分钟才能阅读完成。
海报生成示例
最近 智酷君 在做[小程序]canvas 生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。
原型图
这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。
<center> 海报生成流程 </center>
下面分享下主要的代码内容和“填坑现场”:
一、添加字体
https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html
canvasContext.font = value // 示例 | |
ctx.font = `normal bold 20px sans-serif`// 设置字体大小,默认 10 | |
ctx.setTextAlign('left'); | |
ctx.setTextBaseline("top"); | |
ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)// 绘制文本 |
符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif
文字过长在 canvas 下换行问题处理(最多两行,超过“…”代替)
ctx.setTextAlign('left'); | |
ctx.setFillStyle('#000');// 文字颜色:默认黑色 | |
ctx.font = `normal bold 18px sans-serif`// 设置字体大小,默认 10 | |
let canvasTitleArray = canvasTitle.split(""); | |
let firstTitle = ""; // 第一行字 | |
let secondTitle = ""; // 第二行字 | |
for (let i = 0; i < canvasTitleArray.length; i++) {let element = canvasTitleArray[i]; | |
let firstWidth = ctx.measureText(firstTitle).width; | |
//console.log(ctx.measureText(firstTitle).width); | |
if (firstWidth > 260) {let secondWidth = ctx.measureText(secondTitle).width; | |
// 第二行字数超过,变为... | |
if (secondWidth > 260) { | |
secondTitle += "..."; | |
break; | |
} else {secondTitle += element;} | |
} else {firstTitle += element;} | |
} | |
// 第一行文字 | |
ctx.fillText(firstTitle, 20, 278, 280)// 绘制文本 | |
// 第二行问题 | |
if (secondTitle) {ctx.fillText(secondTitle, 20, 300, 280)// 绘制文本 | |
} |
通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。
(一行字允许宽度为 280 时,判断需要写小点,比如 260)
二、获取临时地址并设置图片
let mainImg = "https://demo.com/url.jpg"; | |
wx.getImageInfo({ | |
src: mainImg,// 服务器返回的图片地址 | |
success: function (res) { | |
// 处理图片纵横比例过大或者过小的问题!!!let h = res.height; | |
let w = res.width; | |
let setHeight = 280, // 默认源图截取的区域 | |
setWidth = 220; // 默认源图截取的区域 | |
if (w / h > 1.5) { | |
setHeight = h; | |
setWidth = parseInt(280 / 220 * h); | |
} else if (w / h < 1) { | |
setWidth = w; | |
setHeight = parseInt(220 / 280 * w); | |
} else { | |
setHeight = h; | |
setWidth = w; | |
}; | |
console.log(setWidth, setHeight) | |
ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); | |
ctx.draw(true); | |
}, | |
fail: function (res) {// 失败回调} | |
}); |
在开发过程中如果封面图无法按照约定的比例(280×220)给到:
那么我们就需要处理默认封面图过大或者过小的问题,大致思路是 :代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度 100%,过宽的图是高度 100%。
在 canvas 中 draw 图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的 临时路径 。
微信官方提供两个 API:
wx.downloadFile(OBJECT)和 wx.getImageInfo(OBJECT)。都需先配置 download 域名 才能生效。
三、裁切“圆形”头像画图
ctx.save(); // 保存画图板 | |
ctx.beginPath()// 开始创建一个路径 | |
ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)// 画一个圆形裁剪区域 | |
ctx.clip()// 裁剪 | |
ctx.closePath(); | |
ctx.drawImage(headImageLocal, 20, 10, 30, 30); | |
ctx.draw(true); | |
ctx.restore()// 恢复之前保存的绘图上下文 |
使用图形上下文的不带参数的 clip()方法来实现 Canvas 的图像裁剪功能。该方法使用路径来对 Canvas 话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用 clip()方法来设置裁剪区域。
需要注意的是 裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小 ,也就是说画布是越切越小的,要想保证最后仍然能在 canvas 最初定义的大小下绘图需要注意save() 和 restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~
小程序 canvas 裁切 BUG
ctx.setFillStyle("#fff"); | |
ctx.fillRect(0, 0, 320, 500); // 第一个填充矩形 | |
wx.downloadFile({ | |
url: headUri, | |
success(res) {ctx.beginPath() | |
ctx.arc(50, 50, 25, 0, 2 * Math.PI) | |
ctx.clip() | |
ctx.drawImage(res.tempFilePath, 25, 25); // 第二个填充图片 | |
ctx.draw() | |
ctx.restore() | |
ctx.setFillStyle("#fff"); | |
ctx.fillRect(0, 0, 320, 500); | |
ctx.draw(true) | |
ctx.restore()} | |
}) |
clip 裁切这个功能,如果有 超过一张 图片 / 背景叠加,则裁切效果失效。
错误参考:http://html51.com/info-38753-1/
四、将 canvas 导出成虚拟地址
wx.canvasToTempFilePath({ | |
fileType: 'jpg', | |
canvasId: 'customCanvas', | |
success: (res) => {console.log(res.tempFilePath) // 为 canvas 的虚拟地址 | |
} | |
}) | |
res:{ | |
errMsg: "canvasToTempFilePath:ok", | |
tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" | |
} |
这里需要把 canvas 里面的内容,导出成一个临时地址才能保存在相册,比如:
http://tmp/wx02935bb29080a7b4…
五、询问并获取访问手机本地相册权限
wx.getSetting({success(res) {console.log(res) | |
if (!res.authSetting['scope.writePhotosAlbum']) { // 判断权限 | |
wx.authorize({ // 获取权限 | |
scope: 'scope.writePhotosAlbum', | |
success() {console.log('授权成功') | |
// 转化路径 | |
self.saveImg();} | |
}) | |
} else {self.saveImg(); | |
} | |
} | |
}) |
判断是否有访问相册的权限,如果没有,则请求权限。
六、保存到用户手机本地相册
wx.saveImageToPhotosAlbum({ | |
filePath: res.tempFilePath, | |
success: function (data) { | |
wx.showToast({ | |
title: '保存到系统相册成功', | |
icon: 'success', | |
duration: 2000 | |
}) | |
}, | |
fail: function (err) {console.log(err); | |
if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") {console.log("当初用户拒绝,再次发起授权") | |
wx.openSetting({success(settingdata) {console.log(settingdata) | |
if (settingdata.authSetting['scope.writePhotosAlbum']) {console.log('获取权限成功,给出再次点击图片保存到相册的提示。') | |
} else {console.log('获取权限失败,给出不给权限就无法正常使用的提示') | |
} | |
} | |
}) | |
} else { | |
wx.showToast({ | |
title: '保存失败', | |
icon: 'none' | |
}); | |
} | |
}, | |
complete(res) {console.log(res); | |
} | |
}) |
保存到本地需要一定的时间,需要加一个 loading 的状态。
七、关于组件中引用 canvas
let ctx = wx.createCanvasContext('posterCanvas',this); // 需要加 this
在 components 中 canvas 无法选中的问题:
在 components 自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas>,如果省略则不在任何自定义组件内查找。
如果有什么疑问或者纠错可以在下面给 智酷君 留言。
如果 智酷君 的分享能够帮助到你,或者想持续获得最新的全栈攻略
可以在 segmentfault 关注我,或在 VX 搜索“Geek_Club”或者“智酷方程式”
扫描关注公众号????????????