【canvas】canvas的综合运用:图片裁剪

简言

使用canvas实现图片裁剪功能。
在这里插入图片描述

图像裁剪

图像裁剪是一个比较常见的功能,使用canvas的相关方法和属性可以实现简易的图片裁剪功能。

功能分析

  1. 上传图片
  2. 图片显示
  3. 绘制裁剪框
  4. 裁剪操作
  5. 生成预览
  6. 提供下载

上传图片

使用input的file类型的元素可以上传图片文件数据,然后转成base64供使用。

 file.addEventListener('change', (e) => {
      const file = e.target.files[0];
      
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = (e) => {
        img.src = e.target.result;
        img.onload = () => {
          console.log(img.width, img.height);
          //  更新图片缩放比例
          if (img.width > img.height) {
            scale = 500 / img.width
          } else {
            scale = 500 / img.height
          }
          drawOperatingLine()
        }

      }
    })

图片显示

canvas的drawImage()方法可以接收一个img元素来显示图片。
我这边canvas固定宽高了,所以做了缩放。

  ctx.clearRect(0, 0, origin.width, origin.height)
      //  绘制图像
      dx = (origin.width - scale * img.width) / 2 //  图片偏移 x
      dy = (origin.height - scale * img.height) / 2 //  图片偏移 y
      dw = scale * img.width
      dh = scale * img.height
      ctx.drawImage(img, 0, 0, img.width, img.height, dx, dy, dw, dh);

绘制裁剪框

主要绘制半透明蒙层和裁剪框,以及四周边缘点。

理想情况下是使用canvas的globalCompositeOperation(组合)实现,可以绘制多样性裁剪框

在这里插入图片描述

  //  透明度50%蒙版
      ctx.save()

      ctx.fillStyle = 'rgba(0,0,0,0.4)';
      ctx.fillRect(0, 0, origin.width, initY);
      ctx.fillRect(0, endY, origin.width, origin.height - initY);
      ctx.fillRect(0, initY, initX, endY - initY);
      ctx.fillRect(endX, initY, origin.width - endX, endY - initY);
      ctx.restore()


      // 边缘线
      ctx.save()
      ctx.strokeStyle = 'red';
      ctx.setLineDash([5, 5])
      ctx.moveTo(initX, initY);
      ctx.lineTo(endX, initY);
      ctx.lineTo(endX, endY);
      ctx.lineTo(initX, endY);
      ctx.closePath();
      ctx.stroke();
      // 矩形四周点
      ctx.fillStyle = '#fff';
      ctx.fillRect(initX - 5, initY - 5, 10, 10);
      ctx.fillRect(endX - 5, initY - 5, 10, 10);
      ctx.fillRect(endX - 5, endY - 5, 10, 10);
      ctx.fillRect(initX - 5, endY - 5, 10, 10);
      ctx.beginPath();
      ctx.restore()

裁剪操作

裁剪操作有:

  • 操作边缘线改变裁剪框大小
  • 操作四周边缘点改变裁剪框大小
  • 操作裁剪框中间移动裁剪框

改变裁剪框位置和大小需要按下、移动、抬起三个事件。

操作后记得及时更新画布

   origin.addEventListener("mousedown", (e) => {
      if (now !== 0) {
        modify = true
        lastX = e.offsetX
        lastY = e.offsetY
      }
    });
    origin.addEventListener("mousemove", (e) => {
      if (!ctx) return

      let x = e.offsetX
      let y = e.offsetY
      //  判定类型
      if ((x > initX + 5 && x < endX - 5) && (y < initY + 5 && y > initY - 5)) {  //  上边
        origin.style.cursor = 'ns-resize'
        now = 1
      } else if ((x < endX + 5 && x > endX - 5) && (y > initY + 5 && y < endY - 5)) {  //  右边
        origin.style.cursor = 'ew-resize'
        now = 2

      } else if ((x > initX + 5 && x < endX - 5) && (y < endY + 5 && y > endY - 5)) {  //  下边
        origin.style.cursor = 'ns-resize'
        now = 3

      } else if ((x < initX + 5 && x > initX - 5) && (y > initY + 5 && y < endY - 5)) {  //  左边
        origin.style.cursor = 'ew-resize'
        now = 4

      } else if (x <= initX + 5 && x >= initX - 5 && y <= initY + 5 && y >= initY - 5) { //  左上角
        origin.style.cursor = 'nwse-resize'
        now = 5

      } else if (x <= endX + 5 && x >= endX - 5 && y <= endY + 5 && y >= endY - 5) { //  右下角
        origin.style.cursor = 'nwse-resize'
        now = 6

      } else if (x <= endX + 5 && x >= endX - 5 && y <= initY + 5 && y >= initY - 5) { //  右上角
        origin.style.cursor = 'nesw-resize'
        now = 7

      } else if (x <= initX + 5 && x >= initX - 5 && y <= endY + 5 && y >= endY - 5) { //  左下角
        origin.style.cursor = 'nesw-resize'
        now = 8
      }
      else if (x > initX + 5 && x < endX - 5 && y > initY + 5 && y < endY - 5) { //  移动
        origin.style.cursor = 'all-scroll'
        now = 9
      }
      else {
        origin.style.cursor = 'auto'
        now = 0
      }
      //  若在拖动根据类型更改值
      if (modify) {
        switch (now) {
          case 1: //  上边
            initY = e.offsetY
            break
          case 2: //  右边
            endX = e.offsetX
            break
          case 3: //  下边
            endY = e.offsetY
            break
          case 4: //  左边
            initX = e.offsetX
            break
          case 5: //  左上角
            initX = e.offsetX
            initY = e.offsetY
            break
          case 6: //  右下角
            endY = e.offsetY
            endX = e.offsetX
            break
          case 7: //  右上角
            endX = e.offsetX
            initY = e.offsetY
            break

          case 8: //  左下角
            initX = e.offsetX
            endY = e.offsetY
            break
          case 9: //  左下角
            let vx = e.offsetX - lastX
            let vy = e.offsetY - lastY

            initX += vx
            endX += vx

            initY += vy
            endY += vy

            //  更新
            lastX = e.offsetX
            lastY = e.offsetY
            break


        }
        //  更新

        drawOperatingLine()
      }
    })
    origin.addEventListener('mouseup', (e) => {
      if (modify) {
        modify = false

      }
    })

