最近瀏覽了不少網站的圖片裁切效果,大部分的做法如下圖所示(借用一張腳本之家的圖片),透過改變裁切框的大小來選取合適的位置。
但本文介紹的是另一種裁切方式,裁切框由開發者決定,圖片大小由使用者決定,透過縮放、拖曳圖片來選取合適位置,並且在此過程中始終保持圖片寬高比,如右上圖。
這樣做法主要有以下優點:
裁切框的寬高與跟實際使用的處寬高比一致,防止圖片變形問題
不限制圖片的顯示大小,保證圖片原始比例,透過縮放可得到原始尺寸
對於局部的裁切更友好,例如截取一張高清圖片中很小的一個部位,我們只需將圖片放大並拖動到裁切框內即可,而其他方式需要將裁切框調整的非常小,不利於用戶操作
說完了有點也該說說缺點,缺點就是難度增加了一個數量級。 。 。 。
主體思路是利用兩張圖片,將他們絕對定位,一張放在裁切框內一張放在裁切框外並設定透明效果,裁切框overflow為hidden,隨時保持兩張圖片的絕對同步。
<p class="jimu-crop-image" data-dojo-attach-point="cropSection"> <p class="viewer-box" data-dojo-attach-point="viewerBox"> <p class="viewer-content" data-dojo-attach-point="viewerContent"> <img class="viewer-image hide-image" data-dojo-attach-point="viewerImage" src=""> </p> <img class="base-image hide-image" data-dojo-attach-point="baseImage" data-dojo-attach-event="mousedown:_onViewerMouseDown,mouseup:_onViewerMouseUp"> <p class="controller"> <p class="zoom-out" data-dojo-attach-event="click:_onZoomOutClick">-</p> <p class="slider" data-dojo-attach-point="sliderNode"> <p class="button" data-dojo-attach-point="sliderButton" data-dojo-attach-event="mousedown:_onSliderMouseDown,mouseup:_onSliderMouseUp"></p> <p class="horizontal"></p> </p> <p class="zoom-in" data-dojo-attach-event="click:_onZoomInClick">+</p> </p> </p> </p>
首先在postCreate中綁定document的mousemove跟mousedown事件,在滑鼠離開工作區後仍可以繼續拖曳或縮放。接下來的主要工作在startup跟_init函數。不熟悉dojo的道友只要知道postCreate會在startup之前執行即可。
startup: function() { var timeOut = /data:image\/(.*);base64/.test(this.imageSrc) ? 50 : 500; var tic = lang.hitch(this, function() { var imageStyle = html.getComputedStyle(this.baseImage); var imageWidth = parseFloat(imageStyle.width); console.log('image width', imageWidth); if (isFinite(imageWidth) && imageWidth > 0) { this._init(); } else { setTimeout(tic, timeOut); } }); setTimeout(tic, timeOut); }, _init: function() { debugger; var cropSectionStyle = html.getComputedStyle(this.cropSection); var cropSectionContentBox = html.getContentBox(this.cropSection); var imageStyle = html.getComputedStyle(this.baseImage); var imageWidth = parseFloat(imageStyle.width); var imageHeight = parseFloat(imageStyle.height); var imageRadio = imageWidth / imageHeight; this._maxImageWidth = imageWidth; this._maxImageHeight = imageHeight; if (imageHeight < this.realHeight && imageWidth < this.realWidth) { alert('image is too smaller to display'); return; } //create a box which keep the ratio of width and height to full fill the content of popup this.idealWidth = this.realWidth; this.idealHeight = this.realHeight; this.ratio = this.ratio ? this.ratio : this.realWidth / this.realHeight; if (this.ratio >= 1) { if (this.realWidth <= cropSectionContentBox.w) { this.idealWidth += (cropSectionContentBox.w - this.realWidth) / 2; } else { this.idealWidth = cropSectionContentBox.w; } this.idealHeight = this.idealWidth / this.ratio; } else { if (this.realHeight <= cropSectionContentBox.h) { this.idealHeight += (cropSectionContentBox.h - this.idealHeight) / 2; } else { this.idealHeight = cropSectionContentBox.h; } this.idealWidth = this.idealHeight * this.ratio; } html.setStyle(this.viewerBox, { width: this.idealWidth + 'px', height: this.idealHeight + 'px' }); var paddingTop = Math.abs((parseFloat(cropSectionStyle.height) - this.idealHeight) / 2); html.setStyle(this.cropSection, { 'paddingTop': paddingTop + 'px', 'paddingBottom': paddingTop + 'px' }); // keep original ratio of image if (imageRadio >= 1) { if (this.idealHeight * imageRadio >= this.idealWidth) { html.setStyle(this.viewerImage, 'height', this.idealHeight + 'px'); html.setStyle(this.baseImage, 'height', this.idealHeight + 'px'); } else { var properlyHeight = this._findProperlyValue(0, this.idealWidth, this.idealWidth, function(p) { return p * imageRadio; }); html.setStyle(this.viewerImage, 'height', properlyHeight + 'px'); html.setStyle(this.baseImage, 'height', properlyHeight + 'px'); } } else { if (this.idealWidth / imageRadio >= this.idealHeight) { html.setStyle(this.viewerImage, 'width', this.idealWidth + 'px'); html.setStyle(this.baseImage, 'width', this.idealWidth + 'px'); } else { var properlyWidth = this._findProperlyValue(0, this.idealHeight, this.idealHeight, function(p) { return p / imageRadio; }); html.setStyle(this.viewerImage, 'width', properlyWidth + 'px'); html.setStyle(this.baseImage, 'width', properlyWidth + 'px'); } } query('.hide-image', this.domNode).removeClass('hide-image'); imageStyle = html.getComputedStyle(this.baseImage); imageWidth = parseFloat(imageStyle.width); imageHeight = parseFloat(imageStyle.height); this._minImageWidth = imageWidth; this._minImageHeight = imageHeight; this._currentImageWidth = imageWidth; this._currentImageHeight = imageHeight; this._currentTop = -(imageHeight - this.idealHeight) / 2; this._currentLeft = -(imageWidth - this.idealWidth) / 2; html.setStyle(this.baseImage, { top: this._currentTop + 'px', left: this._currentLeft + 'px' }); html.setStyle(this.viewerImage, { top: this._currentTop + 'px', left: this._currentLeft + 'px' }); //sometimes zoomratio < 1; it's should be not allowed to zoom this._zoomRatio = this._maxImageWidth / this._minImageWidth; if (!this._latestPercentage) { this._latestPercentage = 0; } },
這裡面做了以下幾件事:
等待圖片載入完畢,取得圖片的原始尺寸,後續計算縮放因子時會用到
在確保裁切區域寬高比的情況下,讓裁切區域盡量的填滿工作區。這裡裁切工作最重要的就是防止圖片變形,所以只要確保寬高比一致可以將裁切區域適當放大。
保持圖片原始寬高比的前提下,讓圖片盡量接近裁切框
機上計算完成後設定圖片初始位置,讓裁切框相對圖片居中
平移的過程比較簡單,只需要記錄移動過程中滑鼠的相對位置變化,不斷改變圖片左上角的left跟top即可,在dragstart跟selectstart事件中preventDefault防止元素被選取變藍。
_resetImagePosition: function(clientX, clientY) { var delX = clientX - this._currentX; var delY = clientY - this._currentY; if (this._currentTop + delY >= 0) { html.setStyle(this.baseImage, 'top', 0); html.setStyle(this.viewerImage, 'top', 0); this._currentY = clientY; this._currentTop = 0; } else if (this._currentTop + delY <= this._maxOffsetTop) { html.setStyle(this.baseImage, 'top', this._maxOffsetTop + 'px'); html.setStyle(this.viewerImage, 'top', this._maxOffsetTop + 'px'); this._currentY = clientY; this._currentTop = this._maxOffsetTop; } else { html.setStyle(this.baseImage, 'top', this._currentTop + delY + 'px'); html.setStyle(this.viewerImage, 'top', this._currentTop + delY + 'px'); this._currentY = clientY; this._currentTop += delY; } if (this._currentLeft + delX >= 0) { html.setStyle(this.baseImage, 'left', 0); html.setStyle(this.viewerImage, 'left', 0); this._currentX = clientX; this._currentLeft = 0; } else if (this._currentLeft + delX <= this._maxOffsetLeft) { html.setStyle(this.baseImage, 'left', this._maxOffsetLeft + 'px'); html.setStyle(this.viewerImage, 'left', this._maxOffsetLeft + 'px'); this._currentX = clientX; this._currentLeft = this._maxOffsetLeft; } else { html.setStyle(this.baseImage, 'left', this._currentLeft + delX + 'px'); html.setStyle(this.viewerImage, 'left', this._currentLeft + delX + 'px'); this._currentX = clientX; this._currentLeft += delX; } },
縮放的主要原則就是保持裁切框的中心點在縮放前後的相對位置不變。
為了將縮放後的原始裁切框的中心點移回原位,我們需要計算兩中位數:圖片大小變化量,圖片左上角移動量。
var delImageWidth = this._minImageWidth * (this._zoomRatio - 1) * leftPercentage / 100; var delImageHeight = this._minImageHeight * (this._zoomRatio - 1) * leftPercentage / 100; var imageStyle = html.getComputedStyle(this.baseImage); this._currentLeft = parseFloat(imageStyle.left); this._currentTop = parseFloat(imageStyle.top); var delImageLeft = (Math.abs(this._currentLeft) + this.idealWidth / 2) * ((this._minImageWidth + delImageWidth) / this._currentImageWidth - 1); var delImageTop = (Math.abs(this._currentTop) + this.idealHeight / 2) * ((this._minImageWidth + delImageWidth) / this._currentImageWidth - 1);
其中_zoomRatio = _maxImageWidth / _minImageWidth; _maxImageWidth為圖片原始大小,_minImageWidth是讓圖片接近裁切框的最小寬度。
leftPercentage為滑動按鈕相對滑動條的位移百分比。
_currentLeft、_currentTop是本次縮放前圖片相對裁切框的絕對位置(position:absolute)。
_currentImageWidth、_currentImageHeight是本次縮放前圖片的大小。
剩下要做的是防止裁切框內出現空白現象,假設用戶放大圖片,將圖片拖放到邊界與裁切框邊界重合,這時縮小圖片的話裁切框內便會出現空白。為了防止這種情況我們也需要做相應處理。
當圖片左上邊界與裁切框左上邊界重合時,無論如何縮小,image的left、top始終為零,只改變圖片大小。
當圖片右下邊界與裁切框右下邊界重合時,根據圖片大小與裁切框大小可以計算出合適的left跟top
//prevent image out the crop box if (leftPercentage - _latestPercentage >= 0) { console.log('zoomin'); html.setStyle(this.baseImage, { top: this._currentTop -delImageTop + 'px', left: this._currentLeft -delImageLeft + 'px' }); html.setStyle(this.viewerImage, { top: this._currentTop -delImageTop + 'px', left: this._currentLeft -delImageLeft + 'px' }); } else { console.log('zoomout'); var top = 0; var left = 0; if (this._currentTop - delImageTop >= 0) { top = 0; } else if (this._currentTop - delImageTop + this._minImageHeight + delImageHeight <= this.idealHeight) { top = this.idealHeight - this._minImageHeight - delImageHeight; } else { top = this._currentTop - delImageTop; } console.log(this._currentLeft, delImageLeft); if (this._currentLeft - delImageLeft >= 0) { left = 0; } else if (this._currentLeft - delImageLeft + this._minImageWidth + delImageWidth <= this.idealWidth) { left =this.idealWidth - this._minImageWidth - delImageWidth; } else { left = this._currentLeft - delImageLeft; } html.setStyle(this.baseImage, { top: top + 'px', left: left + 'px' }); html.setStyle(this.viewerImage, { top: top + 'px', left: left + 'px' }); }
以上便是客戶端的實現思路。全部程式碼,瀏覽器支援:現代瀏覽器和ie9+,稍後會將ie8也支援上。
服务器端使用nodejs+express框架,主要代码如下:
/********** body: { imageString: base64 code maxSize: w,h cropOptions: w,h,t,l } ************/ exports.cropImage = function(req, res) { var base64Img = req.body.imageString; if(!/^data:image\/.*;base64,/.test(base64Img)){ res.send({ success: false, message: 'Bad base64 code format' }); } var fileFormat = base64Img.match(/^data:image\/(.*);base64,/)[1]; var base64Data = base64Img.replace(/^data:image\/.*;base64,/, ""); var maxSize = req.body.maxSize; maxSize = maxSize.split(','); var cropOptions = req.body.cropOptions; cropOptions = cropOptions.split(','); try{ var buf = new Buffer(base64Data, 'base64'); var jimp = new Jimp(buf, 'image/' + fileFormat, function() { var maxW = parseInt(maxSize[0], 10); var maxH = parseInt(maxSize[1], 10); var cropW = parseInt(cropOptions[0], 10); var cropH = parseInt(cropOptions[1], 10); var cropT = parseInt(cropOptions[2], 10); var cropL = parseInt(cropOptions[3], 10); this.resize(maxW, maxH) .crop(cropT, cropL, cropW, cropH); }); jimp.getBuffer('image/' + fileFormat, function(b) { var base64String = "data:image/" + fileFormat + ";base64," + b.toString('base64'); res.send({ success: true, source: base64String }); }); }catch(err) { logger.error(err); res.send({ success: false, message: 'unable to complete operations' }); } };
以上是JavaScript圖片裁切的無變形的範例程式碼分享(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!