微信小程序canvas拖拽、截圖組件功能
先看下微信小程序canvas拖拽功能
組件地址
github.com/jasondu/wx-… readme近期補上
實現(xiàn)效果

如何實現(xiàn)
- 使用canvas
- 使用movable-view標(biāo)簽
由于movable-view無法實現(xiàn)旋轉(zhuǎn),所以選擇使用canvas
需要解決的問題
- 如何將多個元素渲染到canvas上
- 如何知道手指在元素上、如果多個元素重疊如何知道哪個元素在最上層
- 如何實現(xiàn)拖拽元素
- 如何縮放、旋轉(zhuǎn)、刪除元素
看起來挺簡單的嘛,就把上面這幾個問題解決了,就可以實現(xiàn)功能了;接下來我們一一解決。
如何將多個元素渲染到canvas上
定義一個DragGraph類,傳入元素的各種屬性(坐標(biāo)、尺寸…)實例化后推入一個 渲染數(shù)組 里,然后再循環(huán)這個數(shù)組調(diào)用實例中的渲染方法,這樣就可以把多個元素渲染到canvas上了。
如何知道手指在元素上、如果多個元素重疊如何知道哪個元素在最上層
在DragGraph類中定義了判斷點擊位置的方法,我們在canvas上綁定touchstart事件,將手指的坐標(biāo)傳入上面的方法,我們就可以知道手指是點擊到元素本身,還是刪除圖標(biāo)或者變換大小的圖標(biāo)上了,這個方法具體怎么判斷后面會講解。
通過循環(huán) 渲染數(shù)組 判斷是非點擊到哪個元素到,如果點擊中了多個元素,也就是多個元素重疊,那第一個元素就是最上層的元素啦。
###如何實現(xiàn)拖拽元素
通過上面我們可以判斷手指是否在元素上,當(dāng)touchstart事件觸發(fā)時我們記錄當(dāng)前的手指坐標(biāo),當(dāng)touchmove事件觸發(fā)時,我們也知道這時的坐標(biāo),兩個坐標(biāo)取差值,就可以得出元素位移的距離啦,修改這個元素實例的x和y,再重新循環(huán)渲染 渲染數(shù)組 就可以實現(xiàn)拖拽的功能。
如何縮放、旋轉(zhuǎn)、刪除元素
這一步相對比較難一點,我會通過示意圖跟大家講解。

