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

  

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

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

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

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

  

  主要实现思路:

  1. 将两张图片绝对定位
  2. 一张放在裁切框内一张放在裁切框外并设置透明效果,裁切框overflow为hidden
  3. 鼠标操作过程中时刻保持两张图片的绝对同步。
<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>

 

  

  初始工作:

  首先在postCreate中绑定document的mousemove跟mousedown事件,在鼠标离开工作区后仍可以继续拖动或缩放。startup中利用计时器获取image的原始宽度,据此来判断图片是否加载完毕。不熟悉dojo的道友只要知道postCreate会在startup之前执行即可。

postCreate: function() {this._dragingHandlers = [];this.setImageSrc(this.imageSrc);this.own(on(this.ownerDocument, 'mousemove', lang.hitch(this, '_onMouseMove')));this.own(on(this.ownerDocument, 'mouseup', lang.hitch(this, '_onMouseUp')));this.loadingImg.src = require.toUrl('jimu') + '/images/loading.gif';
},
startup: function() {var timeOut = /data:image\/(.*);base64/.test(this.imageSrc) ? 50 : 500;var tic = lang.hitch(this, function() {var imageStyle = this._getComputedStyle(this.baseImage);var imageWidth = parseFloat(imageStyle.width);console.log('image width', imageWidth);// debugger;if (isFinite(imageWidth) && imageWidth > 0) {this._init();html.setStyle(this.loadingImg, 'display', 'none');} else {setTimeout(tic, timeOut);}});setTimeout(tic, timeOut);
},

 _init函数做了以下几件事:

  1、获取图片的原始尺寸,后续计算缩放因子时会用到

var cropSectionStyle = this._getComputedStyle(this.cropSection);
var cropSectionContentBox = html.getContentBox(this.cropSection);
var imageStyle = this._getComputedStyle(this.baseImage);
var imageWidth = parseFloat(imageStyle.width) || this.baseImage.offsetWidth;
var imageHeight = parseFloat(imageStyle.height) || this.baseImage.offsetHeight;
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;

2、在保证裁切区域宽高比的情况下,让裁切区域尽量的填满工作区。

  这里裁切工作最重要的就是防止图片变形,所以只要保证宽高比一致可以将裁切区域适当放大。cropSectionContentBox是当前工作区的content区域,假设用户设置裁切框的宽度大于高度,对于实际出现在页面中的裁切框宽、高,我们从数值较小的高度来找起,找到合适的高度将宽度按原比例放大即可。

  如果将裁切框高度设置与工作区高度相同,判断裁切区宽度是否超出工作区,没有则裁切区高度等于工作区高度。若超过,则在0~cropSectionContentBox.h中找一个数,保证在该高度下,裁切区宽度尽量接近工作区。这样我们找到一个可以尽量覆盖工作区的裁切框。

  高度大于宽度时的处理类似。

//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;
var _ratio = this.ratio;
if (this.ratio >= 1) {if (cropSectionContentBox.h * this.ratio <= cropSectionContentBox.w) {this.idealHeight = cropSectionContentBox.h;this.idealWidth = cropSectionContentBox.h * this.ratio;} else {this.idealHeight = this._findProperlyValue(0, cropSectionContentBox.h, cropSectionContentBox.w - 5, function(p) {return p * _ratio;});this.idealWidth = this.idealHeight * this.ratio;}
} else {if (cropSectionContentBox.w / this.ratio <= cropSectionContentBox.h) {this.idealWidth = cropSectionContentBox.w;this.idealHeight = cropSectionContentBox.w / this.ratio;} else {this.idealWidth = this._findProperlyValue(0, cropSectionContentBox.w, cropSectionContentBox.h - 5, function(p) {return p / _ratio;});this.idealHeight = this.idealWidth / this.ratio;}
}

 3、保持图片原始宽高比的前提下,让图片尽量接近裁切框

  该过程与上文将裁切框尽量填充工作区的处理类似,区别在上上文是填充这里是覆盖,所以判断条件要变一下。如果图片宽度大于高度,寻找一个高度使得断按比例缩放后的宽度是否能够覆盖裁切框的宽度,首先考虑裁切框高度,不满足的话从0~idealWidth中找到一个值;这里不用担心图片超出工作区,只需要找到一个合适高度即可。

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

3、以上计算完成后设置图片初始位置,让裁切框相对图片居中

imageStyle = this._getComputedStyle(this.baseImage);
imageWidth = parseFloat(imageStyle.width) || this.baseImage.offsetWidth;
imageHeight = parseFloat(imageStyle.height) || this.baseImage.offsetHeight;
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'
});

