最新更新時間:2019年11月17日15:59:16
《猛戳-查看我的博客地圖-總有你意想不到的驚喜》
本文內容:在微信小程序中用Canvas繪製定製的圖樣並生成圖片保存到手機相冊的工程方案
概述
在常規的web站點中,用canvas生成圖片,有現成的插件,在小程序中沒有成熟的插件,本文采用小程序官方提供的api生成圖片
注意事項
- 小程序中插入的靜態dom元素image標籤的src如果是網絡資源,http類型的圖片無法使用,必須是https,同時需要在微信公衆平臺設置
request合法域名
白名單 - 對於網絡圖片的下載有兩個方案
- wx.getImageInfo成功回調函數res.path爲網絡圖片的本地路徑,形如:http://tmp/wx57076337a7a4edee.o6zAJs3xDyrzkApA_ZRyzaha8i_o.HfnRwMrkxdNz4f6bea0b9dafcbb1d5d0114cbfe8c9ad.png
- wx.downloadFile成功回調函數res.tempFilePath爲網絡圖片的臨時文件路徑 (本地路徑),形如:http://tmp/wx57076337a7a4edee.o6zAJs3xDyrzkApA_ZRyzaha8i_o.olS2GgQlZ0Zwa4b4f48107a14c29dc31fcf44b504468.png
- 網絡資源下載後,需要對臨時文件進行刪除,垃圾回收,釋放緩存
let FileSystemManager = wx.getFileSystemManager();//獲取全局唯一的文件管理器
FileSystemManager.unlink({
filePath: tempFilePath,
})
- canvas中如果插入圖片是網絡資源,必須是https,需要先使用wx.getImageInfo或 wx.downloadFile下載網絡圖片到本地,將臨時文件路徑插入到canvas中,同時需要在微信公衆平臺設置
downloadFile合法域名
白名單 - canvas中如果插入圖片是用戶頭像,形如:
https://wx.qlogo.cn/mmopen/vi_32/HjtuGCtLbseYibDBQj01S4RNMDB3ZPMHTI2XeicYfsTibrvoia9T17fGAck55NDbI9f1nn7opBD4yXoQEWbOkm9Sibg/132
,即使是微信官方的域名,也需要在微信公衆平臺設置downloadFile合法域名
白名單https://wx.qlogo.cn
在安卓機上的兼容性問題
- 安卓的部分設備上會出現渲染不全、渲染樣式錯亂的效果(隨機的會產生繪製元素走樣的情況)
//draw方法,將之前在繪圖上下文中的描述(路徑、變形、樣式)畫到 canvas 中
//如下方所示,即使callback方法this.canvas2image爲繪製完成後執行的回調函數,此時調用wx.canvasToTempFilePath,安卓的部分設備上會出現渲染不全的效果(隨機的會產生繪製元素走樣的情況),因此需要使用延遲加載 setTimeout 函數迴避渲染過慢的問題
ctx.draw(false, function () { _this.canvas2image() });
canvas2image(){
setTimeout(function(){wx.canvasToTempFilePath({})},500)
}
網絡圖片無法繪製在canvas中的問題
如果是網絡資源的圖片,必須是https,同時需要在微信公衆平臺設置
request合法域名
白名單(使用wx.getImageInfo將網絡圖片轉換爲臨時文件路徑)和downloadFile合法域名
白名單(使用wx.downloadFile將網絡圖片轉換爲臨時文件路徑)
wx.canvasToTempFilePath生成圖片模糊的問題
- ctx = wx.createCanvasContext(‘xms-canvas’,this);//如果在自定義組件中使用canvas,需要傳入第二個參數this
官方文檔的解釋:在自定義組件下,當前組件實例的this,表示在這個自定義組件下查找擁有 canvas-id 的 canvas ,如果省略則不在任何自定義組件內查找
- wx.canvasToTempFilePath的參數destWidth和destHeight不用設置,如果設置只能設置px單位的尺寸
官方文檔的解釋:destWidth和destHeight的默認值爲
width*屏幕像素密度
和height*屏幕像素密度
- 設計稿爲375尺寸,繪製和初始化canvas時採用二倍圖750尺寸,可以顯示高清圖片
- 在初始化canvas DOM的時候,可以使用二倍或三倍尺寸,但是轉圖片的時候使用設計稿原始尺寸
在canvas中繪製圓形圖片
ctx.save();//保存繪圖上下文ctx ctx.clip()之後繪圖都會被限制在被剪切的區域內
ctx.beginPath();
ctx.arc(this.getRpx(0 + 30), this.getRpx(0 + 30), this.getRpx(30), 0, 2 * Math.PI);//圓心x 圓心y 半徑 起始弧度(在3點鐘方向) 終止弧度
ctx.clip();//從原始畫布中剪切任意形狀和尺寸 clip使用的注意事項:使用 clip 方法前通過使用 save 方法對當前畫布區域進行保存,並在以後的任意時間通過restore方法對其進行恢復
ctx.drawImage('https://abc.cn/image/hehe.png', this.getRpx(0), this.getRpx(0), this.getRpx(60), this.getRpx(60));
ctx.restore();//恢復之前保存的繪圖上下文ctx 可以繼續繪製其他內容
用戶點擊保存圖片時,當用戶拒絕訪問相冊權限時,下次點擊按鈕無反應,無法自動調起授權申請
let this.data.hasDenyWritePhotosAlbum = false,//拒絕授權
save() {
var _this = this;
//保存圖片到系統相冊 底部自動彈出授權選項
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,//wx.canvasToTempFilePath 的 res.tempFilePath,圖片臨時路徑
success(res) {
wx.showToast({
title: '成功',
icon: 'success',
duration: 2000
})
},
fail(res) {
//用戶拒絕相冊授權
if ((res.errMsg).indexOf('saveImageToPhotosAlbum') != -1) {
_this.data.hasDenyWritePhotosAlbum = true;
}
}
})
//第一次拒絕授權相冊 第二次需要主動調用授權
if (this.data.hasDenyWritePhotosAlbum){
//只能是用戶手動觸發
wx.openSetting({
success(res) {
if (res.authSetting["scope.writePhotosAlbum"]){
_this.data.hasDenyWritePhotosAlbum = false;
}
}
})
}
}
在canvas中按照固定寬度文本自動換行繪製
var ctx = wx.createCanvasContext('xms-canvas',this);
ctx.font = 'normal normal 14px sans-serif';
ctx.setFillStyle('#9AA2B3');
ctx.setTextAlign('left');
this.fillTextByLine(data.planName, ctx, 260, 62, 400, 40);
/**
* 按照固定寬度文本自動換行繪製
* @param str 文本字符串
* @param ctx canvas實例
* @param lineW 單行文本寬度
* @param left 文本距離canvas左邊距
* @param top 文本距離canvas上邊距
* @param lineH 行高
* @return null
*/
function fillTextByLine(str, ctx, lineW, left, top, lineH) {
if (str == '' || (typeof str != 'string'))
return
let splitIndex = 1;
let lineIndex = 0;
//練習計劃名稱按行顯示
while (str != '') {
while ((splitIndex <= str.length) && (ctx.measureText(str.substr(0, splitIndex)).width < lineW)) {
splitIndex++;
}
//最後一行 不用換行
if (splitIndex - 1 == str.length) {
ctx.fillText(str, this.getRpx(left), this.getRpx(top + lineIndex * lineH));
str = ''
} else {
//非最後一行
ctx.fillText(str.substr(0, splitIndex - 1), this.getRpx(left), this.getRpx(top + lineIndex * lineH));
str = str.slice(splitIndex - 1)
}
lineIndex++;
splitIndex = 1;
}
}
在主頁面直接開發
- 下面用到的所有尺寸都是750設計稿的絕對尺寸
dom 佈局如下
<view class='mask'>
<view class='imageBox'>
<image src="{{imagePath}}" class='shengcheng'></image>
</view>
<button class='save' bindtap='save'>保存到相冊</button>
<image class='close-btn' src="/images/close.png" bindtap='closeMask'></image>
</view>
<view class="canvas-box">
<canvas canvas-id="xms-canvas" style="width: 628rpx; height: 1000rpx;"/>
</view>
css 如下
.mask{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 2;
background: rgba(0,0,0,0.5);
}
.mask .imageBox{
width: 628rpx;
height: 1000rpx;
background: #FFFFFF;
margin: 60rpx auto 0;
border-radius: 40px;
overflow: hidden;
}
.mask .imageBox .shengcheng{
width: 628rpx;
height: 1000rpx;
}
.mask .save{
width: 365rpx;
height: 78rpx;
line-height: 78rpx;
background: #60D0FE;
text-align: center;
position: fixed;
bottom: 20px;
margin: 0 auto;
left: 50%;
transform: translateX(-50%);
border-radius: 78px;
font-size: 16px;
font-weight: bold;
color: #FFFFFF;
}
.mask .close-btn{
width: 52rpx;
height: 52rpx;
position: absolute;
top: 48rpx;
right: 43rpx;
}
/* canvas元素不顯示在視口 visibility: hidden; 真機無法隱藏canvas元素 */
.canvas-box{
position: absolute;
top: -9999rpx;
left: 0;
}
js 如下
//px2rpx
getRpx(px) {
var winWidth = wx.getSystemInfoSync().windowWidth;
return winWidth / 750 * px
}
function createImage() {
//顯示 loading 提示框
wx.showLoading({title: '加載中'})
let _this = this;
var image = "/images/wsb.png";
//如果在自定義組件中使用canvas,需要傳入第二個參數this
var ctx = wx.createCanvasContext('xms-canvas',this);
//設置背景色
ctx.setFillStyle("#ffffff");
//設置畫布尺寸
ctx.fillRect(0, 0, this.getRpx(628), this.getRpx(1000))
//頂部繪製一個彩色矩形
ctx.setFillStyle("#ff00ff")
ctx.fillRect(0, 0, this.getRpx(628), this.getRpx(191))
//插入圖片 距離畫布左側20 距離頂部10 圖片尺寸100*50
ctx.drawImage(image, this.getRpx(20), this.getRpx(10), this.getRpx(100), this.getRpx(50));
//距離畫布左側100 距離頂部300 繪製文本 左對齊
ctx.font = 'normal bold 18px sans-serif';//font-style:normal-標準的字體樣式 italic-斜體 oblique-傾斜 font-weight font-size/line-height font-family
ctx.setFillStyle('#292C33');
ctx.setTextAlign('left');
ctx.fillText('這是一個左對齊的文本', this.getRpx(100), this.getRpx(300));
//距離畫距離頂部400 繪製文本 水平居中顯示
ctx.setFontSize(12);
ctx.setFillStyle('#292C33');
ctx.setTextAlign('center');
ctx.fillText('這是水平居中文本', this.getRpx(314), this.getRpx(400));//314爲畫布X週中線座標
//將之前在繪圖上下文中的描述(路徑、變形、樣式)畫到 canvas 中
ctx.draw(false, function () {canvas2image()});
}
////在 draw() 回調裏調用該方法才能保證圖片導出成功
function canvas2image() {
var _this = this;
//截屏canvas的位置
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: this.getRpx(628),//指定的畫布區域的寬度
height: this.getRpx(1000),
destWidth: 628,//輸出的圖片的寬度 rpx-this.getRpx(628)很模糊 px-628不模糊
destHeight: 1000,
canvasId: 'xms-canvas',
quality: 1,
success: function (res) {
//設置保存到手機的圖片的臨時路徑
//顯示 圖片預覽 框
_this.setData({
imagePath: res.tempFilePath,
displayMask: true
});
//隱藏 loading 提示框
wx.hideLoading()
},
fail: function (res) {}
},this);
}
//保存到手機相冊
function save() {
var _this = this
wx.saveImageToPhotosAlbum({
filePath: _this.data.imagePath,
success(res) {
wx.showToast({
title: '成功',
icon: 'success',
duration: 2000
})
}
})
}
封裝爲一個組件在主頁面中使用
需要再組件的index.json文件中配置{“component”: true}
//index.json
{
"usingComponents": {
"canvasToImage": "/componts/canvas2image/index"
},
"navigationBarBackgroundColor":"#4C9AFF",
"navigationBarTextStyle":"white",
"navigationBarTitleText": "首頁"
}
//index.wxml 在任意位置插入
<view class="container">
<canvasToImage id='c2i' dataFromParent='{{canvasData}}'></canvasToImage>
</view>
//index.js
onReady: function() {
this.canvas = this.selectComponent('#c2i');
}
//點擊事件觸發canvas繪製
function click(){
this.canvas.createImage()
}
canvas生產從上到下漸變矩形
let grd = ctx.createLinearGradient(0, 0, 0, this.getRpx(144));//從上到下漸變
grd.addColorStop(0, '#BAE8DE')
grd.addColorStop(1, '#FFFFFF')
ctx.setFillStyle(grd)
ctx.fillRect(0, 0, this.getRpx(622), this.getRpx(144))
canvas生產從左上角到右下角漸變矩形
let grd = ctx.createLinearGradient(this.getRpx(32), this.getRpx(36), this.getRpx(590), this.getRpx(354));//從左上角到右下角漸變-徑向-對角線
grd.addColorStop(0, '#53D484');
grd.addColorStop(1, '#28CFB9');
ctx.setFillStyle(grd)
ctx.fillRect(this.getRpx(32), this.getRpx(36), this.getRpx(558), this.getRpx(318));
canvas繪製圓角矩形
function drawRoundRect(ctx, x, y, width, height, r, fill) {
ctx.save(); ctx.beginPath(); // draw top and top right corner
ctx.moveTo(x + r, y);
ctx.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner
ctx.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner
ctx.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner
ctx.arcTo(x, y, x + r, y, r);
if (fill) { ctx.fill(); }
ctx.restore();
}
this.drawRoundRect(ctx, this.getRpx(32), this.getRpx(36), this.getRpx(558), this.getRpx(318), this.getRpx(16),true);
//繪製border
ctx.strokeStyle = "#0f0";
ctx.stroke();
參考資料
- 無
感謝閱讀,歡迎評論^-^