本文原文鏈接:http://classlfz.com/2017/07/23/write-an-image-scissors/
作為一個(gè)前端開(kāi)發(fā)人員,這次的這篇博客文章終于“正?!绷?。
這應(yīng)該算是一個(gè)造輪子的實(shí)踐,JS的圖片開(kāi)源裁剪器有很多,像使用JQuery庫(kù)編寫(xiě)的cropper插件很多,在github上邊的star數(shù)量也不少,現(xiàn)流行的前端框架也肯定有對(duì)應(yīng)的圖片裁剪器,都是可以選擇的成熟的技術(shù)方案。但不是所有的情況都能適用,設(shè)計(jì)師的要求,本身項(xiàng)目的條件限制等等原因,有些時(shí)候,還是“自己動(dòng)手,豐衣足食”?。?/p>
因?yàn)橛玫搅撕芏?code>html5的特性,所以,這里編寫(xiě)的圖片裁剪器,只能是適合于支持這些HTML5特性的瀏覽器才能夠正常的使用。
需求描述
點(diǎn)擊按鈕選擇圖片,并進(jìn)行展示;
展示區(qū)有一個(gè)正方形裁剪區(qū),圖片可以放大縮??;
在展示區(qū),點(diǎn)擊鼠標(biāo)后拖拽,可對(duì)圖片的位置進(jìn)行調(diào)試,但展示圖片不能越過(guò)裁剪區(qū);
點(diǎn)擊裁剪按鈕,對(duì)位于裁剪區(qū)的圖片進(jìn)行裁剪,并展示裁剪后的圖片;
HTML & CSS部分
首先,我們要編寫(xiě)一些基礎(chǔ)的HTML代碼:
<div id="scissorsContainer">
<input id="imageInput" type="file" hidden onchange="ImageInputChanged(event)">
<label class="btn" for="imageInput">SELECT IMAGE</label>
<div id="workArea" onmousedown="startDrag(event)">
<div id="overlay">
<div id="overlayInner"></div>
<img id="avatorImg" onload="avatorImgChanged()" alt="">
</div>
</div>
<div id="resizeBox">
<div>
<button class="btn" onclick="resizeDown()">縮小</button>
<button class="btn" onclick="resizeUp()">放大</button>
</div>
</div>
<button class="btn" onclick="crop()">CROP</button>
</div>
<h4>Image Show</h4>
<img id="imageShow" src="" alt="">
添加樣式。我這里是用了flex布局,如果不想用flex布局的話,可以選擇其他的布局方式。
body {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
#scissorsContainer {
display: flex;
justify-content: center;
flex-direction: column;
}
.btn {
padding: 8px;
font-size: 14px;
border: 1px solid #1976d2;
border-radius: 2px;
background: #ffffff;
text-align: center;
cursor: pointer;
}
.btn:hover {
background-color: #1976d2;
color: #ffffff;
}
#workArea {
width: 500px;
height: 500px;
position: relative;
margin: 16px 0;
overflow: hidden;
}
#overlay {
position: absolute;
width: 100%;
height: 100%;
background: #eeeeee;
display: flex;
justify-content: center;
align-items: center;
}
#overlayInner {
width: 300px;
height: 300px;
border: 100px solid gray;
opacity: 0.7;
z-index: 1;
}
#avatorImg {
width: auto;
height: auto;
border: none;
outline: none;
position: absolute;
}
#resizeBox {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
}
#resizeBox > div > button {
margin: 0 8px;
}
#imageShow {
width: 300px;
height: 300px;
}
設(shè)置全局變量
這里,我們?cè)O(shè)置一些JS代碼里邊需要的全局變量,具體的作用,下邊的部分我們會(huì)介紹到的。
window.onload = function() {
// *** 定義全局變量 ***
// 全局需要的元素
window.workArea = document.querySelector('#workArea');
window.avatorImg = document.querySelector('#avatorImg');
window.imageShow = document.querySelector('#imageShow');
// 鼠標(biāo)初始位置坐標(biāo)數(shù)值
window.mouseStartX = 0;
window.mouseStartY = 0;
// 圖片初始化后的尺寸記錄
window.initLength = {
width: 0,
height: 0
};
// 圖片原始尺寸記錄
window.primitiveLength = {
width: 0,
height: 0
};
// 圖片放大縮小的數(shù)值
window.resizeValue = 0;
// 圖片呈現(xiàn)高度&寬度,需要根據(jù)HTML以及CSS部分的overlayInner的高寬度一致
window.showSide = document.querySelector('#overlayInner').clientWidth;
// 裁剪的圖片類(lèi)型
window.croppedImageType = 'image/png';
}
獲取圖片數(shù)據(jù),并展示
我們?cè)谏线叺腍TML部分寫(xiě)了一個(gè)隱藏的input[type="file"],通過(guò)一個(gè)label標(biāo)簽來(lái)觸發(fā)它,同時(shí)監(jiān)聽(tīng)這個(gè)input元素,當(dāng)它的值發(fā)生改變時(shí),可以通過(guò)一個(gè)FileReader的對(duì)象獲取到這個(gè)input的數(shù)據(jù),并將它轉(zhuǎn)換成base64格式的數(shù)據(jù)。
/**
* 圖片選擇元素的值發(fā)生變化后,重置圖片裁剪區(qū)的樣式
* @param {Object} e input數(shù)值變化事件
*/
function ImageInputChanged(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = function(event) {
// 賦值給圖片展示元素
avatorImg.src = event.target.result;
// 重置樣式
avatorImg.style.width = 'auto';
avatorImg.style.height = 'auto';
avatorImg.style.top = 'auto';
avatorImg.style.left = 'auto';
}
reader.readAsDataURL(file);
}
圖片展示區(qū)的數(shù)據(jù)發(fā)生變化
圖片展示區(qū)的數(shù)據(jù)發(fā)生變化,我們?cè)谶@里除了收集圖片的原始信息以外,還對(duì)圖片進(jìn)行了初始化的像素調(diào)整。
function avatorImgChanged() {
if (avatorImg.offsetWidth >= avatorImg.offsetHeight) {
avatorImg.style.top = '100px';
initLength.width = showSide * avatorImg.offsetWidth / avatorImg.offsetHeight
initLength.height = showSide;
} else {
avator.style.left = '100px';
initLength.height = showSide * avatorImg.offsetWidth / avatorImg.offsetWidth;
initLength.width = showSide;
}
// 保存新的圖片原始像素值
primitiveLength = {
width: avatorImg.offsetWidth,
height: avatorImg.offsetHeight
};
// 更新圖片樣式
avatorImg.style.width = initLength.width + 'px';
avatorImg.style.height = initLength.height + 'px';
}
圖片的放大與縮小
在上邊對(duì)應(yīng)的HTML部分,我們添加了圖片放大與縮小的按鈕。
/**
* 圖片放大
*/
function resizeUp() {
if (resizeValue <= 0) return;
resizeValue += 10;
resize();
}
/**
* 圖片縮小
*/
function resizeDown() {
resizeValue -= 10;
resize();
}
/**
* 修改圖片比例大小
*/
function resize() {
avatorImg.style.width = (resizeValue + 100) / 100 * initLength.width + 'px';
avatorImg.style.height = (resizeValue + 100) / 100 * initLength.height + 'px';
}
圖片的拖拽
圖片的拖拽,就是利用鼠標(biāo)的三個(gè)事件來(lái)完成——mousedown,mousemove以及mouseup。我們?cè)?code>mousedown的時(shí)候,記錄鼠標(biāo)的初始位置,添加mousemove以及mouseup事件監(jiān)聽(tīng)函數(shù)。
/**
* 監(jiān)測(cè)鼠標(biāo)點(diǎn)擊,開(kāi)始拖拽
* @param {Object} e 鼠標(biāo)點(diǎn)擊事件
*/
function startDrag(e) {
e.preventDefault();
if (avatorImg.src) {
// 記錄鼠標(biāo)初始位置
window.mouseStartX = e.clientX;
window.mouseStartY = e.clientY;
// 添加鼠標(biāo)移動(dòng)以及鼠標(biāo)點(diǎn)擊松開(kāi)事件監(jiān)聽(tīng)
workArea.addEventListener('mousemove', window.dragging, false);
workArea.addEventListener('mouseup', window.clearDragEvent, false);
}
}
圖片的拖拽,我們這里做了一些限制,限制不讓它拖拽出裁剪區(qū)。這主要是防止我們裁剪出不屬于原圖的空白區(qū)域。
/**
* 處理拖拽
* @param {Object} e 鼠標(biāo)移動(dòng)事件
*/
function dragging(e) {
// *** 圖片不存在 ***
if (!avatorImg.src) return;
// *** 圖片存在 ***
// X軸
let _moveX = avatorImg.offsetLeft + e.clientX - mouseStartX;
// 這里的100是HTML里邊overlayInner的border屬性的寬度
// 下邊的400就是overlayInner元素的邊長(zhǎng)加上border的寬度之和
if (_moveX >= 100) {
avatorImg.style.left = '100px';
mouseStartX = e.clientX;
return;
} else if (_moveX <= 400 - avatorImg.offsetWidth) {
_moveX = 400 - avatorImg.offsetWidth;
}
avatorImg.style.left = _moveX + 'px';
mouseStartX = e.clientX;
// Y軸
let _moveY = avatorImg.offsetTop + e.clientY - mouseStartY;
if (_moveY >= 100) {
avatorImg.style.top = '100px';
mouseStartY = e.clientY;
return;
} else if (_moveY <= 400 - avatorImg.offsetHeight) {
_moveY = 400 - avatorImg.offsetHeight;
}
avatorImg.style.top = _moveY + 'px';
mouseStartY = e.clientY;
}
裁剪
最后,我們利用HTML5的canvas元素具有的ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);方法,描繪出位于裁剪區(qū)的圖片信息。
/**
* 對(duì)圖片進(jìn)行裁剪
*/
function crop() {
if (!avatorImg.src) return;
let _cropCanvas = document.createElement('canvas');
// 計(jì)算邊長(zhǎng)
let _side = (showSide / avatorImg.offsetWidth) * primitiveLength.width;
_cropCanvas.width = _side;
_cropCanvas.height = _side;
// 計(jì)算截取時(shí)從原圖片的原始長(zhǎng)度的坐標(biāo)
// 因?yàn)閳D片有可能會(huì)被放大/縮小,這時(shí)候,初始化時(shí)記錄下來(lái)的primitiveLength信息就有用處了
let _sy = (100 - avatorImg.offsetTop) / avatorImg.offsetHeight * primitiveLength.height;
let _sx = (100 - avatorImg.offsetLeft) / avatorImg.offsetWidth * primitiveLength.width;
// 繪制圖片
_cropCanvas.getContext('2d').drawImage(avatorImg, _sx, _sy, _side, _side, 0, 0, _side, _side);
// 保存圖片信息
let _lastImageData = _cropCanvas.toDataURL(croppedImageType);
// 將裁剪出來(lái)的信息展示
imageShow.src = _lastImageData;
imageShow.style.width = showSide + 'px';
imageShow.style.height = showSide + 'px';
}