前段时间,公司项目要求绘制商品的分享海报,当时尝试使用了html2canvas,但是后来发现,它只有在Safari浏览器上面效果比较理想,但是在Android手机上面,各种移位,总之就是不能生成完整的图。虽然后面有尝试过在绘制过程中加一个延时,但是效果还是不太好,最后还是决定使用原生canvas来制作了。过程虽然麻烦些,但是最终的效果还是不错的。
先上一张效果图
下面是我整理的绘制过程
前期准备
<div class="hidden" style="display: none;">
<canvas id="mycanvas" :width="w" :height="h" />
<img class="goodsImg" src="" alt="" crossOrigin="*">
</div>
<img class="postImg" src alt>
这里,canvas模块,我是用display: none 将其隐藏,因为只需要获取到canvas生成的base64格式的图片就可以了,而原始的canvas内容不展示也不影响。
之所以这样处理,canvas的宽度高度,要比生成的图片的宽高大一些,这样生成的图片会清晰很多。
data() {
return {
w: 650,
h: 1200
}
}
.postImg {
width: 570px;
}
注意:canvas的宽高最好用 width 和 height 属性,以免变形。
写好布局,接下来还有比较重要的一步:
对于绘制图片的处理
这里分为两种情况: 一种是本地图片绘制,一种是网络图片绘制;
其实两种绘制差别上不是很大,只是网络图片需要解决图片跨域的问题。
解决方法也是有的,需要在 oss 上面去配置一下允许跨域。这个需要找公司负责这方面人员去帮你配置,当然如果你自己也可以配置的话,那也再好不过了,具体的配置方法网上有很多,可以自己去搜。
这里我用的是网络图片,所以 src 的属性是空的,后面会在获取到数据后填充上的。
如果是绘制本地图片,需要把上面的 html 代码稍作修改
<div class="hidden" style="display: none;">
<canvas id="mycanvas" :width="w" :height="h" />
<img src="本地图片地址" alt="" />
</div>
<img class="postImg" src alt>
vue中如果要把本地图片绘制出来,一定要直接把 img标签 写在 html里。 通过 let img = new Image(),img.src = 本地图片路径,这种方式,没有办法将图片绘制出来;这样做的结果就是,不管你用了什么办法,图片就是出不来。。。。
现在准备工作基本上就完了,接下来就是比较重要的绘制过程了。
绘制
data(){
return {
w: 650,
h: 1200,
info: null
}
},
methods: {
draw() {
// 这里可以添加一个loading
let qrImg = new Image() // 创建二维码图片对象
qrImg.src = this.info.url // 二维码 base64 链接码
let goodsImg = ''
goodsImg = document.querySelector('.goodsImg')
goodsImg.src = this.info.goodsImg // 网络图片地址
// 开始绘制
goodsImg.onload = () => {
let canvas = document.getElementById('mycanvas')
let ctx = canvas.getContext('2d', {
antialias: true // 是画质更清晰 其他配置可以参考文档
})
// 填充背景色
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, 650, 1200)
// 绘制价格
ctx.fillStyle = '#E85700' // 设置字体颜色
ctx.font = '24px PingFang SC' // 字体、字体大小
ctx.fillText('¥', 50, 720) // 文字内容和位置 相当于绝对定位left 和top值 定位参照位置是canvas区域左上角
ctx.font = '36px PingFang SC'
ctx.fillText(`${this.info.sharePrice1}${this.info.sharePrice2}`, 75, 720) // 价格涉及到小数点,所以对价格做了处理,拼在一起是一个完整的价格
ctx.fillStyle = 'rgba(153,153,153,1)' // 切换颜色 绘制原价
ctx.font = '24px PingFangSC-Regular,PingFang SC'
ctx.fillText(`原价:¥${this.info.afterPrice1}${this.info.afterPrice2}`, 220, 720)
// 绘制标题
ctx.fillStyle = 'rgba(0, 0, 0, 1)'
ctx.font = '36px PingFangSC-Regular,PingFang SC'
/* 标题分了两行绘制,因为标题长度是随机的,只绘制一行,
效果不好;
所以这里写了一个方法,可以截取固定字节的字符串
标题长度超过28字节,截取前28字节的字符串,在第
一行绘制;
然后拿到剩余的字符串second,截取固定26字节字符串
strSecond,对比两者的的长度是否一致,或者两者的
内容是否完全一样。如果一样,second== strSecond,
可以直接绘制 second; 如果 second > strSecond,
可以在 strSecond 后面 + '...' ,再绘制,达到溢出
省略的效果。
*/
let first = this.autoAddEllipsis(this.info.title, 28)
ctx.fillText(first, 50, 780)
let second = this.info.title.substr(first.length)
let strSecond = this.autoAddEllipsis(this.info.title.substr(first.length), 26)
if (second == strSecond) {
ctx.fillText(second, 50, 830)
} else {
ctx.fillText(strSecond + '...', 50, 830)
}
// 绘制提示语
ctx.fillStyle = 'rgba(153,153,153,1)'
ctx.font = '24px PingFangSC-Regular,PingFang SC'
ctx.fillText('长按或扫描查看', 232, 1110)
ctx.drawImage(qrImg, 211, 872, 209, 209) // 绘制二维码
ctx.drawImage(proImg, 0, 0, 650, 650) // 绘制商品图片
let Url = document.getElementById('mycanvas').toDataURL('image/png') // 转base64
document.querySelector('.postImg').src = Url // 展示图片
// 哈哈,这里又写了一遍,因为出现二维码没有绘制出来的情况
qrImg.onload = () => {
ctx.drawImage(qrImg, 211, 872, 209, 209)
ctx.drawImage(proImg, 0, 0, 650, 650)
let Url = document.getElementById('mycanvas').toDataURL('image/png')
document.querySelector('.postImg').src = Url
// 绘制完成 清除loading 什么loading,我也不知道
}
}
}
}
这样这个海报就绘制完成了,剩余的就是怎么处理这个海报了。
然后那个每次都截取固定字符串的方法,简单说一下,因为我也是从别人那里搜刮来的,不是自己的成果,不好意思放上来。就说一下大概思路:
autoAddEllipsis 方法 接收两个参数:
str 需要截取的字符串,
length 需要截取的字节长度
然后它的核心是一句话: Unicode编码大于255说明占两个字节(至于如何将字符转为Unicode编码,可以自行查文档)
str的每个字符都需要转码计算它的字节数,所以需要对 str 进行遍历,判断每个字符的字节数 是一个还是两个,然后进行计数count,直到count 超过 length未知。