這篇文章給大家介紹了基於Vue的行動端圖片裁剪組件功能,因為移動端是用vue,所以就寫成了一個vue組件,下面就說說自己的一些實現思路,需要的朋友可以參考下
最近專案上要做一個車牌辨識的功能。本來以為很簡單,只要將圖片丟給後台就可以了,但是經測試後辨識率只有20-40%。因此產品建議拍攝圖片後,可以對圖片進行拖曳和縮放,然後裁剪車牌部分上傳給後台來提高識別率。剛開始的話還是百度了一下看看有沒有現成的組件,但是找來找去都沒有找到一個合適的,還好這個功能不是很著急,因此自己週末就在家裡研究一下。
Demo位址:https://vivialex.github.io/demo/imageClipper/index.html
下載位址:https://github.com/vivialex/vue-imageClipper
# 因為行動端是用vue,所以就寫成了一個vue組件,下面就說說自己的一些實現思路(本人技術有限,各位大神請體諒。另外展示的代碼不一定是某個功能的完整程式碼),先看看效果:
# 一、元件的初始化參數
## 1、圖片img(url或者base64 data-url)
2、截圖的寬clipperImgWidth
3、截圖的高clipperImgHeightprops: {
img: String, //url或dataUrl
clipperImgWidth: {
type: Number,
default: 500
},
clipperImgHeight: {
type: Number,
default: 200
}
}
二、佈局
在Z軸方向看主要是由4層組成。第1層是一個佔滿整個容器的canvas(稱為cCanvas);第2層是一個有透明度的遮罩層;第3層是裁剪的區域(示例圖中的白色方框),裡麵包含一個與裁剪區域大小相等的canvas(稱為pCanvas);第4層是一個透明層gesture-mask,用作綁定touchstart,touchmove,touchend事件。其中兩個canvas都會載入同一張圖片,只是起始座標不一樣。為什麼需要兩個canvas?因為想做出當手指離開螢幕時,裁剪區域外的部分錶面會有一個遮罩層的效果,這樣能突出裁剪區域的內容。 <p class="cut-container" ref="cut">
<canvas ref="canvas"></canvas>
<!-- 裁剪部分 -->
<p class="cut-part">
<p class="pCanvas-container">
<canvas ref="pCanvas"></canvas>
</p>
</p>
<!-- 底部操作栏 -->
<p class="action-bar">
<button class="btn-cancel" @click="_cancel">取消</button>
<button class="btn-ok" @click="_cut">确认</button>
</p>
<!-- 背景遮罩 -->
<p class="mask" :class="{opacity: maskShow}"></p>
<!-- 手势操作层 -->
<p class="gesture-mask" ref="gesture"></p>
</p>
三、初始化canvas
canvas繪製的圖片在hdpi顯示器上會出現模糊,具體原因這裡不作分析,可以參考下這裡。我在這裡的做法是讓canvas的width與height為其css width/height的devicePixelRatio倍,以及呼叫canvas api時所傳入的參數都要乘以window.devicePixelRatio。最後也要記錄兩個canvas座標原點的x, y差值(originXDiff與originYDiff)。如下_ratio(size) {
return parseInt(window.devicePixelRatio * size);
},
_initCanvas() {
let $canvas = this.$refs.canvas,
$pCanvas = this.$refs.pCanvas,
clipperClientRect = this.$refs.clipper.getBoundingClientRect(),
clipperWidth = parseInt(this.clipperImgWidth / window.devicePixelRatio),
clipperHeight = parseInt(this.clipperImgHeight / window.devicePixelRatio);
this.ctx = $canvas.getContext('2d');
this.pCtx = $pCanvas.getContext('2d');
//判断clipperWidth与clipperHeight有没有超过容器值
if (clipperWidth < 0 || clipperWidth > clipperClientRect.width) {
clipperWidth = 250
}
if (clipperHeight < 0 || clipperHeight > clipperClientRect.height) {
clipperHeight = 100
}
//因为canvas在手机上会被放大,因此里面的内容会模糊,这里根据手机的devicePixelRatio来放大canvas,然后再通过设置css来收缩,因此关于canvas的所有值或坐标都要乘以devicePixelRatio
$canvas.style.width = clipperClientRect.width + 'px';
$canvas.style.height = clipperClientRect.height + 'px';
$canvas.width = this._ratio(clipperClientRect.width);
$canvas.height = this._ratio(clipperClientRect.height);
$pCanvas.style.width = clipperWidth + 'px';
$pCanvas.style.height = clipperHeight + 'px';
$pCanvas.width = this._ratio(clipperWidth);
$pCanvas.height = this._ratio(clipperHeight);
//计算两个canvas原点的x y差值
let cClientRect = $canvas.getBoundingClientRect(),
pClientRect = $pCanvas.getBoundingClientRect();
this.originXDiff = pClientRect.left - cClientRect.left;
this.originYDiff = pClientRect.top - cClientRect.top;
this.cWidth = cClientRect.width;
this.cHeight = cClientRect.height;
}
四、載入圖片
載入圖片比較簡單,首先是建立一個Image物件並監聽器onload事件(因為載入的圖片有可能是跨域的,因此要設定其crossOrigin屬性為Anonymous,然後伺服器上要設定Access-Control-Allow-Origin回應頭)。載入的圖片如果寬高大於容器的寬高,要對其進行縮小處理。最後垂直水平居中顯示()(這裡注意的是要保存圖片繪製前的寬高值,因為日後縮放圖片是以該值為基礎再乘以縮放倍率,這裡取imgStartWidth,imgStartHeight)如下
_loadImg() { if (this.imgLoading || this.loadImgQueue.length === 0) { return; } let img = this.loadImgQueue.shift(); if (!img) { return; } let $img = new Image(), onLoad = e => { $img.removeEventListener('load', onLoad, false); this.$img = $img; this.imgLoaded = true; this.imgLoading = false; this._initImg($img.width, $img.height); this.$emit('loadSuccess', e); this.$emit('loadComplete', e); this._loadImg(); }, onError = e => { $img.removeEventListener('error', onError, false); this.$img = $img = null; this.imgLoading = false; this.$emit('loadError', e); this.$emit('loadComplete', e); this._loadImg(); }; this.$emit('beforeLoad'); this.imgLoading = true; this.imgLoaded = false; $img.src = this.img; $img.crossOrigin = 'Anonymous'; //因为canvas toDataUrl不能操作未经允许的跨域图片,这需要服务器设置Access-Control-Allow-Origin头 $img.addEventListener('load', onLoad, false); $img.addEventListener('error', onError, false); } _initImg(w, h) { let eW = null, eH = null, maxW = this.cWidth, maxH = this.cHeight - this.actionBarHeight; //如果图片的宽高都少于容器的宽高,则不做处理 if (w <= maxW && h <= maxH) { eW = w; eH = h; } else if (w > maxW && h <= maxH) { eW = maxW; eH = parseInt(h / w * maxW); } else if (w <= maxW && h > maxH) { eW = parseInt(w / h * maxH); eH = maxH; } else { //判断是横图还是竖图 if (h > w) { eW = parseInt(w / h * maxH); eH = maxH; } else { eW = maxW; eH = parseInt(h / w * maxW); } } if (eW <= maxW && eH <= maxH) { //记录其初始化的宽高,日后的缩放功能以此值为基础 this.imgStartWidth = eW; this.imgStartHeight = eH; this._drawImage((maxW - eW) / 2, (maxH - eH) / 2, eW, eH); } else { this._initImg(eW, eH); } }
五、繪製圖片
下面的_drawImage有四個參數,分別是圖片對應cCanvas的x,y座標以及圖片目前的寬高w ,h。函數首先會清空兩個canvas的內容,方法是重新設定canvas的寬高。然後更新元件實例中對應的值,最後再呼叫兩個canvas的drawImage去繪製圖片。對於pCanvas來說,其繪製的圖片座標值為x,y減去對應的originXDiff與originYDiff(其實相當於切換座標系顯示而已,因此只需要減去兩個座標係原點的x,y差值即可)。看看程式碼
_drawImage(x, y, w, h) { this._clearCanvas(); this.imgX = parseInt(x); this.imgY = parseInt(y); this.imgCurrentWidth = parseInt(w); this.imgCurrentHeight = parseInt(h); //更新canvas this.ctx.drawImage(this.$img, this._ratio(x), this._ratio(y), this._ratio(w), this._ratio(h)); //更新pCanvas,只需要减去两个canvas坐标原点对应的差值即可 this.pCtx.drawImage(this.$img, this._ratio(x - this.originXDiff), this._ratio(y - this.originYDiff), this._ratio(w), this._ratio(h)); }, _clearCanvas() { let $canvas = this.$refs.canvas, $pCanvas = this.$refs.pCanvas; $canvas.width = $canvas.width; $canvas.height = $canvas.height; $pCanvas.width = $pCanvas.width; $pCanvas.height = $pCanvas.height; }
### 六、行動圖片############ 行動圖片實作非常簡單,先給gesture-mask綁定touchstart,touchmove,touchend事件,以下分別介紹這三個事件的內容###### 先定義四個變數scx, scy(手指的起始座標),iX,iY(圖片目前的座標,相對於cCanvas)。 ###### 1、touchstart###### 方法很簡單,就是取得touches[0]的pageX,pageY來更新scx與scy以及更新iX與iY###### 2、touchmove##2、touchmove## #### 取得touches[0]的pageX,宣告變數f1x存放,移動後的x座標等於iX f1x - scx,y座標同理,最後呼叫_drawImage來更新圖片。 ###### 看看程式碼吧###
_initEvent() { let $gesture = this.$refs.gesture, scx = 0, scy = 0; let iX = this.imgX, iY = this.imgY; $gesture.addEventListener('touchstart', e => { if (!this.imgLoaded) { return; } let finger = e.touches[0]; scx = finger.pageX; scy = finger.pageY; iX = this.imgX; iY = this.imgY; }, false); $gesture.addEventListener('touchmove', e => { e.preventDefault(); if (!this.imgLoaded) { return; } let f1x = e.touches[0].pageX, f1y = e.touches[0].pageY; this._drawImage(iX + f1x - scx, iY + f1y - scy, this.imgCurrentWidth, this.imgCurrentHeight); }, false); }
七、缩放图片(这里不作特别说明的坐标都是相对于cCanvas坐标系)
绘制缩放后的图片无非需要4个参数,缩放后图片左上角的坐标以及宽高。求宽高相对好办,宽高等于imgStartWidth * 缩放比率与imgstartHeight * 缩放倍率(imgStartWidth ,imgstartHeight 上文第四节有提到)。接下来就是求缩放倍率的问题了,首先在touchstart事件上求取两手指间的距离d1;然后在touchmove事件上继续求取两手指间的距离d2,当前缩放倍率= 初始缩放倍率 + (d2-d1) / 步长(例如每60px算0.1),touchend事件上让初始缩放倍率=当前缩放倍率。
至于如何求取缩放后图片左上角的坐标值,在草稿纸上画来画去,画了很久......终于有点眉目。首先要找到一个缩放中心(这里做法是取双指的中点坐标,但是这个坐标必须要位于图片上,如果不在图片上,则取图片上离该中点坐标最近的点),然后存在下面这个等式
(缩放中心x坐标 - 缩放后图片左上角x坐标)/ 缩放后图片的宽度 = (缩放中心x坐标 - 缩放前图片左上角x坐标)/ 缩放前图片的宽度;(y坐标同理)
接下来看看下面这个例子(在visio找了很久都没有画坐标系的功能,所以只能手工画了)
绿色框是一张10*5的图片,蓝色框是宽高放大两倍后的图片20*10,根据上面的公式推算的x2 = sx - w2(sx - x1) / w1,y2 = sy - h2(sy - y1) / h1。
坚持...继续看看代码吧
_initEvent() { let $gesture = this.$refs.gesture, cClientRect = this.$refs.canvas.getBoundingClientRect(), scx = 0, //对于单手操作是移动的起点坐标,对于缩放是图片距离两手指的中点最近的图标。 scy = 0, fingers = {}; //记录当前有多少只手指在触控屏幕 //one finger let iX = this.imgX, iY = this.imgY; //two finger let figureDistance = 0, pinchScale = this.imgScale; $gesture.addEventListener('touchstart', e => { if (!this.imgLoaded) { return; } if (e.touches.length === 1) { let finger = e.touches[0]; scx = finger.pageX; scy = finger.pageY; iX = this.imgX; iY = this.imgY; fingers[finger.identifier] = finger; } else if (e.touches.length === 2) { let finger1 = e.touches[0], finger2 = e.touches[1], f1x = finger1.pageX - cClientRect.left, f1y = finger1.pageY - cClientRect.top, f2x = finger2.pageX - cClientRect.left, f2y = finger2.pageY - cClientRect.top; scx = parseInt((f1x + f2x) / 2); scy = parseInt((f1y + f2y) / 2); figureDistance = this._pointDistance(f1x, f1y, f2x, f2y); fingers[finger1.identifier] = finger1; fingers[finger2.identifier] = finger2; //判断变换中点是否在图片中,如果不是则去离图片最近的点 if (scx < this.imgX) { scx = this.imgX; } if (scx > this.imgX + this.imgCurrentWidth) { scx = this.imgX + this.imgCurrentHeight; } if (scy < this.imgY) { scy = this.imgY; } if (scy > this.imgY + this.imgCurrentHeight) { scy = this.imgY + this.imgCurrentHeight; } } }, false); $gesture.addEventListener('touchmove', e => { e.preventDefault(); if (!this.imgLoaded) { return; } this.maskShowTimer && clearTimeout(this.maskShowTimer); this.maskShow = false; if (e.touches.length === 1) { let f1x = e.touches[0].pageX, f1y = e.touches[0].pageY; this._drawImage(iX + f1x - scx, iY + f1y - scy, this.imgCurrentWidth, this.imgCurrentHeight); } else if (e.touches.length === 2) { let finger1 = e.touches[0], finger2 = e.touches[1], f1x = finger1.pageX - cClientRect.left, f1y = finger1.pageY - cClientRect.top, f2x = finger2.pageX - cClientRect.left, f2y = finger2.pageY - cClientRect.top, newFigureDistance = this._pointDistance(f1x, f1y, f2x, f2y), scale = this.imgScale + parseFloat(((newFigureDistance - figureDistance) / this.imgScaleStep).toFixed(1)); fingers[finger1.identifier] = finger1; fingers[finger2.identifier] = finger2; if (scale !== pinchScale) { //目前缩放的最小比例是1,最大是5 if (scale < this.imgMinScale) { scale = this.imgMinScale; } else if (scale > this.imgMaxScale) { scale = this.imgMaxScale; } pinchScale = scale; this._scale(scx, scy, scale); } } }, false); $gesture.addEventListener('touchend', e => { if (!this.imgLoaded) { return; } this.imgScale = pinchScale; //从finger删除已经离开的手指 let touches = Array.prototype.slice.call(e.changedTouches, 0); touches.forEach(item => { delete fingers[item.identifier]; }); //迭代fingers,如果存在finger则更新scx,scy,iX,iY,因为可能缩放后立即单指拖动 let i, fingerArr = []; for(i in fingers) { if (fingers.hasOwnProperty(i)) { fingerArr.push(fingers[i]); } } if (fingerArr.length > 0) { scx = fingerArr[0].pageX; scy = fingerArr[0].pageY; iX = this.imgX; iY = this.imgY; } else { this.maskShowTimer = setTimeout(() => { this.maskShow = true; }, 300); } //做边界值检测 let x = this.imgX, y = this.imgY, pClientRect = this.$refs.pCanvas.getBoundingClientRect(); if (x > pClientRect.left + pClientRect.width) { x = pClientRect.left } else if (x + this.imgCurrentWidth < pClientRect.left) { x = pClientRect.left + pClientRect.width - this.imgCurrentWidth; } if (y > pClientRect.top + pClientRect.height) { y = pClientRect.top; } else if (y + this.imgCurrentHeight < pClientRect.top) { y = pClientRect.top + pClientRect.height - this.imgCurrentHeight; } if (this.imgX !== x || this.imgY !== y) { this._drawImage(x, y, this.imgCurrentWidth, this.imgCurrentHeight); } }); }, _scale(x, y, scale) { let newPicWidth = parseInt(this.imgStartWidth * scale), newPicHeight = parseInt(this.imgStartHeight * scale), newIX = parseInt(x - newPicWidth * (x - this.imgX) / this.imgCurrentWidth), newIY = parseInt(y - newPicHeight * (y - this.imgY) / this.imgCurrentHeight); this._drawImage(newIX, newIY, newPicWidth, newPicHeight); }, _pointDistance(x1, y1, x2, y2) { return parseInt(Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))); }
说明一下fingers是干嘛的,是用来记录当前有多少只手指在屏幕上触摸。可能会出现这种情况,双指缩放后,其中一只手指移出显示屏,而另外一个手指在显示屏上移动。针对这种情况,要在touchend事件上根据e.changedTouches来移除fingers里已经离开显示屏的finger,如果此时fingers里只剩下一个finger,则更新scx,scy,iX,iY为移动图片做初始化准备。
八、裁剪图片
这里很简单,就调用pCanvas的toDataURL方法就可以了
_clipper() { let imgData = null; try { imgData = this.$refs.pCanvas.toDataURL(); } catch (e) { console.error('请在response header加上Access-Control-Allow-Origin,否则canvas无法裁剪未经许可的跨域图片'); } this.$emit('sure', imgData); }
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
在Bootstrap框架里使用treeview如何实现动态加载数据
以上是使用Vue行動端如何實現圖片裁切組件的詳細內容。更多資訊請關注PHP中文網其他相關文章!