4、如果原始裁切区域很小,经过上述步骤后工作区内的裁切框会比原始框大,这时上传一张小图片,在工作区内这张图片有可能被拉伸,这种情况下隐藏缩放功能

//sometimes zoomratio < 1; it's should be not allowed to zoom
this._zoomRatio = this._maxImageWidth / this._minImageWidth;
if (this._zoomRatio < 1) {html.setStyle(this.zoomController, 'display', 'none');
}
if (!this._latestPercentage) {this._latestPercentage = 0;
}

第一个重要过程结束了,这是整个过程的完整代码:

_init: function() {var cropSectionStyle = this._getComputedStyle(this.cropSection);var cropSectionContentBox = html.getContentBox(this.cropSection);var imageStyle = this._getComputedStyle(this.baseImage);var imageWidth = parseFloat(imageStyle.width) || this.baseImage.offsetWidth;var imageHeight = parseFloat(imageStyle.height) || this.baseImage.offsetHeight;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 popupthis.idealWidth = this.realWidth;this.idealHeight = this.realHeight;this.ratio = this.ratio ? this.ratio : this.realWidth / this.realHeight;var _ratio = this.ratio;if (this.ratio >= 1) {if (cropSectionContentBox.h * this.ratio <= cropSectionContentBox.w) {this.idealHeight = cropSectionContentBox.h;this.idealWidth = cropSectionContentBox.h * this.ratio;} else {this.idealHeight = this._findProperlyValue(0, cropSectionContentBox.h, cropSectionContentBox.w - 5, function(p) {return p * _ratio;});this.idealWidth = this.idealHeight * this.ratio;}} else {if (cropSectionContentBox.w / this.ratio <= cropSectionContentBox.h) {this.idealWidth = cropSectionContentBox.w;this.idealHeight = cropSectionContentBox.w / this.ratio;} else {this.idealWidth = this._findProperlyValue(0, cropSectionContentBox.w, cropSectionContentBox.h - 5, function(p) {return p / _ratio;});this.idealHeight = this.idealWidth / 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 imageif (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-status', this.domNode).removeClass('hide-status');imageStyle = this._getComputedStyle(this.baseImage);imageWidth = parseFloat(imageStyle.width) || this.baseImage.offsetWidth;imageHeight = parseFloat(imageStyle.height) || this.baseImage.offsetHeight;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 zoomthis._zoomRatio = this._maxImageWidth / this._minImageWidth;if (this._zoomRatio < 1) {html.setStyle(this.zoomController, 'display', 'none');}if (!this._latestPercentage) {this._latestPercentage = 0;}},_findProperlyValue: function(start, end, value, formatter, tolerance) {tolerance = isFinite(tolerance) ? parseFloat(tolerance) : 1;value = value - tolerance < 0 || value + tolerance < 0 ? tolerance : value;var middle = (start + end) / 2;var formatterValue = formatter(middle);if (formatterValue <= value + tolerance && formatterValue >= value - tolerance) {return middle;} else if (formatterValue > value) {return this._findProperlyValue(start, middle, value, formatter);} else if (formatterValue < value) {return this._findProperlyValue(middle, end, value, formatter);}},

  平移与缩放:

  平移的过程比较简单,只需要记录移动过程中鼠标的相对位置变化,不断改变图片左上角的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 boxif (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'});}

 以上便是客户端的实现思路。全部代码也已放在github中,浏览器支持:现代浏览器和ie8+

  

  服务器端使用nodejs+express框架,主要代码如下:

/**********
body: {imageString: base64 codemaxSize: w,hcropOptions: 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图片裁切相关推荐

  1. 【cropper】介绍:JavaScript图片裁切

    前言 略 源码 GITHUB: https://github.com/fengyuanchen/cropperjs GITEE镜像: https://gitee.com/mirrors/cropper ...

  2. 转javascript图片预加载技术

    今天看一篇文章,再谈javascript图片预加载技术(http://www.planeart.cn/?p=1121) http://www.qiqiboy.com/2011/05/20/javasc ...

  3. 图片裁切,上传,自动匹配颜色。

    图片裁切,上传,自动匹配颜色. photoclip插件学习. https://github.com/baijunjie/PhotoClip.js是官方文档. 使用方法很简单.不过我在使用的过程中遇到几 ...

  4. php jcrop,PHP结合JQueryJcrop实现图片裁切实例详解

    我们经常可以看到一些网站上有图片剪切的功能,或许你会觉得这一功能炫目华丽,神秘莫测!但是今天介绍的一款专用于图片裁切的插件jquery.Jcrop.min.js就将揭开图片剪切的神秘面纱.使用这个插件 ...

  5. 原型图Mockplus:怎样使用图片裁切功能

    2019独角兽企业重金招聘Python工程师标准>>> 图片裁切功能就是裁掉图片中多余的或不需要的部分. https://www.mockplus.cn/skill/single/s ...

  6. JavaScript 图片滑动切换效果

    序一(08/07/06) 看到alibaba的一个图片切换效果,感觉不错,想拿来用用.但代码一大堆的,看着昏,还是自己来吧. 由于有了做图片滑动展示效果的经验,做这个就容易得多了. 序二(09/03/ ...

  7. Vue项目中使用图片裁切器 cropperjs (头像裁切)

    cropperjs官方文档 cropperjs结合element-ui 组件后的显示效果: 1. npm 安装 cropperjs cmd命令行输入:npm install cropperjs --s ...

  8. javascript图片轮换

    先完成结构层与表现层部分,做一个纯CSS相册,好让JS不能动弹时,相册还能运作.过程见<纯CSS相册>,只不过是在它的基础再做了一些优化,更符合人的思路走向,好让下面JS顺产而已. < ...

  9. php图片滑动的属性,JavaScript_javascript图片滑动效果实现,本文为大家分享了javascript图片 - phpStudy...

    javascript图片滑动效果实现 本文为大家分享了javascript图片滑动效果实现方法,具体内容如下,先看一下效果图: 鼠标滑过那张图,显示完整的哪张图,移除则复位: 简单的CSS加JS操作D ...

  10. JavaScript 图片切割效果(带拖放、缩放效果)

    JavaScript 图片切割效果(带拖放.缩放效果) 转载于:https://www.cnblogs.com/xiaoluozi513/archive/2008/11/14/1333882.html

最新文章

  1. 2020年SWPUACM团队新生第四次周赛(题解)
  2. Spring Cloud分布式微服务云架构—源码结构图
  3. 英特尔AI芯片首次商用交货!推理性能3.7倍于英伟达T4,年贡献245亿涨250%
  4. Python入门100题 | 第032题
  5. 新年新征程——写在“微软中国研发集团”更名之际
  6. JS域:加载(它的页面的)域 -(所在页面的域)--------- 资源域
  7. iOS万能跳转界面的方法
  8. mysql通过函数完成10的阶乘_请使用函数的递归调用编写求阶乘的函数,并计算1!+2!+3!+4!+5!...
  9. python填空题及答案知乎_GitHub - HSINAN/zhihu-python: 获取知乎内容信息,包括问题,答案,用户,收藏夹信息...
  10. 神奇的 SQL 之 HAVING → 容易被轻视的主角
  11. C - 师--链表的结点插入
  12. 修改tomcat控制台title的方法
  13. Vue 混合、自定义指令、插件
  14. 物联网卡对企业设备的重要性
  15. angularjscheckbox全选_Vue Checkbox全选和选中的方法
  16. AUTOCAD——文本标注
  17. LTE下行物理层传输机制(6)-下行资源分配方式(Resource Allocation Type)
  18. matlab进行差分检验,热传导方程几种差分格式的MATLAB数值解法比较
  19. Z-TEK USB转422接口问题 RS422接口常见使用注意事项
  20. Lua实现简易飞机大战

热门文章

  1. GoLang之浅析unsafe.Pointer与uintptr
  2. 长整数转化成16进制字符串
  3. JavaScript ES6介绍
  4. 六种人类天性基本倾向
  5. 李智慧 - 架构师训练营总览
  6. Google IO 2018开发者大会:Android P
  7. rust发射台主楼_各专业分类词库(完全).doc
  8. 计算机加号公式,怎样在excel表格中显示加号,而不被当成是公式来计算
  9. 江南大学物联网工程学院数据库课程实验一作业1实验报告
  10. linux解压img镜像文件,Linux系统上解压或挂载img文件的方法