作者:邹峰立,微博:zrunker,邮箱:zrunker@yahoo.com,微信公众号:书客创作,个人平台:www.ibooker.cc。
Web端图片裁剪的应用其实是非常常见的,例如上传图片或者上传封面的时候,往往都会要求先裁剪,后上传。那么究竟该如何去实现图片裁剪功能呢?
分析
首先先看一下最终效果图:
在图中,实现了以下几个功能:
- 能够切换待裁剪图片。
- 能够实现图片裁剪相关功能,如拖动裁剪框,改变裁剪框的大小等。
- 能够时时预览裁剪效果。
- 点击保存按钮,能够获取图片裁剪相关信息。
分析1:如何实现切换裁剪图片?
待裁剪图片是选择的本地图片,对于本地文件的选择可以通过input type=file标签。
<input id="imgFile" type="file" name="imgFile">
图片选择之后将选择的图片同步到待裁剪图片的显示位置(即裁剪框所在位置),从而达到切换裁剪图片。
分析2:如何实现裁剪布局?
仔细观察裁剪效果图,可以发现待裁剪图片,被选中的那一部分是明亮的而其他属于灰色。那么如何去实现这样的效果呢?
可以将这一部分分三层,每一层都堆叠在前一层的上面。第一层是灰色层,这里用图片(即真正待裁剪图片)显示,设置其透明度,使之不为全透明。第二层也是图片(即真正待裁剪图片),背景全透明,但是只显示图片跟裁剪框一样的大小部分,这里需要用到clip。第三层是裁剪框,根据裁剪框的大小修改第二层图片要显示大小,从而达到裁剪视觉效果。
分析3:如何实现预览?
预览效果的实现,其实跟裁剪效果的实现一样,但是只需要一层即可,这一层只放置图片(即真正待裁剪图片)。在裁剪过程中,根据裁剪框的大小,位置来设置预览图片的要显示的大小,位置。
分析4:点击保存按钮,如何获取图片裁剪的相关信息?
其实图片裁剪的相关信息即裁剪框的信息。这里只需要知道,裁剪框的大小(宽高)和裁剪框相对于待裁剪图片的位置(如果把待裁剪图片的左上角设为原点,那么裁剪框左上角相对于原点的x和y坐标)。
真正进行裁剪的是后台进行裁剪,而前端只是模拟一个效果。后台获取这些参数之后,可以对图片进行相应的裁剪处理。
实现
一、布局实现
在body体中,需要实现两部分,一部分是用来实现图片裁剪,另一部分是用来实现图片上传的form表单。
<body>
<%-- 裁剪图片布局 --%>
<div id="crop_image">
<div id="crop_image_top">
<h4>编辑图片</h4>
<!-- <a>X</a> -->
</div>
<div id="crop_image_content">
<%-- 裁剪部分 --%>
<div id="cropBox">
<img id="cropimg1" alt="" src="images/test.jpg">
<img id="cropimg2" alt="" src="images/test.jpg">
<%-- 裁剪框 --%>
<div id="mainBox">
<div id="left-up" class="minBox left-up"></div>
<div id="up" class="minBox up"></div>
<div id="right-up" class="minBox right-up"></div>
<div id="left" class="minBox left"></div>
<div id="right" class="minBox right"></div>
<div id="left-down" class="minBox left-down"></div>
<div id="down" class="minBox down"></div>
<div id="right-down" class="minBox right-down"></div>
</div>
</div>
<%-- 预览部分 --%>
<div id="preview">
<img id="cropimg3" alt="" src="images/test.jpg">
</div>
</div>
<div id="crop_image_bottom">
<input class="btn" type="button" value="取消">
<input id="submitBtn" class="btn" type="button" value="确定" onclick="saveCropImage()">
</div>
</div>
<!-- 上传图片表单 -->
<form id="uploadForm" action="">
<div id="uploadImage">
<img id="cropimg4" alt="" src="images/test.jpg">
<a href="javascript:;" class="addImage">
<span>上传图片</span>
<input id="imgFile" type="file" name="imgFile">
</a>
</div>
</form>
</body>
注:裁剪框实现,是通过一个大的div(mainBox)和8个小的div(上,下,左,右,左上,左下,右上,右下)组成。通过设置大的div来整体控制裁剪框的大小。通过操作大的div内部8个小div来实现裁剪效果。
设置样式,这里为了方便,把样式写在布局文件里面,所以在header里面添加。这里给出的样式代码,只是正对body体里面布局内容的样式。
<style type="text/css">
#crop_image {
float: left;
width: 55%;
background-color: #333;
user-select: none;
margin: 3% 20%;
border: 1px solid #DEDEDE;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 1px 1px 5px 2px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 1px 1px 5px 2px rgba(0, 0, 0, 0.2);
box-shadow: 1px 1px 5px 2px rgba(0, 0, 0, 0.2);
}
#crop_image_top,#crop_image_bottom {
float: left;
width: 96%;
padding: 2%;
background-color: white;
}
/* 底部 */
#crop_image_bottom {
text-align: right;
}
#crop_image_bottom input {
height: 30px;
padding: 5px 15px;
border-radius: 3px;
border: none;
margin-left: 20px;
outline: none;
}
#submitBtn {
background-color: #40aff2;
color: white;
cursor: pointer;
}
#submitBtn:HOVER {
background-color: #409ff2;
}
/* 顶部 */
#crop_image_top h4 {
margin: 0;
padding: 0;
font-weight: normal;
}
/* 裁剪部分 */
#crop_image_content {
float: left;
position: relative;
text-align: center;
width: 92%;
margin: 4%;
}
#cropBox {
position: relative;
}
#crop_image_content #cropimg1 {
opacity: 0.5;
position: absolute;
top: 0;
left: 0;
}
#crop_image_content #cropimg2 {
position: absolute;
top: 0;
left: 0;
clip: rect(0, 150px, 150px, 0);
}
#crop_image_content #mainBox {
border: 1px solid white;
position: absolute;
top: 0;
left: 0;
width: 150px;
height: 150px;
cursor: move;
}
.minBox {
position: absolute;
height: 8px;
width: 8px;
background-color: white;
}
.left-up {
top: -4px;
left: -4px;
cursor: nw-resize;
}
.up {
left: 50%;
margin-left: -4px;
top: -4px;
cursor: n-resize;
}
.right-up {
right: -4px;
top: -4px;
cursor: ne-resize;
}
.left {
top: 50%;
margin-top: -4px;
left: -4px;
cursor: w-resize;
}
.right {
top: 50%;
margin-top: -4px;
right: -4px;
cursor: w-resize;
}
.left-down {
bottom: -4px;
left: -4px;
cursor: sw-resize;
}
.down {
bottom: -4px;
left: 50%;
margin-left: -4px;
cursor: s-resize;
}
.right-down {
bottom: -4px;
right: -4px;
cursor: se-resize;
}
/* 预览框 */
#preview {
position: absolute;
top: 0;
}
#preview #cropimg3 {
position: absolute;
}
/* 上传图片区域 */
#uploadForm {
float: left;
margin: -2% 20% 5% 20%;
}
#uploadImage {
position: relative;
width: 372px;
height: 202px;
background-color: #40aff2;
text-align: center;
}
#uploadImage #cropimg4 {
position: absolute;
height: 100%;
width: 100%;
padding: 0;
margin: 0;
top: 0;
left: 0;
}
#uploadImage .addImage {
display: inline-block;
position: relative;
min-width: 80px;
height: 40px;
overflow: hidden;
padding: 0 30px;
margin: 81px auto;
border: none;
background-color: #F3F3F3;
color: #555;
font: 14px/40px 'MicroSoft Yahei', 'Simhei';
cursor: pointer;
text-align: center;
text-decoration: none;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#uploadImage .addImage:HOVER {
background-color: #DEDEDE;
}
#uploadImage #imgFile
{
display: block;
position: absolute;
top: 0;
left: 0;
width: 140px;
height: 40px;
cursor: pointer;
cursor: hand;
border: none;
font-size: 0;
padding: 0;
/* older safari/Chrome browsers */
-webkit-opacity: 0;
/* Netscape and Older than Firefox 0.9 */
-moz-opacity: 0;
/* Safari 1.x (pre WebKit!) 老式khtml内核的Safari浏览器*/
-khtml-opacity: 0;
/* IE9 + etc...modern browsers */
opacity: 0;
/* IE 4-9 */
filter:alpha(opacity=0);
/*This works in IE 8 & 9 too*/
-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
/*IE4-IE9*/
filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
}
#uploadImage #imgFile::-webkit-file-upload-button {
display: block;
position: absolute;
top: 0;
left: 0;
width: 140px;
height: 40px;
cursor: pointer;
cursor: hand;
border: none;
font-size: 0;
padding: 0;
/* older safari/Chrome browsers */
-webkit-opacity: 0;
/* Netscape and Older than Firefox 0.9 */
-moz-opacity: 0;
/* Safari 1.x (pre WebKit!) 老式khtml内核的Safari浏览器*/
-khtml-opacity: 0;
/* IE9 + etc...modern browsers */
opacity: 0;
/* IE 4-9 */
filter:alpha(opacity=0);
/*This works in IE 8 & 9 too*/
-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
/*IE4-IE9*/
filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
}
</style>
注:1、opacity属性是用来设置透明度,取值范围0~1。2、clip属性是用来设置控件要显示的位置、大小、样式等,如clip: rect(0, 150px, 150px, 0);表示控件显示矩形,矩形上右下左相对于主控件坐标分别是0,150,150,0。3、cursor表示鼠标样式,如cursor: nw-resize;表示南西可更改大小的鼠标样式。
二、图片裁剪功能实现
1、裁剪框位置变化:当鼠标按下裁剪框的时候,裁剪框就可以根据鼠标移动的位置变化而变化。
当裁剪框移动的时候,裁剪框的大小未发生变化,变化的只是裁剪框的坐标或者说是相对于浏览器左侧和上侧的距离或者说是鼠标在x轴或者y轴上的变化。
所以对于裁剪框变化处理,就相当于重新设置裁剪框到浏览器上侧和左侧的距离。
上侧距离变化值 = y轴坐标 - 裁剪框到浏览器顶部的距离。
左侧距离变化值 = x轴坐标 - 裁剪框到浏览器左边的距离。
例如向上移动:
var mainBoxElem = document.getElementById("mainBox");// 裁剪框
var y = e.clientY;// 鼠标y坐标
var mainY = getPosition(mainBoxElem).top;// 裁剪框相对于屏幕上边的距离
var addHeight = mainY - y;// 增加的高度
mainBoxElem.style.top = mainBoxElem.offsetTop - addHeight + "px";// 裁剪框相对于父控件的距离
问题:裁剪框移动范围设置。
裁剪框只能在自己的父控件内进行位置变化,这就相对于鼠标在x轴的变化,不能小于裁剪框父控件左侧到浏览器左侧距离,不能大于裁剪框父控件右侧到浏览器左侧距离。鼠标在y轴上变化,不能小于裁剪框父控件上侧到浏览器上侧距离,不能大于裁剪框父控件下侧到浏览器上侧距离。
2、裁剪框大小变化:裁剪框可以往任意方向进行拉伸或缩放。
这里只分析裁剪框内部id为right的div点击之后,向左或向右移动的处理,其他div的处理方式与其略同。裁剪框的大小变化跟鼠标的移动有关,所以该处理过程应该放在鼠标移动事件内。
window.onmousemove = function(e) {}
A. 鼠标向右移动,实际上就相当于将裁剪框的宽度增加。
这个难点是在计算裁剪框增加的宽度。当鼠标移动的时候,可以知道鼠标在x轴位置,同时可以通过元素.offsetLeft可以知道裁剪框相对于浏览器左侧的距离,然后用x轴位置 - 裁剪框相对于浏览器其左侧距离 - 裁剪框最初宽度 = 裁剪框新增宽度。
var mainBoxElem = document.getElementById("mainBox");// 裁剪框
var x = e.clientX;// 鼠标x坐标
var widthBefore = mainBoxElem.offsetWidth - 2;// 裁剪框变化前的宽度,2为裁剪框边框
var addWidth = x - mainBoxElem.offsetLeft- widthBefore;// 鼠标移动后,裁剪框增加的宽度
var width = widthBefore + addWidth;
mainBoxElem.style.width = width + "px";// 裁剪框变化后,设置宽度
B. 鼠标向左移动,实际上就相当于将裁剪框的宽度减少,或者说裁剪框增加的宽度为负值。
裁剪框增加宽度= 鼠标x轴坐标 - (裁剪框相对于浏览器左侧距离 + 裁剪框原宽度)。
var x = e.clientX;// 鼠标x坐标
var widthBefore = mainBoxElem.offsetWidth -2;// 裁剪框变化前的宽度
var addWidth = mainBoxElem.offsetLeft - x;// 鼠标移动后,裁剪框增加的宽度
var width = widthBefore + addWidth;
mainBoxElem.style.width = width + "px";// 裁剪框变化后,设置宽度
问题1:无论裁剪框增加多少,最终裁剪框都是在其父控件范围内进行变化。
解决:在进行裁剪框大小变化的时候,判断设置的宽度或者高度不能超过父控件的宽度或者高度。
问题2:无论裁剪框减少多少,都不可能将裁剪框的高或者宽减少到小于0的值。
解决:在设置裁剪框大小变化的时候,判断设置的宽度或者高度不能等于0。
综合以上分析,对该剪裁相关js代码进行整理和封装为clip-image.js。
// 实现图片裁剪功能
window.onload = function() {
document.onselectstart=new Function('event.returnValue=false;');
// 裁剪框拖动
$("#mainBox").draggable({containment: 'parent', drag: setChoice});
// 获取裁剪框移动范围
var cropimgElem = document.getElementById("cropimg1");
var cropimgElemWidth = cropimgElem.clientWidth;
var cropimgElemHeight = cropimgElem.clientHeight;
var cropimgElemPosition = getPosition(cropimgElem);
var minX = cropimgElemPosition.left;// 待裁剪的图片最小x坐标
var maxX = cropimgElemPosition.left + cropimgElemWidth;// 待裁剪的图片最大x坐标
var minY = cropimgElemPosition.top;// 待裁剪的图片最小y坐标
var maxY = cropimgElemPosition.top + cropimgElemHeight;// 待裁剪的图片最大y坐标
// 动态设置父控件的宽度和高度
var cropBox = document.getElementById("cropBox");
cropBox.style.width = cropimgElemWidth + "px";
cropBox.style.height = cropimgElemHeight + "px";
// 预览框设置
var preview = document.getElementById("preview");
preview.style.width = cropimgElemWidth + "px";
preview.style.height = cropimgElemHeight + "px";
preview.style.left = cropimgElemWidth + 10 + "px";
// 裁剪框内相关元素
var mainBoxElem = document.getElementById("mainBox");// 裁剪框
var rightElem = document.getElementById("right");
var upElem = document.getElementById("up");
var leftElem = document.getElementById("left");
var downElem = document.getElementById("down");
var leftUpElem = document.getElementById("left-up");
var rightUpElem = document.getElementById("right-up");
var leftDownElem = document.getElementById("left-down");
var rightDownElem = document.getElementById("right-down");
var ifKeyDown = false;// 鼠标按下事件
var contact = "";// 表示被按下触点
// 鼠标按下事件
rightElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "right";
};
upElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "up";
};
leftElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "left";
};
downElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "down";
};
leftUpElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "left-up";
};
rightUpElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "right-up";
};
leftDownElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "left-down";
};
rightDownElem.onmousedown = function(e) {
e.stopPropagation();
ifKeyDown = true;
contact = "right-down";
};
// 鼠标松开事件
window.onmouseup = function(e) {
ifKeyDown = false;
contact = "";
};
// 鼠标移动事件
window.onmousemove = function(e) {
e.stopPropagation();
if (ifKeyDown == true) {
switch (contact) {
case "right":
rightMove(e);
break;
case "up":
upMove(e);
break;
case "left":
leftMove(e);
break;
case "down":
downMove(e);
break;
case "left-up":
leftMove(e);
upMove(e);
break;
case "right-up":
rightMove(e);
upMove(e);
break;
case "left-down":
leftMove(e);
downMove(e);
break;
case "right-down":
rightMove(e);
downMove(e);
break;
default:
break;
}
setChoice();
}
};
setChoice();// 初始化选择区域可见
// 右边移动
function rightMove(e) {
var x = e.clientX;// 鼠标x坐标
// if(x > getPosition(cropimgElem).left + cropimgElem.offsetWidth){
// x = getPosition(cropimgElem).left + cropimgElem.offsetWidth;
// }
if (x > maxX || x < minX) {
return;
}
var widthBefore = mainBoxElem.offsetWidth -2;// 裁剪框变化前的宽度
var addWidth = x - getPosition(mainBoxElem).left - widthBefore;// 鼠标移动后,裁剪框增加的宽度
var width = widthBefore + addWidth;
if (width < 1) {
return;
}
mainBoxElem.style.width = width + "px";// 裁剪框变化后,设置宽度
};
// 上边移动
function upMove(e) {
var y = e.clientY;// 鼠标y坐标
// if(y < getPosition(cropimgElem).top){
// y = getPosition(cropimgElem).top;
// }
if (y > maxY || y < minY) {
return;
}
var mainY = getPosition(mainBoxElem).top;// 裁剪框相对于屏幕上边的距离
var addHeight = mainY - y;// 增加的高度
var heightBefore = mainBoxElem.offsetHeight - 2;// 裁剪框变化前的高度
var height = heightBefore + addHeight;
if (height < 1) {
return;
}
mainBoxElem.style.height = height + "px";// 裁剪框变化后,设置高度
mainBoxElem.style.top = mainBoxElem.offsetTop - addHeight + "px";// 裁剪框相对于父控件的距离
};
// 左边移动
function leftMove(e) {
var x = e.clientX;// 鼠标x坐标
// if(x < getPosition(cropimgElem).left){
// x = getPosition(cropimgElem).left;
// }
if (x > maxX || x < minX) {
return;
}
var mainX = getPosition(mainBoxElem).left;
var widthBefore = mainBoxElem.offsetWidth -2;// 裁剪框变化前的宽度
var addWidth = mainX - x;// 鼠标移动后,裁剪框增加的宽度
var width = widthBefore + addWidth;
if (width < 1) {
return;
}
mainBoxElem.style.width = width + "px";// 裁剪框变化后,设置宽度
mainBoxElem.style.left = mainBoxElem.offsetLeft - addWidth + "px";// 裁剪框变化后,设置到父元素左边的距离
};
// 下边移动
function downMove(e) {
var y = e.clientY;// 鼠标y坐标
// if(y > getPosition(cropimgElem).top + cropimgElem.offsetHeight){
// y = getPosition(cropimgElem).top + cropimgElem.offsetHeight;
// }
if (y > maxY || y < minY) {
return;
}
var heightBefore = mainBoxElem.offsetHeight - 2;// 裁剪框变化前的高度
var mainY = getPosition(mainBoxElem).top;// 裁剪框相对于屏幕上边的距离
var addHeight = y - heightBefore - mainY;// 增加的高度
var height = heightBefore + addHeight;
if (height < 1) {
return;
}
mainBoxElem.style.height = height + "px";// 裁剪框变化后,设置高度
};
// 设置裁剪框的位置
function setChoice() {
var top = mainBoxElem.offsetTop;
var right = mainBoxElem.offsetLeft + mainBoxElem.offsetWidth;
var bottom = mainBoxElem.offsetTop + mainBoxElem.offsetHeight;
var left = mainBoxElem.offsetLeft;
var cropimg2 = document.getElementById("cropimg2");
cropimg2.style.clip = "rect(" + top + "px, " + right + "px, " + bottom + "px, " + left + "px)";
setPreview();
};
// 预览函数
function setPreview() {
var top = mainBoxElem.offsetTop;
var right = mainBoxElem.offsetLeft + mainBoxElem.offsetWidth;
var bottom = mainBoxElem.offsetTop + mainBoxElem.offsetHeight;
var left = mainBoxElem.offsetLeft;
var previewImg = document.getElementById("cropimg3");
previewImg.style.top = -top + "px";
previewImg.style.left = -left + "px";
previewImg.style.clip = "rect(" + top + "px, " + right + "px, " + bottom + "px, " + left + "px)";
}
};
// 获取元素相对于屏幕左边的距离 offsetLeft,offsetTop
function getPosition(node) {
var left = node.offsetLeft;
var top = node.offsetTop;
var parent = node.offsetParent;
while (parent != null) {
left += parent.offsetLeft;
top += parent.offsetTop;
parent = parent.offsetParent;
}
return {"left":left,"top":top};
};
注:
1、为了防止父控件对子控件操作进行影响,所以增加e.stopPropagation();。
2、实现裁剪框拖动,还需要借助于jquery-ui-1.10.4.custom.min.js插件,同时添加以下代码:
// 裁剪框拖动
$("#mainBox").draggable({containment: 'parent', drag: setChoice});
3、为了防止裁剪框拖动时,父控件蓝屏闪烁,这里要添加以下代码,对父控件进行屏蔽:
document.onselectstart=new Function('event.returnValue=false;');
最后引入js到布局文件中即可:
<script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="js/jquery-ui-1.10.4.custom.min.js"></script>
<script type="text/javascript" src="js/clip-image.js"></script>
其他功能实现
1、上传图片并预览
<%--上传图片并预览--%>
$('#imgFile').change(function(event) {
// 根据这个 <input> 获取文件的 HTML5 js对象
var files = event.target.files, file;
if (files && files.length > 0) {
// 获取目前上传的文件
file = files[0];
// 获取window的 URL工具
var URL = window.URL || window.webkitURL;
// 通过 file生成目标 url
var imgURL = URL.createObjectURL(file);
// 用这个URL产生一个 <img> 将其显示出来
$('#cropimg1').attr('src', imgURL);
$('#cropimg2').attr('src', imgURL);
$('#cropimg3').attr('src', imgURL);
$('#cropimg4').attr('src', imgURL);
}
});
2、获取裁剪相关信息,并上传
<%--保存裁剪之后的图片--%>
function saveCropImage() {
// 需要获取裁剪之后,裁剪框的宽度和高度,以及裁剪框相对于裁剪图片的坐标位置
var mainBox = document.getElementById("mainBox");
// 裁剪框的宽度
var width = mainBox.clientWidth;
// 裁剪框的高度
var height = mainBox.clientHeight;
// 相对于裁剪图片x左边
var x = mainBox.offsetLeft;
// 相对于裁剪图片y左边
var y = mainBox.offsetTop;
alert(width);
alert(height);
alert(x);
alert(y);
// AjaxFileUpload提交 或者 jQuery提交表单
};