很多网站和 App 在上传头像的时候,如果图片的长宽比不一样,就需要选择区域裁剪为正方形。

在很早以前,基本上只能在前端选择裁剪区域,发到服务器裁剪,现在随着浏览器的功能越来越强大和用户设备性能的提升,基本上可以在前端裁剪完再上传。

这里简单实现图片的框选和裁剪,访问 图片框选裁剪demo 可以在线预览图片框选裁剪效果。

图片框选

下面先实现图片的框选。

HTML:

<!--Mr. Ma's Blog www.misterma.com-->
<img src="image.jpg" alt="图片">
<div id="select-box"></div>
<canvas width="200" height="200"></canvas>
<button type="button" id="crop-btn">裁剪</button>

我这里为了方便就直接使用 img 加载图片了,idselect-boxdiv 就是可以拖拽的选择框,canvas 裁剪的时候会用到。

给选择框加一点 CSS:

* {
  margin: 0;
  padding: 0;
}

#select-box {
  width: 200px;
  height: 200px;
  background: rgba(255, 255, 0, 0.4);
  position: absolute;
  display: none;
  cursor: move;
}

选择框的宽度和高度都是 200px,半透明,不可缩放,默认隐藏,只有图片加载完成后才显示。

下面是选择框拖拽的 JS:

const imgEl = document.querySelector('img');  // 图片
const selectBox = document.querySelector('#select-box');  // 区域选择框
const cropBtn = document.querySelector('#crop-btn');  // 裁剪按钮
const canvasEl = document.querySelector('canvas');  // canvas

// 图片加载完成
imgEl.addEventListener('load', () => {
  // 显示区域选择框
  selectBox.style.display = 'block';
  // 把区域选择框放到 img 上
  selectBox.style.top = imgEl.offsetTop + 'px';
  selectBox.style.left = imgEl.offsetLeft + 'px';
});

// 区域选择框鼠标按下
selectBox.addEventListener('mousedown', ev => {
  const X = ev.clientX - ev.target.offsetLeft;
  const Y = ev.clientY - ev.target.offsetTop;

  // 鼠标移动
  document.onmousemove = ev => {
    selectBox.style.left = ev.clientX - X + 'px';
    selectBox.style.top = ev.clientY - Y + 'px';
    // 限制选择框的拖动范围,禁止拖出图片区域
    if (selectBox.offsetLeft <= imgEl.offsetLeft) {
      selectBox.style.left = imgEl.offsetLeft + 'px';
    }
    if (selectBox.offsetLeft >= imgEl.offsetWidth - selectBox.offsetWidth) {
      selectBox.style.left = imgEl.offsetWidth - selectBox.offsetWidth + 'px';
    }
    if (selectBox.offsetTop <= imgEl.offsetTop) {
      selectBox.style.top = imgEl.offsetTop + 'px';
    }
    if (selectBox.offsetTop >= imgEl.offsetHeight - selectBox.offsetHeight) {
      selectBox.style.top = imgEl.offsetHeight - selectBox.offsetHeight + 'px';
    }
  }

  // 鼠标放开
  document.onmouseup = () => {
    document.onmousemove = null;
  }
  return false;
});

我在 CSS 中已经给区域选择框设置了 absolute 绝对定位,图片加载完成后区域选择框就会被放到图片的左上方。

我这里为了方便清除事件,没有用 addEventListenerdocument 添加鼠标移动和鼠标放开事件,而是直接给 document 绑定事件。

关于元素拖拽的实现方式我这里就不详细的写了,上面的代码很容易看懂。

canvas 和裁剪按钮上面还没有用到,下面写裁剪的时候会用到。

区域选择框拖拽效果如下:

区域选择框拖拽

区域选择框是无法拖出图片区域的。

图片裁剪

在上面的代码基础上再增加裁剪功能:

const imgEl = document.querySelector('img');  // 图片
const selectBox = document.querySelector('#select-box');  // 区域选择框
const cropBtn = document.querySelector('#crop-btn');  // 裁剪按钮
const canvasEl = document.querySelector('canvas');  // canvas
let imgFile = null;  // 存放裁剪后的图片

