JavaScript裁剪圖片不變形的實現方法

phpw34 9年前發布 | 24K 次閱讀 裁剪圖片 圖形/圖像處理

最近瀏覽了不少網站的圖片裁切效果,大部分的做法如下圖所示(借用一張腳本之家的圖片),通過改變裁切框的大小來選取合適的位置。

JavaScript裁剪圖片不變形的實現方法

JavaScript裁剪圖片不變形的實現方法

但本文介紹的是另外一種裁切方式,裁切框由開發者決定,圖片大小由用戶決定,通過縮放、拖動圖片來選取合適位置,并且在這一過程中始終保持圖片寬高比,如右上圖。

這樣做法主要有以下優點:

  1. 裁切框的寬高與跟實際使用的處寬高比一致,防止出現圖片變形問題
  2. 不限制圖片的顯示大小,保證圖片原始比例,通過縮放可得到原始尺寸
  3. 對于局部的裁切更加友好,比如截取一張高清圖片中很小的一個部位,我們只需將圖片放大并拖動到裁切框內即可,而其他方式需要將裁切框調整的非常小,不利于用戶操作
  4. </ol>

    說完了有點也該說說缺點,缺點就是難度增大了一個數量級。。。。

    主體思路是利用兩張圖片,將他們絕對定位,一張放在裁切框內一張放在裁切框外并設置透明效果,裁切框overflow為hidden,時刻保持兩張圖片的絕對同步。

    <div class="jimu-crop-image" data-dojo-attach-point="cropSection">
        <div class="viewer-box" data-dojo-attach-point="viewerBox">
            <div class="viewer-content" data-dojo-attach-point="viewerContent">
                <img class="viewer-image hide-image" data-dojo-attach-point="viewerImage" src="">
            </div>
            <img class="base-image hide-image" data-dojo-attach-point="baseImage" data-dojo-attach-event="mousedown:_onViewerMouseDown,mouseup:_onViewerMouseUp">

        <div class="controller">
            <div class="zoom-out" data-dojo-attach-event="click:_onZoomOutClick">-</div>
            <div class="slider" data-dojo-attach-point="sliderNode">
                <div class="button" data-dojo-attach-point="sliderButton" data-dojo-attach-event="mousedown:_onSliderMouseDown,mouseup:_onSliderMouseUp"></div>
                <div class="horizontal"></div>
            </div>
            <div class="zoom-in" data-dojo-attach-event="click:_onZoomInClick">+</div>
        </div>
    </div>
    

    </div></pre>

    JavaScript裁剪圖片不變形的實現方法

    首先在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;
                }
            },</pre> <p>這里面做了以下幾件事: </p>
    

    1. 等待圖片加載完畢,獲取圖片的原始尺寸,后續計算縮放因子時會用到
    2. 在保證裁切區域寬高比的情況下,讓裁切區域盡量的填滿工作區。這里裁切工作最重要的就是防止圖片變形,所以只要保證寬高比一致可以將裁切區域適當放大。
    3. 保持圖片原始寬高比的前提下,讓圖片盡量接近裁切框
    4. 機上計算完成后設置圖片初始位置,讓裁切框相對圖片居中
    5. </ol>

      平移的過程比較簡單,只需要記錄移動過程中鼠標的相對位置變化,不斷改變圖片左上角的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;
                  }
              },</pre> <p>縮放的主要原則就是<strong>保持裁剪框的中心點在縮放前后的相對位置不變</strong>。 </p>
      

      JavaScript裁剪圖片不變形的實現方法

      JavaScript裁剪圖片不變形的實現方法

      為了將縮放后的原裁切框的中心點移回原位,我們需要計算兩中值:圖片大小變化量,圖片左上角移動量。

      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);</pre></div>

      其中_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'
                      });
                  }</pre> <p>以上便是客戶端的實現思路。<a href="/misc/goto?guid=4959630893966408891" target="_blank">全部代碼</a>,瀏覽器支持:現代瀏覽器和ie9+,稍后會將ie8也支持上。 </p>
      

      服務器端使用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' }); } };</pre>來源:我的小樹林

       本文由用戶 phpw34 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
       轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
       本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!