生成预览

我实现的是预览要裁剪后的原图片部分内容。
使用另外一个canvas来绘制img元素的裁剪包含部分。

有缩放的话,注意还原

在这里插入图片描述

下载裁剪后图片

canvas有一个toDataURL()方法可以将canvas转成base64数据。

 btn.onclick = () => {

      //  生成图片
      let imgData = canvas.toDataURL('image/png', 1)
      let a = document.createElement('a')
      a.href = imgData
      a.download = 'image.png'
      a.click()
    }

源码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图像剪辑</title>
  <style>
    html,
    body {
      width: 100vw;
      height: 100%;
      margin: 0;
      padding: 0;
    }

    .box {
      box-sizing: border-box;
      width: 100%;
      height: 100%;
      padding: 24px;
    }

    .box-content {
      margin-top: 16px;
      box-sizing: border-box;
      width: 100%;
      /* height: 500px; */
      /* display: flex; */
      align-items: center;
      border: 1px solid #000;
    }

    #origin {
      width: 500px;
      height: 500px;
    }
  </style>
</head>

<body>
  <div class="box">
    <div>
      <h1>图像剪辑</h1>
      <input type="file" name="file" id="file">

    </div>
    <div class="box-content">
      <canvas id="origin" width="500" height="500">
      </canvas>
      <div>
        <div>预览:</div>
        <canvas id="canvas">
        </canvas>
      </div>
    </div>
    <div>
      <button id="btn">下载裁剪后的内容</button>
    </div>
  </div>
  <script>
    const file = document.getElementById('file');
    const origin = document.getElementById('origin');
    const canvas = document.getElementById('canvas')
    const btn = document.getElementById('btn');
    const ctx = origin.getContext('2d');
    const img = document.createElement('img');
    //  缩放
    let scale = 1 //  图片缩放比例
    let dx = 0 //  图片偏移 x
    let dy = 0//  图片偏移 y
    let dw = origin.width
    let dh = origin.height
    //  裁剪位置
    let initX = 50
    let initY = 50
    let endX = origin.width - initX
    let endY = origin.height - initY
    //  裁剪状态
    let now = 0;
    let modify = false
    let lastX = 0, lastY = 0

    origin.addEventListener("mousedown", (e) => {
      if (now !== 0) {
        modify = true
        lastX = e.offsetX
        lastY = e.offsetY
      }
    });
    origin.addEventListener("mousemove", (e) => {
      if (!ctx) return

      let x = e.offsetX
      let y = e.offsetY
      //  判定类型
      if ((x > initX + 5 && x < endX - 5) && (y < initY + 5 && y > initY - 5)) {  //  上边
        origin.style.cursor = 'ns-resize'
        now = 1
      } else if ((x < endX + 5 && x > endX - 5) && (y > initY + 5 && y < endY - 5)) {  //  右边
        origin.style.cursor = 'ew-resize'
        now = 2

      } else if ((x > initX + 5 && x < endX - 5) && (y < endY + 5 && y > endY - 5)) {  //  下边
        origin.style.cursor = 'ns-resize'
        now = 3

      } else if ((x < initX + 5 && x > initX - 5) && (y > initY + 5 && y < endY - 5)) {  //  左边
        origin.style.cursor = 'ew-resize'
        now = 4

      } else if (x <= initX + 5 && x >= initX - 5 && y <= initY + 5 && y >= initY - 5) { //  左上角
        origin.style.cursor = 'nwse-resize'
        now = 5

      } else if (x <= endX + 5 && x >= endX - 5 && y <= endY + 5 && y >= endY - 5) { //  右下角
        origin.style.cursor = 'nwse-resize'
        now = 6

      } else if (x <= endX + 5 && x >= endX - 5 && y <= initY + 5 && y >= initY - 5) { //  右上角
        origin.style.cursor = 'nesw-resize'
        now = 7

      } else if (x <= initX + 5 && x >= initX - 5 && y <= endY + 5 && y >= endY - 5) { //  左下角
        origin.style.cursor = 'nesw-resize'
        now = 8
      }
      else if (x > initX + 5 && x < endX - 5 && y > initY + 5 && y < endY - 5) { //  移动
        origin.style.cursor = 'all-scroll'
        now = 9
      }
      else {
        origin.style.cursor = 'auto'
        now = 0
      }
      //  若在拖动根据类型更改值
      if (modify) {
        switch (now) {
          case 1: //  上边
            initY = e.offsetY
            break
          case 2: //  右边
            endX = e.offsetX
            break
          case 3: //  下边
            endY = e.offsetY
            break
          case 4: //  左边
            initX = e.offsetX
            break
          case 5: //  左上角
            initX = e.offsetX
            initY = e.offsetY
            break
          case 6: //  右下角
            endY = e.offsetY
            endX = e.offsetX
            break
          case 7: //  右上角
            endX = e.offsetX
            initY = e.offsetY
            break

          case 8: //  左下角
            initX = e.offsetX
            endY = e.offsetY
            break
          case 9: //  左下角
            let vx = e.offsetX - lastX
            let vy = e.offsetY - lastY

            initX += vx
            endX += vx

            initY += vy
            endY += vy

            //  更新
            lastX = e.offsetX
            lastY = e.offsetY
            break


        }
        //  更新

        drawOperatingLine()
      }
    })
    origin.addEventListener('mouseup', (e) => {
      if (modify) {
        modify = false

      }
    })

    file.addEventListener('change', (e) => {
      const file = e.target.files[0];

      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = (e) => {
        img.src = e.target.result;
        img.onload = () => {
          console.log(img.width, img.height);
          //  更新图片缩放比例
          if (img.width > img.height) {
            scale = 500 / img.width
          } else {
            scale = 500 / img.height
          }
          drawOperatingLine()
        }

      }
    })
    //  绘制裁剪控制图像
    function drawOperatingLine() {
      ctx.clearRect(0, 0, origin.width, origin.height)
      //  绘制图像
      dx = (origin.width - scale * img.width) / 2 //  图片偏移 x
      dy = (origin.height - scale * img.height) / 2 //  图片偏移 y
      dw = scale * img.width
      dh = scale * img.height
      ctx.drawImage(img, 0, 0, img.width, img.height, dx, dy, dw, dh);

      //  透明度50%蒙版
      ctx.save()

      ctx.fillStyle = 'rgba(0,0,0,0.4)';
      ctx.fillRect(0, 0, origin.width, initY);
      ctx.fillRect(0, endY, origin.width, origin.height - initY);
      ctx.fillRect(0, initY, initX, endY - initY);
      ctx.fillRect(endX, initY, origin.width - endX, endY - initY);
      ctx.restore()


      // 边缘线
      ctx.save()
      ctx.strokeStyle = 'red';
      ctx.setLineDash([5, 5])
      ctx.moveTo(initX, initY);
      ctx.lineTo(endX, initY);
      ctx.lineTo(endX, endY);
      ctx.lineTo(initX, endY);
      ctx.closePath();
      ctx.stroke();
      // 矩形四周点
      ctx.fillStyle = '#fff';
      ctx.fillRect(initX - 5, initY - 5, 10, 10);
      ctx.fillRect(endX - 5, initY - 5, 10, 10);
      ctx.fillRect(endX - 5, endY - 5, 10, 10);
      ctx.fillRect(initX - 5, endY - 5, 10, 10);
      ctx.beginPath();
      ctx.restore()
      //  生成预览
      preview()
    }

    //  生成预览
    function preview() {
      let lsx = initX < dx ? dx : initX
      let lsy = initY < dy ? dy : initY
      let lex = endX > dx + dw ? dx + dw : endX
      let ley = endY > dy + dh ? dy + dh : endY
      let sx = 0
      let sy = 0
      if (initX > dx + dw) {
        sx = img.width
      } else if (initX > dx) {
        sx = (initX - dx) / scale
      }
      if (initY > dy + dh) {
        sy = img.height
      } else if (initY > dy) {
        sy = (initY - dy) / scale
      }
      const sw = (lex - lsx) / scale //  实际图片裁剪宽度
      const sh = (ley - lsy) / scale  //  实际图片裁剪高度
      canvas.width = sw
      canvas.height = sh
      canvas.getContext('2d').drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh)
    }
    btn.onclick = () => {

      //  生成图片
      let imgData = canvas.toDataURL('image/png', 1)
      let a = document.createElement('a')
      a.href = imgData
      a.download = 'image.png'
      a.click()
    }
  </script>
</body>

</html>

在这里插入图片描述

结语

有一些边界情况要处理。例如,裁剪框边界情况,图片过小缩放后模糊情况等。

觉得有用的话,麻烦动动小手点个赞,关注一下吧!

  • 35
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZSK6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值