2023-05-12 开启多语言插件支持……

JavaScript图片裁剪的无变形实现方法

javascript 苏 demo 2233℃ 0评论

最近浏览了不少网站的图片裁切效果,大部分的做法如下图所示(借用一张脚本之家的图片),通过改变裁切框的大小来选取合适的位置。

1

但本文介绍的是另外一种裁切方式,裁切框由开发者决定,图片大小由用户决定,通过缩放、拖动图片来选取合适位置,并且在这一过程中始终保持图片宽高比,如右上图。

这样做法主要有以下优点:

  • 裁切框的宽高与跟实际使用的处宽高比一致,防止出现图片变形问题
  • 不限制图片的显示大小,保证图片原始比例,通过缩放可得到原始尺寸
  • 对于局部的裁切更加友好,比如截取一张高清图片中很小的一个部位,我们只需将图片放大并拖动到裁切框内即可,而其他方式需要将裁切框调整的非常小,不利于用户操作

说完了有点也该说说缺点,缺点就是难度增大了一个数量级。。。。

主体思路是利用两张图片,将他们绝对定位,一张放在裁切框内一张放在裁切框外并设置透明效果,裁切框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>

1

首先在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;
  }
},

缩放的主要原则就是 保持裁剪框的中心点在缩放前后的相对位置不变

 

1

2

为了将缩放后的原裁切框的中心点移回原位,我们需要计算两中值:图片大小变化量,图片左上角移动量。

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'
    });
  }
};

 

 

打赏

转载请注明:苏demo的别样人生 » JavaScript图片裁剪的无变形实现方法

   如果本篇文章对您有帮助,欢迎向博主进行赞助,赞助时请写上您的用户名。
支付宝直接捐助帐号oracle_lee@qq.com 感谢支持!
喜欢 (0)or分享 (0)