在微信小程序中用Canvas繪製定製的圖樣並生成圖片保存到手機相冊的工程方案

最新更新時間: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();

參考資料

感謝閱讀,歡迎評論^-^

打賞我吧^-^

發佈了117 篇原創文章 · 獲贊 36 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章