这是我参与8月更文挑战的第4天,活动详情查看: 8月更文挑战
背景
今天跟看掘金酱的聊天记录时,发现去年年度创作者大赛
的拉票海报是设计小哥哥小姐姐手动生成的,正好最近在做一个微信裂变的项目,仿照掘金海报写个8月更文海报
demo,为下次活动解放一下设计小姐姐的双手。
如下图,这张年度创作者大赛
的海报其实是程序员比较喜欢的类型了,所有需要动态填写的位置都预留了足够空白位置,绘制一张背景图,然后在对应的坐标位置绘制文字就可以。
我想来的点技术点较多的,结合目前正在开发的微信裂变需求以及8月更文活动
,设计了一张8月更文活动
的海报,下面来一起手操实现一下。
手操实现
1. 需求分析
拿到这个设计图后,我们首先分析一下都涉及到哪些绘制的功能点(因为后绘制的会覆盖上一个绘制,类似z-index层上的布局,所以我们先绘制层级较低的比如背景,再按照从上往下从左至右的顺序绘制):
- 绘制
540*900
的白色背景; - 绘制顶部
540*650
的头图; - 绘制
我正在参加xxx
这几个文字; - 绘制分隔线;
- 绘制头像,昵称,更文天数;
- 绘制二维码或者小程序码;
- 绘制底部的
长按查看
提醒
2. 代码实现
1.创建canvas容器
首先是第一步,我们先创建Canvas
容器,用来绘制海报,这里有个小技巧,直接把Canvas
容器展示出来的话会因为像素比例、单位换算(小程序rpx转px)问题,在绘制时候需要不断计算,所以我们可以将Canvas
容器使用定位布局移动到页面看不到的地方,在页面的位置放置image
标签,src
执行Canvas
生成的图片地址。这样Canvas
容器的大小就可以设置的跟设计图一样大,image
标签自己去适配单位换算。
index.wxml
<view class="share-box">
<view class="main">
<view class="canvas-box">
<image src="{{imgSrc}}"></image>
</view>
<canvas canvas-id="shareCanvas" style="width: 540px;height:900px;position: fixed;top: -10000px;" ></canvas>
<view class="btn-box">
<view class="btn" bindtap="download">保存图片</view>
</view>
</view>
</view>
index.wxss
.share-box {
background: rgba(14, 13, 13, .8);
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 100;
}
.main {
position: relative;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 101;
}
.canvas-box {
width: 540rpx;
height: 900rpx;
position: fixed;
top: 100rpx;
left: 50%;
margin-left: -270rpx;
box-shadow: 0rpx 5rpx 10rpx 0rpx rgba(0, 12, 32, 0.17);
z-index: 9999;
}
.canvas-box image {
width: 100%;
height: 100%;
}
.btn-box{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 150rpx;
background-color: #fff;
padding-top: 32rpx;
}
.btn{
width: 686rpx;
height: 90rpx;
margin: 0 auto;
background: linear-gradient(180deg, #8EADE9 0%, #4965B3 100%);
border-radius: 12rpx;
font-size: 36rpx;
font-weight: 500;
color: #FFFFFF;
line-height: 90rpx;
text-align: center;
}
index.js
Page({
data: {
imgSrc: "",
},
onLoad() {
this.init()
},
init() {
wx.showLoading({
title: '正在生成...',
})
const ctx = wx.createCanvasContext('shareCanvas')
ctx.draw(false, () => {
this.canvasToImage()
})
},
canvasToImage() {
wx.canvasToTempFilePath({
canvasId: 'shareCanvas',
success: res => {
wx.hideLoading()
this.setData({
imgSrc: res.tempFilePath
})
}
})
},
//保存
download() {
wx.showLoading({
title: '正在保存'
});
wx.saveImageToPhotosAlbum({
filePath: this.data.imgSrc,
success: function () {
wx.showToast({
title: '保存成功'
});
},
fail: function (e) {
wx.showToast({
title: '保存失败'
});
},
complete: function () {
wx.hideLoading()
}
});
}
})
wx.canvasToTempFilePath()
可以将当前Canvas
容器内容生成图片,需要在draw()
方法的回调里调用。这样就会得到如下图的页面:
wx.saveImageToPhotosAlbum()
可以将图片保存到相册。
2. 绘制 540*900
的白色背景
setFillStyle()
设置填充色为 #fff
fillRect()
从坐标(0,0)开始绘制宽540*高900的矩形
ctx.setFillStyle("#fff")
ctx.fillRect(0, 0, 540, 900)
3. 绘制顶部 540*650
的头图
ctx.drawImage("../../img/bg.png", 0, 0, 540, 650)
这里存在一个问题,因为小程序2M的限制,所以图片基本都是网络资源,drawImage()
绘制网络图片时必须先 getImageInfo()/downloadFile()
先把网络图片下载下来,而getImageInfo()/downloadFile()
是异步操作,加载图片多了会陷入回调地狱,所以我用Promise.all()
封装了一下网络资源的加载。
ps:getImageInfo()/downloadFile()
操作网络资源时需要在小程序后台配置downloadFile合法域名,我使用的是掘金富文本的图片路径,所以配置了掘金的域名。
getImgInfo: src => {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: src,
success: resolve,
fail: reject
})
})
},
init() {
wx.showLoading({
title: '正在生成...',
})
Promise.all([
this.getImgInfo("https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dcb71be2634641849853eef58675e9c8~tplv-k3u1fbpfcp-watermark.image"),
]).then(res => {
const ctx = wx.createCanvasContext('shareCanvas')
// 绘制背景色
ctx.setFillStyle("#fff")
ctx.fillRect(0, 0, 540, 900)
// 绘制头图
ctx.drawImage(res[0].path, 0, 0, 540, 650)
ctx.draw(false, () => {
this.canvasToImage()
})
})
},
这样我们的第一的大步就完成了,得到如下的图片:
4.绘制 我正在参加xxx
这几个文字
ctx.setTextBaseline('top')
ctx.setTextAlign('left')
ctx.setFillStyle('#333')
ctx.setFontSize(26)
ctx.fillText("我正在参加掘金8月更文活动", 30, 680)
ctx.fillText("快来帮我点赞吧!", 30, 710)
setTextBaseline()
设置文字在垂直方向的对齐方式,可选值为top
、bottom
、middle
和normal
。4者的区别如下:(都是在同样的y轴距离绘制的)
我们在设置时可以将其值设置为
top
,这样的好处是设计图上文字距离顶部的距离就是要绘制的y轴距离。
setTextAlign()
设置文字水平对齐方式,可选值为left
、center
和right
。3者的区别如下:(都是在同样的x轴距离绘制的)
我们在设置时可以将其值设置为
left
,这样的好处是设计图上文字距离左边的距离就是要绘制的x轴距离。不定宽度的文字想要水平居中的话可以设置对齐方式为center
,然后绘制的x轴位置为总宽度的1/2。
setFontSize()
设置文字字号,单位默认px。
fillText()
在坐标位置(30px, 680px)处开始绘制文字。
5.绘制分割线
ctx.moveTo(30, 750)
ctx.lineTo(510, 750)
ctx.setStrokeStyle('#eeeeee')
ctx.stroke()
moveTo()
在(30px,750px)位置创建路径起始点。
lineTo()
在(510px,750px)新增一个点。
setStrokeStyle()
设置描边的颜色。
stroke()
按照点的顺序描绘出路径。
6.绘制头像,昵称,更文天数
ctx.save()
ctx.setFillStyle('#fff')
ctx.beginPath()
ctx.arc(70, 800, 40, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(res[1].path, 30, 760, 80, 80)
ctx.restore()
ctx.font = 'normal bold 26px sans-serif';
ctx.fillText("于五五", 130, 770)
ctx.font = 'normal normal 20px sans-serif';
ctx.setFillStyle('#616165')
ctx.fillText("已更文", 130, 810)
let wordWidth = ctx.measureText("已更文").width
ctx.setFillStyle('#333')
ctx.fillText("4", 130 + wordWidth + 5, 810)
let numWidth = ctx.measureText("4").width
ctx.setFillStyle('#616165')
ctx.fillText("天", 130 + wordWidth + 5 + numWidth + 5, 810)
因为要绘制出一个圆形的头像,Canvas
不支持直接操作图片,所以只能使用其他方法:先绘制圆形的Canvas
区域,在这个区域内绘制图片。先使用save()
方法保存上下文,然后beginPath()
开始创建一个新的路径,arc()
方法在圆心坐标为(70px,800px)位置绘制半径为40px的圆,再然后clip()
裁剪,只保留圆形区域。这时候在Promise.all([])
里添加头像的网络地址,drawImage
绘制图片,因为圆的坐标是以圆心开始的,图片绘制是以左上角作为起始点的,所以图片绘制的起点坐标应该为(圆心x距离-半径,圆心y距离-半径),宽高为圆的直径 ,绘制完成后restore()
方法恢复刚才保存的上下文,以便我们可以继续绘制。
再看昵称这几个字是加粗的,目前只找到了对font设置实现加粗的方案,其他有点bug。
已更文xx天这里,因为xx是个变量,宽度不固定,所以调用了measureText()
方法,可以获取到将要绘制内容的宽度,因此 "天"字绘制的x距离="已更文"的x坐标+"已更文"的宽度+"xx"的左右字间距+"xx"的宽度。
7.绘制二维码和底部提醒
Promise.all()
里新增两张网络图片
this.getImgInfo("https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d94a69ca49249278f95c2391cb5dc7d~tplv-k3u1fbpfcp-watermark.image"),//二维码或小程序码
this.getImgInfo("https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45e02d3d598e4fe8ab83f30e25fbc08b~tplv-k3u1fbpfcp-watermark.image")//icon
ctx.drawImage(res[2].path, 410, 750, 100, 100)
ctx.drawImage(res[3].path,30, 860, 20, 20)
ctx.setFillStyle('#AAABAD')
ctx.fillText("长按查看", 58, 860)
8.运行结果
至此,小程序内绘制海报就结束了。可以用自己的个人资料生成自己微信裂变图片。
总结
1.使用image
代替Canvas
显示到页面上,可以避免Canvas
单位换算问题;
2.canvasToTempFilePath()
必须放到draw()
的回调里面;
3.drawImage()
绘制网络图片时需要使用getImageInfo()/downloadFile()
先把网络图片下载下来;
4.getImageInfo()/downloadFile()
下载网络图片时需要配置安全域名,开发环境可以先使用微信开发者工具勾选不校验合法域名;
5.使用Promise.all()
解决回调地狱;
6.设置Canvas
对齐方式为setTextBaseline('top')、setTextAlign('left')
,这样要绘制的元素(x,y)距离坐标就是设计图上元素的位置;
7.绘制文字水平居中可以设置setTextAlign('center')
,fillText()
的x坐标位置为总宽度的一半;
8.绘制圆形头像可以先绘制圆形区域,在圆形区域上绘制图片;
9.setFillStyle()
设置文字颜色、setFontSize()
设置文字大小、font = 'normal bold 26px sans-serif'
实现文字加粗;
10.measureText()
可以获取文字内容所占的宽度;
最后,附上完整js代码:
Page({
data: {
imgSrc: "",
},
onLoad() {
this.init()
},
getImgInfo: src => {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: src,
success: resolve,
fail: reject
})
})
},
init() {
wx.showLoading({
title: '正在生成...',
})
Promise.all([
this.getImgInfo("https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dcb71be2634641849853eef58675e9c8~tplv-k3u1fbpfcp-watermark.image"), //头图
this.getImgInfo("https://sf6-ttcdn-tos.pstatp.com/img/user-avatar/eccbd6c74379889aee23eff8569c815c~300x300.image"), //头像
this.getImgInfo("https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d94a69ca49249278f95c2391cb5dc7d~tplv-k3u1fbpfcp-watermark.image"), //二维码或小程序码
this.getImgInfo("https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45e02d3d598e4fe8ab83f30e25fbc08b~tplv-k3u1fbpfcp-watermark.image") //icon
]).then(res => {
const ctx = wx.createCanvasContext('shareCanvas')
// 绘制背景色
ctx.setFillStyle("#fff")
ctx.fillRect(0, 0, 540, 900)
// 绘制头图
ctx.drawImage(res[0].path, 0, 0, 540, 650)
// 绘制文字
ctx.setTextBaseline('top')
ctx.setTextAlign('left')
ctx.setFillStyle('#333')
ctx.setFontSize(26)
ctx.fillText("我正在参加掘金8月更文活动", 30, 680)
ctx.fillText("快来帮我点赞吧!", 30, 710)
// 绘制分割线
ctx.moveTo(30, 750)
ctx.lineTo(510, 750)
ctx.strokeStyle = '#eeeeee'
ctx.stroke()
// 绘制头像、个人信息
ctx.save()
ctx.setFillStyle('#fff')
ctx.beginPath()
ctx.arc(70, 800, 40, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(res[1].path, 30, 760, 80, 80)
ctx.restore()
ctx.font = 'normal bold 26px sans-serif';
ctx.fillText("于五五", 130, 770)
ctx.font = 'normal normal 20px sans-serif';
ctx.setFillStyle('#616165')
ctx.fillText("已更文", 130, 810)
let wordWidth = ctx.measureText("已更文").width
ctx.setFillStyle('#333')
ctx.fillText("4", 130 + wordWidth + 5, 810)
let numWidth = ctx.measureText("4").width
ctx.setFillStyle('#616165')
ctx.fillText("天", 130 + wordWidth + 5 + numWidth + 5, 810)
// 绘制二维码
ctx.drawImage(res[2].path, 410, 750, 100, 100)
ctx.drawImage(res[3].path, 30, 860, 20, 20)
ctx.setFillStyle('#AAABAD')
ctx.fillText("长按查看", 58, 860)
ctx.draw(false, () => {
this.canvasToImage()
})
})
},
canvasToImage() {
wx.canvasToTempFilePath({
canvasId: 'shareCanvas',
success: res => {
wx.hideLoading()
this.setData({
imgSrc: res.tempFilePath
})
}
})
},
download() {
wx.showLoading({
title: '正在保存'
});
wx.saveImageToPhotosAlbum({
filePath: this.data.imgSrc,
success: function () {
wx.showToast({
title: '保存成功'
});
},
fail: function (e) {
wx.showToast({
title: '保存失败'
});
},
complete: function () {
wx.hideLoading()
}
})
}
})