使用原生JS編寫(xiě)一個(gè)簡(jiǎn)單的圖片裁剪器

本文原文鏈接:http://classlfz.com/2017/07/23/write-an-image-scissors/

作為一個(gè)前端開(kāi)發(fā)人員,這次的這篇博客文章終于“正?!绷?。

點(diǎn)擊查看完整例子

這應(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';
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容