关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » JavaScript »JavaScript图片裁剪的无变形实现方法

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

    2015-04-05 00:00:00 出处:黯羽轻扬
    分享

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

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

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

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

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

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

    首先在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'
        });
      }
    };
    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    帝国CMS7.5编辑器上传图片取消宽高的三种方法

    帝国cms如何自动生成缩略图的实现方法

    Windows 12即将到来,将彻底改变人机交互

    帝国CMS 7.5忘记登陆账号密码怎么办?可以phpmyadmin中重置管理员密码

    帝国CMS 7.5 后台编辑器换行,修改回车键br换行为p标签

    Windows 11 版本与 Windows 10比较,新功能一览

    Windows 11激活产品密钥收集及专业版激活方法

    IT头条

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    严重缩水!NVIDIA将推中国特供RTX 5090 DD:只剩24GB显存

    00:17

    无线路由大厂 TP-Link突然大裁员:补偿N+3

    02:39

    Meta 千万美金招募AI高级人才

    00:22

    技术热点

    微软已修复windows 7/windows 8.1媒体中心严重漏洞 用户可下载安

    卸载MySQL数据库,用rpm如何实现

    windows 7中使用网上银行或支付宝支付时总是打不开支付页面

    一致性哈希算法原理设计

    MySQL数字类型中的三种常用种类

    如何解决SQL Server中传入select语句in范围参数

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。