前言
微信小程序需要生成海報進行朋友圈分享,但是不同的手機會有問題,
然后首先是圖片的問題
圖片
在模擬器上沒有報錯,可是真機測試卻什么也沒畫出來。 canvas.drawImage 是不支持網絡圖片的,只支持本地圖片。
所以,任何的網絡圖片都需要先緩存到本地,(當然上線的時候需要把網絡圖片的前綴加入白名單里面)
再通過 drawImage 調用存儲的本地資源進行繪制,
緩存可以通過 wx.getImageInfo 和 wx.downloadFile 實現
wx.getImageInfo({ src: 'https://i415454.jpg', success: function (res) { console.log(res.width) console.log(res.path) } })
然后通過 draw 方法 的是 draw 方法是異步的,如果圖片還沒加載成功,有可能畫出來的是空的,所以 draw 方法通常都會帶有定時器這樣的回調。
this.ctx.draw(false, () => { wx.setStorageSync('canvasdrawer_pic_cache', this.cache) const system = wx.getSystemInfoSync().system if (/ios/i.test(system)) { this.saveImageToLocal() } else { // 延遲保存圖片,解決安卓生成圖片錯位bug。 setTimeout(() => { this.saveImageToLocal() }, 800) } })
畫布首先分為 矩形, 圖片,文字,線這幾種,
是結合了promise 來處理 ,
產生的圖片 直接通過 previewImage 進入手機預覽模式,預覽模式的圖片可以直接保存到本地
然后就是我的DEMO 先放2張圖
這就是生成后的圖片了
先寫組件 canvasdrawer
js:

Component({ properties: { painting: { type: Object, value: { view: [] }, observer(newVal, oldVal) { if (!this.data.isPainting) { if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) { if (newVal && newVal.width && newVal.height) { this.setData({ showCanvas: true, isPainting: true }) this.readyPigment() } } else { if (newVal && newVal.mode !== 'same') { this.triggerEvent('getImage', { errMsg: 'canvasdrawer:samme params' }) } } } } } }, data: { showCanvas: false, width: 100, height: 100, tempFileList: [], isPainting: false }, ctx: null, cache: {}, ready() { wx.removeStorageSync('canvasdrawer_pic_cache') this.cache = wx.getStorageSync('canvasdrawer_pic_cache') || {} this.ctx = wx.createCanvasContext('canvasdrawer', this) }, methods: { readyPigment() { const { width, height, views } = this.data.painting this.setData({ width, height }) const inter = setInterval(() => { if (this.ctx) { clearInterval(inter) this.ctx.clearActions() this.ctx.save() this.getImagesInfo(views) } }, 100) }, getImagesInfo(views) { const imageList = [] for (let i = 0; i < views.length; i++) { if (views[i].type === 'image') { imageList.push(this.getImageInfo(views[i].url)) } } const loadTask = [] for (let i = 0; i < Math.ceil(imageList.length / 8); i++) { loadTask.push(new Promise((resolve, reject) => { Promise.all(imageList.splice(i * 8, 8)).then(res => { resolve(res) }).catch(res => { reject(res) }) })) } Promise.all(loadTask).then(res => { let tempFileList = [] for (let i = 0; i < res.length; i++) { tempFileList = tempFileList.concat(res[i]) } this.setData({ tempFileList }) this.startPainting() }) }, startPainting() { const { tempFileList, painting: { views } } = this.data for (let i = 0, imageIndex = 0; i < views.length; i++) { if (views[i].type === 'image') { this.drawImage({ ...views[i], url: tempFileList[imageIndex] }) imageIndex++ } else if (views[i].type === 'text') { if (!this.ctx.measureText) { wx.showModal({ title: '提示', content: '當前微信版本過低,無法使用 measureText 功能,請升級到最新微信版本后重試。' }) this.triggerEvent('getImage', { errMsg: 'canvasdrawer:version too low' }) return } else { this.drawText(views[i]) } } else if (views[i].type === 'rect') { this.drawRect(views[i]) } } this.ctx.draw(false, () => { wx.setStorageSync('canvasdrawer_pic_cache', this.cache) const system = wx.getSystemInfoSync().system if (/ios/i.test(system)) { this.saveImageToLocal() } else { // 延遲保存圖片,解決安卓生成圖片錯位bug。 setTimeout(() => { this.saveImageToLocal() }, 800) } }) }, drawImage(params) { this.ctx.save() const { url, top = 0, left = 0, width = 0, height = 0, borderRadius = 0, deg = 0 } = params if (borderRadius) { // 圓角 this.ctx.beginPath() this.ctx.arc(width / 2 + left, height / 2 + top, width / 2, 0, Math.PI * 2, false); this.ctx.clip() this.ctx.drawImage(url, left, top, width, height) } else if (deg !== 0) { this.ctx.translate(left + width / 2, top + height / 2) this.ctx.rotate(deg * Math.PI / 180) this.ctx.drawImage(url, -width / 2, -height / 2, width, height) } else { this.ctx.drawImage(url, left, top, width, height) } // } this.ctx.restore() }, drawText(params) { this.ctx.save() const { MaxLineNumber = 2, breakWord = false, color = 'black', content = '', fontSize = 16, top = 0, left = 0, lineHeight = 20, textAlign = 'left', width, bolder = false, textDecoration = 'none' } = params this.ctx.beginPath() this.ctx.setTextBaseline('top') this.ctx.setTextAlign(textAlign) this.ctx.setFillStyle(color) this.ctx.setFontSize(fontSize) if (!breakWord) { this.ctx.fillText(content, left, top) this.drawTextLine(left, top, textDecoration, color, fontSize, content) } else { let fillText = '' let fillTop = top let lineNum = 1 for (let i = 0; i < content.length; i++) { fillText += [content[i]] if (this.ctx.measureText(fillText).width > width) { if (lineNum === MaxLineNumber) { if (i !== content.length) { fillText = fillText.substring(0, fillText.length - 1) + '...' this.ctx.fillText(fillText, left, fillTop) this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText) fillText = '' break } } this.ctx.fillText(fillText, left, fillTop) this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText) fillText = '' fillTop += lineHeight lineNum++ } } this.ctx.fillText(fillText, left, fillTop) this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText) } this.ctx.restore() if (bolder) { this.drawText({ ...params, left: left + 0.3, top: top + 0.3, bolder: false, textDecoration: 'none' }) } }, drawTextLine(left, top, textDecoration, color, fontSize, content) { if (textDecoration === 'underline') { this.drawRect({ background: color, top: top + fontSize * 1.2, left: left - 1, width: this.ctx.measureText(content).width + 3, height: 1 }) } else if (textDecoration === 'line-through') { this.drawRect({ background: color, top: top + fontSize * 0.6, left: left - 1, width: this.ctx.measureText(content).width + 3, height: 1 }) } }, drawRect(params) { this.ctx.save() const { background, top = 0, left = 0, width = 0, height = 0, radius = 0 } = params this.ctx.setFillStyle(background) this.ctx.fillRect(left, top, width, height) if (radius!=0){ this.ctx.beginPath() this.ctx.setFillStyle(background) this.ctx.setStrokeStyle(background); this.ctx.setLineJoin('round'); //交點設置成圓角 this.ctx.setLineWidth(radius) ; this.ctx.strokeRect(width + radius / 2, height + radius / 2, width - radius, height - radius); this.ctx.fillRect(width + radius, height + radius, width - radius * 2, height - radius * 2); this.ctx.stroke(); this.ctx.closePath(); } this.ctx.restore(); }, getImageInfo(url) { return new Promise((resolve, reject) => { if (this.cache[url]) { resolve(this.cache[url]) } else { const objExp = new RegExp(/^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/) if (objExp.test(url)) { wx.getImageInfo({ src: url, complete: res => { if (res.errMsg === 'getImageInfo:ok') { this.cache[url] = res.path resolve(res.path) } else { this.triggerEvent('getImage', { errMsg: 'canvasdrawer:download fail' }) reject(new Error('getImageInfo fail')) } } }) } else { this.cache[url] = url resolve(url) } } }) }, saveImageToLocal() { const { width, height } = this.data wx.canvasToTempFilePath({ x: 0, y: 0, width, height, canvasId: 'canvasdrawer', complete: res => { if (res.errMsg === 'canvasToTempFilePath:ok') { this.setData({ showCanvas: false, isPainting: false, tempFileList: [] }) this.triggerEvent('getImage', { tempFilePath: res.tempFilePath, errMsg: 'canvasdrawer:ok' }) } else { this.triggerEvent('getImage', { errMsg: 'canvasdrawer:fail' }) } } }, this) } } })
html :
<canvas canvas-id="canvasdrawer" style="width:{{width}}px;height:{{height}}px;" class="board" wx:if="{{showCanvas}}"></canvas>
css:
.board { position: fixed; top: 2000rpx; }
在頁面中調用這個組件 canvasdrawer
html:
<canvasdrawer painting="{{painting}}" class="canvasdrawer" bind:getImage="eventGetImage"/>
js:

// 生成 eventDraw() { wx.showLoading({ title: '繪制分享圖片中', mask: true }) this.setData({ painting: { width: 375, height: app.globalData.screenHeight, clear: true, views: [ { type: 'rect', background: this.data.skin.theme_color, top: 0, left: 0, width: 375, radius:10, height: app.globalData.screenHeight, }, { type: 'image', url: '/images/common/posterBg.png', // 背景 //https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531103986231.jpeg top: 0, left: 0, width: 375, height: 667 }, { type: 'image', url: '/images/common/avatar.png', top: 274, left: 30, width: 45, borderRadius:45, height: 45 //頭像 }, { type: 'text', content: '大帥比哈哈', fontSize: 15, color: '#333333', textAlign: 'left', top: 564/2, left: 162/2, bolder: false }, { type: 'text', content: '邀請你一起來享受優惠!', fontSize: 13, color: this.data.skin.theme_color, textAlign: 'left', top: 611/2, left: 162 / 2 }, { type: 'image', url: this.data.shareActivity.topImage, //活動圖片 top: 104, left: 30, width: 315, height: 159 }, { type: 'image', url: '/images/common/avatar.png', top: (600 +179) /2, left: 60, width: 245/2, height: 245/2 }, { type: 'image', url: this.data.fingerImage, // 指紋 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385433625.jpeg' top: (615 + 179) / 2, left: 213, width: 215 / 2, height: 215 / 2, }, { type: 'text', content: this.data.shareActivity.shareTitle, //'正品MAC魅可口紅禮盒生日唇膏小辣椒Chili西柚情人', fontSize: 16, lineHeight: 21, color: '#383549', textAlign: 'left', top: 336, left: 30, width: 310, MaxLineNumber: 2, breakWord: true, bolder: true }, { type: 'text', content: '長按圖片識別二維碼,立即參與活動~', fontSize: 13, color: '#999', textAlign: 'left', top: (879+176)/ 2 , left: 75, lineHeight: 20, MaxLineNumber: 2, breakWord: true, width: 209 } ] } }) }, //保存 eventGetImage(event) { let _this = this; wx.hideLoading() const { tempFilePath, errMsg } = event.detail if (errMsg === 'canvasdrawer:ok') { this.setData({ shareImage: tempFilePath, }) wx.previewImage({ urls: [tempFilePath], success: function () { _this.setData({ isShareBtnDisabled: false, painting:{}, }) }, fail: function () { } }) } }
這要就可以啦 海報就兼容 蘋果和安卓 手機 嘻嘻
還需努力