微信小程序canvas生成图片、保存到本地

您所在的位置:网站首页 抖音怎么取消复制链接保存到本地 微信小程序canvas生成图片、保存到本地

微信小程序canvas生成图片、保存到本地

2023-10-09 23:50| 来源: 网络整理| 查看: 265

前提

产品提出了一个新需求:根据用户所选择的年月日的打卡详情,生成打卡海报,用户可保存到本地。

image.png 逻辑整理

接到这个需求后我大概缕出了要完成的步骤:

1、收集海报所需的数据; 2、进行canvas绘制; 3、使用wx的api将canvas生成图片拿到临时图片地址; 4、review代码,优化思路和兼容;

数据这里就不需要说了,我们直接进入主题:

进行canvas绘制

设计稿:

image.png 如图所示,canvas的绘制大概可以拆分为以下几部分: 1.获取canvas元素,生成2D画布; 2.绘制图片:a. 绘制背景图片;b. 二维码图片 3.圆角矩阵; 4.分割线; 5.文字;

为了可以拿到canvas画布的生成状态,多选择使用promise来控制整体的流程和进度

获取canvas元素,并生成2D画布

使用wx.createSelectorQuery()来获取指定的节点,拿到元素信息 例如:

canvas是canvas节点;

ctx是2D画布;

wx.createSelectorQuery().in(this) .select('#punchImg') .fields({ node: true, size: true }) .exec((res) => { if(!res[0].node) return canvas = res[0].node ctx = canvas.getContext('2d') dpr = wx.getSystemInfoSync().pixelRatio that.drawing() that.setData({ model_width: wx.getSystemInfoSync().screenWidth, model_height: wx.getSystemInfoSync().screenHeight }) }) 绘制图片

绘制图片的整体逻辑如下: 对节点canvas使用createImage()生成图片,然后设置图片的src, 在图片的onload方法中对画布进行操作。

drawIMG() { const that = this return new Promise((resolve, reject) => { let img = canvas.createImage() img.src = 'XXX.png' img.onload = () => { resolve('小程序二维码渲染完毕') } }) }

下面我们根据这个思路来绘制背景图片和二维码图片。

背景图片

根据设计稿可以看到背景图要占满整个canvas,所以直接使用drawImage()就可以直接渲染

// 绘制海报背景 drawPoster() { const that = this return new Promise(function(resolve, reject) { let poster = canvas.createImage() poster.src = that.data.poster_data.bg_img poster.onload = () => { that.computeCanvasSize(poster.width, poster.height).then(() => { ctx.drawImage(poster, 0, 0, poster.width, poster.height, -2, -1, that.data.canvas_width+3, that.data.canvas_height+1) resolve('海报背景生成完毕') }) } }) }

此时我们已经生成背景图片了

image.png 小程序二维码图片

根据设计稿,我们可以看到二维码是圆角的,并且位置在右下方,那么我们就要在绘制图片的基础上加上"圆角"和"定位"。 思路应该是:

绘制矩形 进行裁剪 放置图片对其定位

1、绘制矩形 设计稿上可以看到有多个圆角矩阵的绘制,那么我们就将绘制圆角矩阵给单独抽出一个方法出来:

/** * @description: * @param {number} x:x轴坐标 * @param {number} y:y轴坐标 * @param {number} width:矩阵的宽度 * @param {number} height:矩阵的高度 * @param {number} radius:圆角角度 * @param {string} color:颜色 * @return {*} */ handleBox(x:number, y:number, width: number, height: number, radius:number, color: string) { ctx.lineWidth = 1 ctx.strokeStyle = color ctx.beginPath() ctx.moveTo(x, y+radius) ctx.lineTo(x, y + height - radius) ctx.quadraticCurveTo(x, y + height, x + radius, y + height); ctx.lineTo(x + width - radius, y + height); ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius); ctx.lineTo(x + width, y + radius) ctx.quadraticCurveTo(x + width, y, x + width - radius, y) ctx.lineTo(x + radius, y); ctx.quadraticCurveTo(x, y, x, y + radius); ctx.stroke(); },

圆角矩阵方法创建完毕后,我们就可以对二维码进行操作啦. 2、绘制二维码图片

// 渲染二维码 drawQr() { const that = this return new Promise((resolve, reject) => { let qr = canvas.createImage() qr.src = that.data.poster_data.qrcode // 这里是接口返回的线上二维码地址 qr.onload = () => { ctx.save() // 创建一个二维码所需的圆角矩阵,并且将其进行定位 that.handleBox(that.data.canvas_width-80, that.data.canvas_height-80, 66, 66, 6, 'rgba(255,255,255,0)') // 对画布进行裁剪 ctx.clip() // 在裁剪的画布中绘制二维码图片并定位 ctx.drawImage(qr, that.data.canvas_width-80, that.data.canvas_height-80, 66, 66) ctx.restore() resolve('小程序二维码渲染完毕') } }) }

image.png 此时我们的所需的图片都已生成完毕。

圆角矩阵

我们需要生成一个圆角矩阵并对其进行填充颜色,上面我们已经展示了需要使用的生成圆角矩阵的方法【handleBox()】了,接下来,我们对填充圆角矩阵进行操作。

/** * @description: * @param {number} x:x轴坐标 * @param {number} y:y轴坐标 * @param {number} width:矩阵的宽度 * @param {number} height:矩阵的高度 * @param {number} radius:圆角角度 * @param {string} color:颜色 * @return {*} */ fillRoundRect(x: number, y: number, width: number, height:number, radius:number, color: string) { ctx.save(); ctx.translate(x, y); ctx.fillStyle = color || "#000"; //若是给定了值就用给定的值否则给予默认值 ctx.fill(); ctx.restore(); } // 这里的颜色随手写的 this.handleBox(19, this.data.canvas_height-190, this.data.canvas_width-38, 90, 8, 'rgba(255, 255, 255, 0.7)') this.fillRoundRect(19, this.data.canvas_height-190, this.data.canvas_width-38, 90, 8, 'rgba(255, 255, 255, 0.4)')

image.png 接下来对文字和分割线进行操作。

分割线 /** * @description: 分割线 * @param {string} color: 颜色 * @param {any} starPosition:开始坐标 * @param {any} endPosition:结束坐标 * @return {*} */ handleLine(color: string, starPosition: any, endPosition: any) { ctx.save() ctx.beginPath() ctx.moveTo(starPosition.x, starPosition.y) ctx.lineTo(endPosition.x, endPosition.y) ctx.lineWidth = 1 ctx.strokeStyle = color ctx.stroke() ctx.closePath() ctx.restore() } that.handleLine('rgba(255,255,255,0.5)', {x:78, y: 19}, {x: 78, y: 70}) // 这里的计算只是这次需求所需的计算,仅参考 that.handleLine('rgba(173,173,173,0.72)', {x:(that.data.canvas_width - 31)/2 + 16, y: that.data.canvas_height - 177 }, {x: (that.data.canvas_width - 31)/2 + 16, y: that.data.canvas_height - 127}) 文字

值得注意的是整个设计稿中的文案分为2种情况: 短文案、长文案【需要换行和超出省略号】。

文字段长度的判断

对文字要占用的长度可以用【画布.measureText(文案).width】来获取。 然后通过与最长宽度的对比来判断是要使用短文案的绘制方法or长文案的绘制方法。

// 计算文案宽度 computedDaySize(day: string|number, font: string) { ctx.font = font let w = ~~(ctx.measureText(day).width) // 这里向上取整or向上取整都可 return w } 短文案

短文案直接渲染即可。

/** * @description: 对短文字的操作 * @param {string} font:字体信息 * @param {string} color:颜色 * @param {string} text:文案 * @param {any} position:定位(x,y) * @return {*} */ handleText(font: string, color: string, text: string, position: any) { ctx.save() ctx.font = font ctx.fillStyle = color ctx.fillText(text, position.x, position.y) ctx.closePath() ctx.restore() } 长文案

文案过长时需要换行和省略号处理。

/** * @description: * @param {string} font:文字信息 * @param {string} color:颜色 * @param {string} text:文案 * @param {any} position:定位信息 * @param {number} count:行数 * @param {string} sign:用户名/打卡信息文案 * @return {*} */ handleMaxWidthText(font: string, color: string, text: string, position: any, count: number, sign: string) { ctx.save() ctx.font = font ctx.fillStyle = color let endPos = 0, // 文案的最长宽度 maxW = this.data.canvas_width- (78 - this.computedDaySize(this.data.poster_data.yearMounth, '11px DIN-Medium') - 20 + 78 + 1) - 20, // 文案宽度 textW = this.computedDaySize(text, font), // 文案在最长宽度的限制下可以分的行数 allRow = Math.ceil(textW / maxW) for(let j = 0; j < count; j++) { let nowStr = text.slice(endPos), rowWid = 0 if(textW > maxW) { for(let m = 0; m < nowStr.length; m++) { rowWid += ctx.measureText(nowStr[m]).width ctx.fillStyle = color // 多行中第一行渲染--要区分姓名还是文案 // 文案第一行正常渲染 // 用户名第一行如果超过最长宽度则直接省略号处理 if(rowWid > maxW && j == 0) { if(sign=='name') { ctx.fillText(nowStr.slice(0, m - 1) + '...:', position.x, position.y); }else { ctx.fillText(nowStr.slice(0, m), position.x, position.y); } endPos += m;//下次截断点 break; } // 文案第二行需要省略号渲染情况 if(allRow > count && rowWid > maxW && j == count-1) { ctx.fillText(nowStr.slice(0, m - 1) + '...', position.x, position.y + j + 16); endPos += m;//下次截断点 break; } // 最后一行不需要省略号渲染的情况 if(allRow 0) { // 对文案过长的兼容 let textW = ctx.measureText(that.data.poster_data.content).width, y1 = 39 // 渲染打卡文案 if(maxW < textW) { that.handleMaxWidthText("12px PingFangSC-Regular", '#fff', that.data.poster_data.content, {x: x, y: 50}, 2, 'content') y1 = 30 }else { that.handleText("normal 12px PingFangSC-Regular", '#fff', that.data.poster_data.content, {x: x, y: 64}) } let nameW = that.computedDaySize(that.data.poster_data.user_name,'14px PingFangSC-Semibold') // 渲染用户名 if(maxW < nameW) { that.handleMaxWidthText("14px PingFangSC-Semibold", '#fff', that.data.poster_data.user_name + '...', {x: x, y: y1}, 1, 'name') }else { that.handleText("normal 14px PingFangSC-Semibold", '#fff', that.data.poster_data.user_name + ':', {x: x, y: y1}) } }else { that.handleText("16px PingFangSC-Regular", '#fff', that.data.poster_data.chicken_soup_zh, {x: x, y: 39}) that.handleText("14px PingFangSC-Light", '#fff', that.data.poster_data.chicken_soup_en, {x: x, y: 64}) } // 处理圆角矩阵 this.handleBox(16, this.data.canvas_height-197, this.data.canvas_width-38, 90, 8, 'rgba(255, 255, 255, 0.7)') this.fillRoundRect(16, this.data.canvas_height-197, this.data.canvas_width-38, 90, 8, 'rgba(0, 0, 0, 0.4)') // 处理圆角矩阵内的文字与分割线 that.handleLine('rgba(173,173,173,0.72)', {x:(that.data.canvas_width - 31)/2 + 16, y: that.data.canvas_height - 177 }, {x: (that.data.canvas_width - 31)/2 + 16, y: that.data.canvas_height - 127}) // 绘制连续打卡时间文案 that.handleText("bold 42px DIN-Bold", '#fff', that.data.poster_data.serial_day, {x: (that.data.canvas_width - 31)/4 - 10, y: that.data.canvas_height - 147}) that.handleText("normal 12px PingFangSC-Regular", '#fff', '天', {x: (that.data.canvas_width - 31)/4 + that.computedDaySize(that.data.poster_data.serial_day, '42px DIN-Bold') - 9, y: that.data.canvas_height - 147}) that.handleText("normal 12px PingFangSC-Regular", '#fff', '已连续打卡', {x: (that.data.canvas_width - 31)/4 - 8, y: that.data.canvas_height - 128}) // 绘制本周打卡次数文案 that.handleText("bold 42px DIN-Bold", '#fff', that.data.poster_data.week_day, {x: (that.data.canvas_width - 31)*3/4 - 18 , y: that.data.canvas_height - 147}) that.handleText("normal 12px PingFangSC-Regular", '#fff', '次', {x:(that.data.canvas_width - 31)*3/4 + that.computedDaySize(that.data.poster_data.week_day, '42px DIN-Bold') - 15, y: that.data.canvas_height - 147}) that.handleText("normal 12px PingFangSC-Regular", '#fff', '本周已打卡', {x: (that.data.canvas_width - 31)*3/4 - 18, y: that.data.canvas_height - 128}) // 底部的基本信息 that.handleText("normal 14px PingFangSC-Semibold", 'rgba(255,255,255,0.7)', '小程序名小程序名', {x: 14, y: that.data.canvas_height - 48}) // that.handleText("normal 14px PingFangSC-Semibold", 'rgba(255,255,255,0.7)', '橙啦考研星球', {x: 14, y: that.data.canvas_height - 48}) that.handleText("normal 12px PingFangSC-Regular", 'rgba(255,255,255,0.7)', '欢迎词,扫码进入小程序', {x: 14, y: that.data.canvas_height - 26}) resolve('渲染完毕') }) } /** * @description: 逐步绘制海报 * @return {*} */ async drawing() { const that = this canvas.width = that.data.canvas_width * dpr canvas.height = that.data.canvas_height * dpr ctx.scale(dpr, dpr) await that.drawPoster() // 生成海报背景 await that.drawQr() // 绘制二维码 await that.drawText().then(res => { if(res) { // 生成图片----下面介绍 that.saveImg() } }) }

此时效果如图所示:

image.png 这个时候我们的海报已经绘制完毕并且已经拿到绘制完毕的结果了,这时就可以将canvas生成临时图片地址,等待用户点击保存图片啦。

canvas生成图片

微信给我们提供了直接将canvas生成图片的api,我们直接进行调用:

/** * @description: canvas生成图片 * 生成图片后存入本地,一天缓存;用户如果点击过保存图片则自动保存; * 如果已经有过当天的环境图片路径,则直接保存 * @return {*} */ saveImg() { let that = this // posterPath 是临时图片的地址,如果已经有过地址,直接保存即可;没有再调用api生成临时地址 if(!this.data.posterPath) { // canvas生成图片 wx.canvasToTempFilePath({ // 由于我canvas标签直接设置了2D,所以直接将节点传过去即可。 canvas: canvas, width: that.data.canvas_width, height: that.data.canvas_height, // 输出的宽高设置了4倍,一定程度上解决了canvas模糊的问题 destWidth: that.data.model_width * dpr * 4, destHeight: (that.data.model_width * dpr * 4) * (that.data.canvas_height / that.data.canvas_width ), fileType: 'png', success: (res) => { if(res) { that.setData({ posterPath: res.tempFilePath, canvasSign: true }) // 将生成的图片临时地址保存到storage中 let pathArr = [app.globalData.userInfo.userName + that.data.poster_data.yearMounth + that.data.poster_data.day, res.tempFilePath] wx.setStorageSync('punchPoster_cl', pathArr) } // 图片未生成之前用户点击过“保存图片”按钮,则图片生成后自动下载到本地 if(that.data.userSaved) { that.downImg(res.tempFilePath) } }, fail: (err) => { console.log(err,'err--canvasToTempFilePath'); } }) }else { // 下载图片到本地 that.downImg(that.data.posterPath) } } 保存图片到本地

微信提供了api可以将临时图片给保存到本地。

/** * @description: 保存canvas生成的图片,将部分变量恢复初始值 * @param {string} path * @return {*} */ downImg(path: string) { let that = this wx.saveImageToPhotosAlbum({ filePath: path, success: (res) => { that.setData({ saveSign: true // 用户保存图片成功标识 }) }, fail: (err) => { wx.showToast({ title: '保存失败', icon: 'none', duration: 2000 }) }, complete: (res) => { // 将部分状态标识给初始化 that.setData({ userSaved: false, haveSaved: false }) } }) } 优化

其实上面代码中已经有部分优化了,但是我们对其总结一下:

临时图片地址保存到storage let pathArr = [app.globalData.userInfo.userName + that.data.poster_data.yearMounth + that.data.poster_data.day, res.tempFilePath] wx.setStorageSync('punchPoster_cl', pathArr)

在成功生成图片临时地址后,将临时地址保存到storage中,存放的结构为数组[用户名+年月+日, 临时地址]。

我们在每次获取打卡信息时,获取storage中的punchPoster_cl 通过对比punchPoster_cl[0]的信息来判断是否为用户选择的海报图片。 如果不是:直接清除数据重新生成canvas; 如果是:则直接使用已生成的临时地址。

此优化在一定程度上让用户不必频繁生成canvas,充分利用了已生成的临时图片地址,优化了用户体验。

生成假海报

将canvas定位在用户看不到的地方,写一个假海报; 解决了由于生成canvas需要一定的时间,会出现空白区的情况,让用户体验感更好。

使用promise来控制和获取进度

image.png

image.png

用promise来控制进度,保证了代码按照逻辑顺序有序进行; 同时也解决了用户在生成图片之前就点击“保存图片按钮”时的情况:图片未生成完毕时,进行toast提示告知用户图片正在生成中。 同时用变量控制用户频繁点击“保存图片按钮”,解决了用户会一次性保存多次图片的情况。

总结

以上就是我对于这次需求的完成和优化,如果有不完善或者不对的地方请大佬们多多指点~~



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3