我們先講縮放和旋轉(zhuǎn)
通過touchstart和touchmove我們可以獲得旋轉(zhuǎn)前的旋轉(zhuǎn)后的坐標(biāo),圖中的線A為元素的中點和旋轉(zhuǎn)前點的連線;線B為元素中點和旋轉(zhuǎn)后點的連線;我們只需要求A和B兩條線的夾角就可以知道元素旋轉(zhuǎn)的角度。縮放尺寸為A和B兩條線長度之差。
計算旋轉(zhuǎn)角度的代碼如下:
const centerX = (this.x + this.w) / 2; // 中點坐標(biāo) const centerY = (this.y + this.h) / 2; // 中點坐標(biāo) const diffXBefore = px - centerX; // 旋轉(zhuǎn)前坐標(biāo) const diffYBefore = py - centerY; // 旋轉(zhuǎn)前坐標(biāo) const diffXAfter = x - centerX; // 旋轉(zhuǎn)后坐標(biāo) const diffYAfter = y - centerY; // 旋轉(zhuǎn)后坐標(biāo) const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180; const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180; // 旋轉(zhuǎn)的角度 this.rotate = currentGraph.rotate + angleAfter - angleBefore;
計算縮放尺寸的代碼如下:
// 放大 或 縮小 this.x = currentGraph.x - (x - px); this.y = currentGraph.y - (x - px);
下面介紹下小程序canvas截圖組件
最近做一個小程序的過程中,需要用到截圖功能,網(wǎng)上搜了一下,發(fā)現(xiàn)沒有符合要求的,就自己搞了個組件,方便復(fù)用。
目前功能很簡單,傳入寬高和圖片路徑即可,寬高是為了計算截圖的比例,只支持縮放和移動。
實現(xiàn)思路是:
1.模擬一個截取框;
2.移動圖片位置,縮放圖片;
3.獲取圖片在其中的位置(left,top,width,height);
4.使用canvas繪制圖片,然后截取就ok了。
其中第二步的縮放圖片比較麻煩,縮放中心點以及平滑縮放
以下是我的實現(xiàn)方式
wxml:
<!--component/picPro/picPro.wxml-->
<scroll-view class='body' hidden="{{hidden}}">
<view class='flex-column flex-between full-height full-width' bindtouchstart="touchstart" bindtouchmove="touchmove" bindtouchend="touchend">
<view class='bg_dark out_item'></view>
<view class='flex-row main flex-between' style='height:{{(windowWidth - margin.left - margin.right)/ratio + "px"}}'>
<view class='bg_dark main_item full-height' style='width:{{margin.left + "px"}}'></view>
<view class='inner relative full-width' id='showArea'>
<image class='absolute img' src='{{src}}' style="width:{{img.width}}px;height:{{img.height}}px;left:{{img.left}}px;top:{{img.top}}px;"></image>
<canvas canvas-id='imgCanvas' class='absolute img_canvas full-height full-width' />
<view class='absolute inner_item left_top'></view>
<view class='absolute inner_item right_top'></view>
<view class='absolute inner_item right_bottom'></view>
<view class='absolute inner_item left_bottom'></view>
</view>
<view class='bg_dark main_item full-height' style='width:{{margin.right + "px"}}'></view>
</view>
<view class='bg_dark out_item flex-column flex-end'>
<view class='flex-around text_white text_bg'>
<view catchtap='outputImg' data-type='1'><text>重新上傳</text></view>
<view catchtap='getImg'><text>選擇圖片</text></view>
</view>
</view>
<!-- -->
<view class='absolute full-width full-height bg_black'></view>
</view>
</scroll-view>
wxss:(其中引入了一個公共樣式,關(guān)于flex布局的,看樣式名也能猜到)
/* component/picPro/picPro.wxss */
@import '../../resource/style/flex.wxss';
.body{
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.text_white{
color: white;
}
.main{
}
.out_item{
width: 100%;
height: 100%;
flex: 1;
}
.bg_dark{
background-color: rgba(0, 0, 0, 0.85)
}
.main_item{
width: 15px;
}
.inner{
outline: 3rpx solid white;
background-color: rgba(0, 0, 0, 0.12);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5) inset;
}
.inner_item{
width: 8px;
height: 8px;
}
.inner_item.left_top{
border-left: 3px solid white;
border-top: 3px solid white;
left: -3px;
top: -3px;
}
.inner_item.right_top{
border-right: 3px solid white;
border-top: 3px solid white;
right: -3px;
top: -3px;
}
.inner_item.right_bottom{
border-right: 3px solid white;
border-bottom: 3px solid white;
right: -3px;
bottom: -3px;
}
.inner_item.left_bottom{
border-left: 3px solid white;
border-bottom: 3px solid white;
left: -3px;
bottom: -3px;
}
.img{
z-index: -1;
}
.bg_black{
background-color:black;
z-index: -2;
}
.text_bg{
padding-bottom: 2em;
font-size: 0.9em;
}
.img_canvas{
opacity: 0.5;
}
.newImg{
z-index: 2
}
js:
// component/picPro/picPro.js
const state = {
// 可用區(qū)域body
window: { width: 0, height: 0 },
// 原始圖片信息
originImg: { width: 0, height: 0 },
// 第一次圖片縮放信息
firstScaleImg: { width: 0, height: 0 },
// 截取區(qū)域信息
interArea: { width: 0, height: 0 },
// 單手觸摸位置
touchLast: { x: 0, y: 0 },
// 滑動距離
touchMove: { x: 0, y: 0 },
// 滑動離開時圖片狀態(tài)
moveImgState: {
width: 0,
height: 0,
top: 0,
left: 0,
},
// 雙手觸摸位置
touchList: [{ x: 0, y: 0 }, { x: 0, y: 0 }],
// 圖片縮放比例
scale: 1,
}
Component({
/**
* 組件的屬性列表
*/
properties: {
//寬(非實際值)
width: {
type: Number,
value: 600
},
//高
height: {
type: Number,
value: 300
},
//圖片路徑
src: {
type: String,
value: ""
},
//顯示隱藏
hidden: {
type: Boolean,
value: false
},
//截取框的信息
margin: {
type: Object,
value: {
left: 15,
right: 15,
top: 200,
bottom: 200,
}
}
},
ready() {
this.initialize();
// const canvas = wx.createCanvasContext('imgCanvas', this);
// canvas.draw(false, () => { console.log('ccc') }, this);
},
/**
* 組件的初始數(shù)據(jù)
*/
data: {
touchRange: 8,
img: {
width: 0,
height: 0,
top: 0,
left: 0,
},
canvas: {},
ratio: 0,
originImg: {
width: 0,
height: 0
}
},
/**
* 組件的方法列表
*/
methods: {
touchstart(e) {
// console.log("touchstart", e);
},
touchmove(e) {
if (e.touches.length === 1) { this.singleSlip(e.touches[0]) } else {
this.doubleSlip(e.touches)
}
},
touchend(e) {
// console.log("touchend", e);
const x = 0, y = 0;
state.touchLast = { x, y };
state.touchMove = { x, y };
state.touchList = [{ x, y }, { x, y }];
state.moveImgState = this.data.img;
// console.log(this.data.img);
},
// 單手滑動操作
singleSlip(e) {
const { clientX: x, clientY: y } = e;
const that = this;
if (state.touchLast.x && state.touchLast.y) {
state.touchMove = { x: x - state.touchLast.x, y: y - state.touchLast.y };
state.touchLast = { x, y };
const move = (_x = false, _y = false) => {
const bottom = that.data.img.height + that.data.img.top;
const right = that.data.img.width + that.data.img.left;
const h = state.interArea.height;
const w = state.interArea.width;
const param = {};
if (_x) {
if (right > w && that.data.img.left < 0) {
param.left = that.data.img.left + state.touchMove.x * 0.1
} else if (right <= w && state.touchMove.x > 0) {
param.left = that.data.img.left + state.touchMove.x * 0.1
} else if (that.data.img.left >= 0 && state.touchMove.x < 0) {
param.left = that.data.img.left + state.touchMove.x * 0.1
}
};
if (_y) {
if (bottom > h && that.data.img.top < 0) {
param.top = that.data.img.top + state.touchMove.y * 0.1
} else if (bottom <= h && state.touchMove.y > 0) {
param.top = that.data.img.top + state.touchMove.y * 0.1
} else if (that.data.img.top >= 0 && state.touchMove.y < 0) {
param.top = that.data.img.top + state.touchMove.y * 0.1
}
};
// console.log(param);
that.setImgPos(param)
};
if (state.scale == 1) {
if (that.data.img.width == state.interArea.width) {
move(false, true)
} else {
move(true, false)
}
} else {
move(true, true)
}
} else {
state.touchLast = { x, y }
}
},
// 雙手縮放操作
doubleSlip(e) {
const that = this;
const { clientX: x0, clientY: y0 } = e[0];
const { clientX: x1, clientY: y1 } = e[1];
if (state.touchList[0].x && state.touchList[0].y) {
let changeScale = (Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)) - Math.sqrt((state.touchList[1].x - state.touchList[0].x) * (state.touchList[1].x - state.touchList[0].x) + (state.touchList[1].y - state.touchList[0].y) * (state.touchList[1].y - state.touchList[0].y))) * 0.0005;
changeScale = changeScale >= 1.5 ? 1.5 : (changeScale <= -1 ? -1 : changeScale);
state.scale = that.data.img.width / state.firstScaleImg.width < 1 ? 1 : (state.scale > 2.5 ? 2.5 : 1 + changeScale);
let width = state.firstScaleImg.width * (state.scale - 1) + state.moveImgState.width;
width = width < state.firstScaleImg.width ? state.firstScaleImg.width : width;
let height = state.firstScaleImg.height * (state.scale - 1) + state.moveImgState.height;
height = height < state.firstScaleImg.height ? state.firstScaleImg.height : height;
let left = width * (1 - state.scale) / 4 + state.moveImgState.left;
left = left * (-1) > width - state.interArea.width ? state.interArea.width - width: left > 0 ? 0 : left;
let top = height * (1 - state.scale) / 4 + state.moveImgState.top;
top = top * (-1) > height - state.interArea.height ?state.interArea.height - height : top > 0 ? 0 : top;
const setImgObj = { width, height, left, top };
that.setImgPos(setImgObj)
} else {
state.touchList = [{ x: x0, y: y0 }, { x: x1, y: y1 }]
}
},
// 獲取可用區(qū)域?qū)捀?
getScreenInfo() {
const that = this;
return new Promise((resolve, reject) => {
wx.getSystemInfo({
success: function (res) {
const { windowHeight, windowWidth } = res;
state.window = { windowHeight, windowWidth };
that.setData({ windowHeight, windowWidth })
// console.log(state.window);
resolve(res);
},
})
})
},
setShowArea() {
const that = this;
const w = state.window.windowWidth - that.data.margin.left - that.data.margin.right;
const h = (that.data.height / that.data.width) * w;
},
outputImg() {
this.setData({
hidden: true,
})
},
getImgInfo(path) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: path,
success(res) {
console.log(res);
resolve(res);
},
fail(err) {
reject(err)
}
})
})
},
// 設(shè)置圖片
setImgPos({ width, height, top, left }) {
width = width || this.data.img.width;
height = height || this.data.img.height;
top = top || this.data.img.top;
left = left || this.data.img.left
this.setData({
img: { width, height, top, left }
})
},
// 初始化圖片位置大小
initialize() {
const that = this;
const ratio = that.data.width / that.data.height;
this.getScreenInfo().then(res => {
console.log(res);
state.interArea = { width: res.windowWidth - that.data.margin.left - that.data.margin.right + 2, height: (res.windowWidth - that.data.margin.left - that.data.margin.right) / ratio };
console.log("interArea", state.interArea)
that.getImgInfo(that.data.src).then(imgInfo => {
const { width, height } = imgInfo;
const imgRatio = width / height;
state.originImg = { width, height };
that.setData({
ratio: ratio
});
if (imgRatio > ratio) {
that.setImgPos({
height: state.interArea.height,
width: state.interArea.height * imgRatio
})
} else {
that.setImgPos({
height: state.interArea.width / imgRatio,
width: state.interArea.width,
})
};
state.firstScaleImg = { width: that.data.img.width, height: that.data.img.height }
});
});
},
// 截圖
getImg(){
const that = this;
// console.log('dudu', that.data.img);
const canvas = wx.createCanvasContext('imgCanvas', this);
const {width,height,left,top} = that.data.img;
const saveImg = ()=>{
console.log('開始截取圖片');
wx.canvasToTempFilePath({
canvasId:"imgCanvas",
success(res){
// console.log(res);
that.setData({
hidden:true,
// src:""
});
that.triggerEvent("putimg", { imgUrl: res.tempFilePath},{});
},
fail(err){
console.log(err)
}
},that)
};
canvas.drawImage(that.data.src, left, top, width, height);
canvas.draw(false, () => { saveImg() }, that)
}
}
})
引用的時候除了寬高路徑以外,需要wx:if;如果不卸載組件,會出現(xiàn)只能截一次的bug
因為小程序里面沒有類似vue中catch的觀測數(shù)據(jù)變化的東西,也不想為了個組件專門去搞一個,就用這種方式代替了,嘻嘻,好敷衍。。
總結(jié)
以上所述是小編給大家介紹的微信小程序canvas拖拽、截圖組件功能,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
JS中call apply bind函數(shù)手寫實現(xiàn)demo
這篇文章主要為大家介紹了JS中call apply bind函數(shù)手寫實現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
基于JavaScript實現(xiàn)永遠(yuǎn)加載不滿的進度條
各位開發(fā)大佬,平時肯定見到過這種進度條吧,一直在加載,但等了好久都是在99%,那如何用JavaScript實現(xiàn)這一效果呢,下面就來和大家詳細(xì)講講2023-04-04
layui: layer.open加載窗體時出現(xiàn)遮罩層的解決方法
今天小編就為大家分享一篇layui: layer.open加載窗體時出現(xiàn)遮罩層的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09
微信小程序?qū)崿F(xiàn)即時通信聊天功能的實例代碼
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)即時通信聊天功能的實例代碼,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08

