海报生成示例
最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。
原型图
这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。
海报生成流程
[代码片段]Canvas生成海报实战demo
demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x
demo的ID:Q74OU3m57c9x
如果你装了IDE工具,可以直接访问上面的demo路径
通过代码片段将demo的ID输入进去也可添加:
下面分享下主要的代码内容和“填坑现场”:
一、添加字体
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) {
//失败回调
}
});
在开发过程中如果封面图无法按照约定的比例(280x220)给到:
那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(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.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg
五、询问并获取访问手机本地相册权限
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> ,如果省略则不在任何自定义组件内查找。
请问下你有没有试过在遮罩层中使用画布?我点击按钮才显示有赞的overlay组件,在里面嵌入了canvas,但是不管画什么都显示不出来,官方示例是直接在page页下显示的。
render() {
// 通过 SelectorQuery 获取 Canvas 节点
wx.createSelectorQuery()
.select('#canvas_poster')
.fields({
node: true,
size: true,
})
.exec(this.init.bind(this));
},
init(res) {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
ctx.scale(dpr, dpr);
ctx.strokeRect(10, 10, 25, 15);
ctx.scale(2, 2);
ctx.strokeRect(10, 10, 25, 15);
ctx.scale(2, 2);
ctx.strokeRect(10, 10, 25, 15);
},
在安卓手机上没有兼容问题吗?
为什么在真机只有打开调试模式才可以,关闭调试模式空白
大佬你好 为什么我生成的图片有些手机特别慢 有些就快一些 是手机性能原因还是代码的原因 你们生成图片的时间大概多少秒
我跟哥们的思路不同,我是通过判断文字的长度,来控制canvas生成的高度,来确保,canvans生成后,能显示所有的详情文字,并且,不超出高度。
嗯,处理文字换行的方法有好几个,这里判断长度你是判断 text的length 还是 ctx.measureText?
我就想知道怎么画emoji
这个canvas目前还没办法直接引入,但是可以用emoji图库的方式draw那一张表情图片进去,算是曲线救国~~~
明白了,操作还是挺繁琐。谢谢
朋友,这2天我仔细研究了下,可能之前表述不完整,是这样的,系统默认的emoji表情在IOS和Android高版本是支持(我拿了身边2台机器测了下,不是全覆盖测试),在canvas里面 wx.fillText("🙅😐💏",0,0,20); 现在版本(基础库2.5.0+)是可以正常显示的。
但是很多微信用户名称用的emoji表情可能来源于第三方,比如搜狗输入法什么的 🀼🀼🀼。。。这个就无法显示了~
这个回复完美
?
火钳刘明!
建议加精!
我们是做一个类似足迹地图的页面,引用了第三方的画布,现在需要使用画布来生成足迹地图,貌似没找到很好的方法来用画布生成一个带画布的分享图。不知道老哥是否有这方面的思路交流一下?