// 图片加载完成
imgEl.addEventListener('load', () => {
  // 显示区域选择框
  selectBox.style.display = 'block';
  // 把区域选择框放到 img 上
  selectBox.style.top = imgEl.offsetTop + 'px';
  selectBox.style.left = imgEl.offsetLeft + 'px';
});

// 区域选择框鼠标按下
selectBox.addEventListener('mousedown', ev => {
  const X = ev.clientX - ev.target.offsetLeft;
  const Y = ev.clientY - ev.target.offsetTop;

  // 鼠标移动
  document.onmousemove = ev => {
    selectBox.style.left = ev.clientX - X + 'px';
    selectBox.style.top = ev.clientY - Y + 'px';
    // 限制选择框的拖动范围,禁止拖出图片区域
    if (selectBox.offsetLeft <= imgEl.offsetLeft) {
      selectBox.style.left = imgEl.offsetLeft + 'px';
    }
    if (selectBox.offsetLeft >= imgEl.offsetWidth - selectBox.offsetWidth) {
      selectBox.style.left = imgEl.offsetWidth - selectBox.offsetWidth + 'px';
    }
    if (selectBox.offsetTop <= imgEl.offsetTop) {
      selectBox.style.top = imgEl.offsetTop + 'px';
    }
    if (selectBox.offsetTop >= imgEl.offsetHeight - selectBox.offsetHeight) {
      selectBox.style.top = imgEl.offsetHeight - selectBox.offsetHeight + 'px';
    }
  }

  // 鼠标放开
  document.onmouseup = () => {
    document.onmousemove = null;
  }
  return false;
});

// 图片裁剪按钮点击
cropBtn.addEventListener('click', () => {
  const sX = selectBox.offsetLeft - imgEl.offsetLeft;  // 区域选择框左侧位置
  const sY = selectBox.offsetTop - imgEl.offsetTop;  // 区域选择框上方位置
  const sW = selectBox.offsetWidth;  // 区域选择框宽度
  const sH = selectBox.offsetHeight;  // 区域选择框高度
  // 把图片截取到 canvas
  canvasEl.getContext('2d').drawImage(imgEl, sX, sY, sW, sH , 0, 0, canvasEl.width, canvasEl.height);
  // 把裁剪后的 canvas 图像转为 Blob
  canvasEl.toBlob(blob => {
    if (blob === null) return false;
    imgFile = blob;
  }, 'image/jpeg');
});

裁剪按钮点击后使用 canvasdrawImageimg 截取图像来绘制图像,drawImage 的参数说明如下:

  • image: 截取的图像资源
  • sX: 截取图像的左侧起始位置
  • sY: 截取图像的顶部起始位置
  • sW: 截取图像的宽度
  • sH: 截取图像的高度
  • dXcanvas 绘制图像的左侧起始位置
  • dYcanvas 绘制图像的顶部起始位置
  • dWcanvas 绘制图像的宽度
  • dHcanvas 绘制图像的高度

我的 sX 使用的是区域选择框左侧位置减去图片左侧位置,sY 是区域选择框顶部位置减去图片顶部位置,sW 是区域选择框的宽度,sH 是区域选择框高度。

使用 canvastoBlob 可以把图像转为 Blob 数据,下面是参数说明:

  • callback: 回调函数,函数会返回一个 Blob
  • type: 指定图片格式,可以省略

转换为 Blob 后就可以添加到 FormData 上传了。

注意,如果 canvas 受到污染是不能调用 toBlob 的!

如果 canvas 截取的图像源出现跨域情况,canvas 就会被判定为受污染。比如 img 跨域调用图片,通过 canvas 截取后 canvas 就会受污染,不能调用 toBlob

图片框选截取的效果如下:

图片框选裁剪

我这里的区域选择框的大小是固定的,不能缩放。

如果要实现区域选择框缩放,可以让鼠标在区域选择框右侧和下方拖动时,让区域选择框跟随鼠标位置增加或减少宽高。

类似文章: