更新,截止2020.4.17日,canvas 2d接口在pc端微信不支持,官方有人在2月份說后續會支持,遙遙無期。
--------------------------------
截止2020.3.26,小程序官方文檔中,有兩種繪制方式:Canvas 2D、webGL
文檔地址:https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html
而開發者工具中,官方推薦使用性能更好的2d模式,用法如下所示:
<canvas type="2d" id="myCanvas"></canvas>
但是網上大多數教程都是使用舊的接口,如:
<canvas canvas-id="canvasBox"></canvas>
本着學習和為后來人踩坑的目的,我們來嘗試一下新接口,迎接未知的挑戰 :)
需要注意的是:官方文檔中CanvasContext的一些函數,在Canvas 2d模式下已經失效,這點,官方用了一句話做了描述:
canvas 組件的繪圖上下文。CanvasContext 是舊版的接口, 新版 Canvas 2D 接口與 Web 一致。
出處:https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html
舉個例子,比如設置填充色:
// 舊方式: ctx.setFillStyle('red') // 在Canvas 2d 下會報錯 // 新方式: ctx.fillStyle = "red";
所以針對新接口的方法,可以參考html5的canvas api。
最終效果
下面就讓我們抽絲剝繭,細細剖析。
以下代碼均在官方開發者工具下編寫
步驟:
wxml文件中,加入canvas標簽以及保存按鈕:
<view style="width:100%;height:{{canvasHeight}}px;"> <canvas type="2d" id="canvasBox" style="width:100%;height:100%;"></canvas> </view>
js文件中:
1.設置數據:數據就相當於所有交通的樞紐
data: { // 數據區,從服務端拿到的數據 name: "作者 Alpiny", // 姓名 phone: "13988887777", // 電話 posterUrl: "https://desk-fd.zol-img.com.cn/t_s1024x1024c5/g5/M00/00/0A/ChMkJlmfw7CIBpnCAAD3xQrT42EAAf9sgAH1ycAAPfd598.jpg", // 海報地址 photoUrl: "https://img2.woyaogexing.com/2020/03/27/3698eb92b78246e99d859f97f4227936!400x400.jpeg", // 頭像地址 qrcodeUrl: "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=144549786,228270254&fm=26&gp=0.jpg", // 小程序二維碼 // 設置區,針對部件的數據設置 photoDiam: 50, // 頭像直徑 qrcodeDiam: 80, // 小程序碼直徑 infoSpace: 13, // 底部信息的間距 saveImageWidth: 500, // 保存的圖像寬度 bottomInfoHeight: 100, // 底部信息區高度 tips: "微信掃碼或長按了解更多", // 提示語 // 緩沖區,無需手動設定 canvasWidth: 0, // 畫布寬 canvasHeight: 0, // 畫布高 canvasDom: null, // 畫布dom對象 canvas:null, // 畫布的節點 ctx: null, // 畫布的上下文 dpr: 1, // 設備的像素比 posterHeight: 0, // 海報高 },
這里數據分了三類:數據區是后端傳送來的數據、設置區是可以定制畫面的數據、緩沖區是用來暫存一些臨時的數據,無需設置。
2.onReady 鈎子中,執行 drawImage 函數
onReady: function () { this.drawImage() },
放到 onReady里的目的,是為了一進入頁面就直接渲染畫面。
3.創建drawImage函數,用來選擇canvas節點並准備繪圖:
// 查詢節點信息,並准備繪制圖像 drawImage() { const query = wx.createSelectorQuery() // 創建一個dom元素節點查詢器 query.select('#canvasBox') // 選擇我們的canvas節點 .fields({ // 需要獲取的節點相關信息 node: true, // 是否返回節點對應的 Node 實例 size: true // 是否返回節點尺寸(width height) }).exec((res) => { // 執行針對這個節點的所有請求,exec((res) => {alpiny}) 這里是一個回調函數 const dom = res[0] // 因為頁面只存在一個畫布,所以我們要的dom數據就是 res數組的第一個元素 const canvas = dom.node // canvas就是我們要操作的畫布節點 const ctx = canvas.getContext('2d') // 以2d模式,獲取一個畫布節點的上下文對象 const dpr = wx.getSystemInfoSync().pixelRatio // 獲取設備的像素比,未來整體畫布根據像素比擴大 this.setData({ canvasDom: dom, // 把canvas的dom對象放到全局 canvas: canvas, // 把canvas的節點放到全局 ctx: ctx, // 把canvas 2d的上下文放到全局 dpr: dpr // 屏幕像素比 },function(){ this.drawing() // 開始繪圖 }) }) // 對以上設置不明白的朋友 // 可以參考 createSelectorQuery 的api地址 // https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html },
看,上面的代碼第20行執行了drawing函數,drawimg 函數里制定了繪制的整體流程,下面我們來創建它。
4.創建 drawimg 函數
// 繪制畫面 drawing() { const that = this; wx.showLoading({title:"生成中"}) // 顯示loading that.drawPoster() // 繪制海報 .then(function () { // 這里用同步阻塞一下,因為需要先拿到海報的高度計算整體畫布的高度 that.drawInfoBg() // 繪制底部白色背景 that.drawPhoto() // 繪制頭像 that.drawQrcode() // 繪制小程序碼 that.drawText() // 繪制文字 wx.hideLoading() // 隱藏loading }) },
這其中要注意的是,為了讓最終生成的圖片自適應高,所以要提前拿到海報的高度來設置畫布,所以,第一步繪制海報是阻塞運行的(采用了Promise來完成阻塞)。
5.創建 drawPoster 函數,繪制海報
// 繪制海報 drawPoster() { const that = this return new Promise(function (resolve, reject) { let poster = that.data.canvas.createImage(); // 創建一個圖片對象 poster.src = that.data.posterUrl // 圖片對象地址賦值 poster.onload = () => { that.computeCanvasSize(poster.width, poster.height) // 計算畫布尺寸 .then(function (res) { that.data.ctx.drawImage(poster, 0, 0, poster.width, poster.height, 0, 0, res.width, res.height); resolve() }) } }) },
而drawPoster大約第7行,又進行了阻塞,是因為,我們要用拿到的海報數據先設置一下畫布,否則直接繪圖會導致失敗。
6.創建 computeCanvasSize 函數,用來計算畫布尺寸
// 計算畫布尺寸 computeCanvasSize(imgWidth, imgHeight){ const that = this return new Promise(function (resolve, reject) { var canvasWidth = that.data.canvasDom.width // 獲取畫布寬度 var posterHeight = canvasWidth * (imgHeight / imgWidth) // 計算海報高度 var canvasHeight = posterHeight + that.data.bottomInfoHeight // 計算畫布高度 海報高度+底部高度 that.setData({ canvasWidth: canvasWidth, // 設置畫布容器寬 canvasHeight: canvasHeight, // 設置畫布容器高 posterHeight: posterHeight // 設置海報高 }, () => { // 設置成功后再返回 that.data.canvas.width = that.data.canvasWidth * that.data.dpr // 設置畫布寬 that.data.canvas.height = canvasHeight * that.data.dpr // 設置畫布高 that.data.ctx.scale(that.data.dpr, that.data.dpr) // 根據像素比放大 setTimeout(function(){ resolve({ "width": canvasWidth, "height": posterHeight }) // 返回成功 },1200) }) }) },
7.創建第4步所需的其他幾個函數:drawInfoBg(繪制底部白色背景)、drawPhoto(繪制頭像)、drawQrcode(繪制二維碼)、drawText(繪制文本)、alpiny(作者本人)
// 繪制白色背景 // 注意:這里使用save 和 restore 來模擬圖層的概念,防止污染 drawInfoBg() { this.data.ctx.save(); this.data.ctx.fillStyle = "#ffffff"; // 設置畫布背景色 this.data.ctx.fillRect(0, this.data.canvasHeight - this.data.bottomInfoHeight, this.data.canvasWidth, this.data.bottomInfoHeight); // 填充整個畫布 this.data.ctx.restore(); }, // 繪制頭像 drawPhoto() { let photoDiam = this.data.photoDiam // 頭像路徑 let photo = this.data.canvas.createImage(); // 創建一個圖片對象 photo.src = this.data.photoUrl // 圖片對象地址賦值 photo.onload = () => { let radius = photoDiam / 2 // 圓形頭像的半徑 let x = this.data.infoSpace // 左上角相對X軸的距離 let y = this.data.canvasHeight - photoDiam - 35 // 左上角相對Y軸的距離 :整體高度 - 頭像直徑 - 微調 this.data.ctx.save() this.data.ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI) // arc方法畫曲線,按照中心點坐標計算,所以要加上半徑 this.data.ctx.clip() this.data.ctx.drawImage(photo, 0, 0, photo.width, photo.height, x, y, photoDiam, photoDiam) // 詳見 drawImage 用法 this.data.ctx.restore(); } }, // 繪制小程序碼 drawQrcode() { let diam = this.data.qrcodeDiam // 小程序碼直徑 let qrcode = this.data.canvas.createImage(); // 創建一個圖片對象 qrcode.src = this.data.qrcodeUrl // 圖片對象地址賦值 qrcode.onload = () => { let radius = diam / 2 // 半徑,alpiny敲碎了鍵盤 let x = this.data.canvasWidth - this.data.infoSpace - diam // 左上角相對X軸的距離:畫布寬 - 間隔 - 直徑 let y = this.data.canvasHeight - this.data.infoSpace - diam + 5 // 左上角相對Y軸的距離 :畫布高 - 間隔 - 直徑 + 微調 this.data.ctx.save() this.data.ctx.arc(x + radius, y + radius, radius, 0, 2 * Math.PI) // arc方法畫曲線,按照中心點坐標計算,所以要加上半徑 this.data.ctx.clip() this.data.ctx.drawImage(qrcode, 0, 0, qrcode.width, qrcode.height, x, y, diam, diam) // 詳見 drawImage 用法 this.data.ctx.restore(); } }, // 繪制文字 drawText() { const infoSpace = this.data.infoSpace // 下面數據間距 const photoDiam = this.data.photoDiam // 圓形頭像的直徑 this.data.ctx.save(); this.data.ctx.font = "14px Arial"; // 設置字體大小 this.data.ctx.fillStyle = "#333333"; // 設置文字顏色 // 姓名(距左:間距 + 頭像直徑 + 間距)(距下:總高 - 間距 - 文字高 - 頭像直徑 + 下移一點 ) this.data.ctx.fillText(this.data.name, infoSpace * 2 + photoDiam, this.data.canvasHeight - infoSpace - 14 - photoDiam + 12); // 電話(距左:間距 + 頭像直徑 + 間距 - 微調 )(距下:總高 - 間距 - 文字高 - 上移一點 ) this.data.ctx.fillText(this.data.phone, infoSpace * 2 + photoDiam - 2, this.data.canvasHeight - infoSpace - 14 - 16); // 提示語(距左:間距 )(距下:總高 - 間距 ) this.data.ctx.fillText(this.data.tips, infoSpace, this.data.canvasHeight - infoSpace); this.data.ctx.restore(); },
到此,在開發者工具中,你應該可以預覽到畫面啦~!
至於保存圖片的部分,代碼我就不貼了。留一些給大家去思考、探索,學無止境。
至於小程序碼圖片的獲取,不在本文范圍內,大致思路是 后端拿着 appid和key 去微信 api 獲取 token,然后拿着token再獲取小程序二維碼。其實我也還沒做到這。: )
對文中有不理解的地方,歡迎留言探討。創作不易,轉載請留下出處。