【JS】图片裁剪上传

前言

流程如下:本地预览 => 裁剪 => 上传

实现

1. 本地预览

将数据读为 dataurl 赋值给 img 标签的 src

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .container {
      display: flex;
    }

    #image-container {
      position: relative;
      width: 400px;
      height: 400px;
      border: 1px solid #ccc;
      background-color: #f3f3f3;
    }

    #result {
      margin-left: 20px;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
      background-color: #f3f3f3;
    }
  </style>
</head>

<body>
  <div>
    <input type="file" id="file-input">
  </div>
  <div class="container">
    <div id="image-container">
      <img id="uploaded-image" style="width: 100%; height: 100%;" />
    </div>
    <canvas id="result" width="200" height="200"></canvas> <!-- 显示裁剪结果的画布 -->
  </div>

  <script>
    const fileInput = document.getElementById('file-input');
    const uploadedImage = document.getElementById('uploaded-image');

    fileInput.onchange = (e) => {
      const file = e.target.files[0];
      const reader = new FileReader();
      reader.onload = (e) => {
        uploadedImage.src = e.target.result;
      }
      reader.readAsDataURL(file);
    }
  </script>
</body>

</html>

2. 裁剪

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图片裁剪预览</title>
  <style>
    .container {
      display: flex;
    }

    #image-container {
      position: relative;
      width: 400px;
      height: 400px;
      border: 1px solid #ccc;
      background-color: #f3f3f3;
    }

    #crop-box {
      position: absolute;
      border: 2px dashed #ff0000;
      cursor: move;
      display: none;
    }

    #crop-box .resize-handle {
      position: absolute;
      width: 10px;
      height: 10px;
      background: #ff0000;
      z-index: 10;
    }

    #crop-box .top-left {
      left: -5px;
      top: -5px;
      cursor: nwse-resize;
    }

    #crop-box .top-right {
      right: -5px;
      top: -5px;
      cursor: nesw-resize;
    }

    #crop-box .bottom-left {
      left: -5px;
      bottom: -5px;
      cursor: nesw-resize;
    }

    #crop-box .bottom-right {
      right: -5px;
      bottom: -5px;
      cursor: nwse-resize;
    }

    #result {
      margin-left: 20px;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
      background-color: #f3f3f3;
    }
  </style>
</head>

<body>

  <div>
    <input type="file" id="file-input">
  </div>
  <div class="container">
    <div id="image-container">
      <img id="uploaded-image" style="max-width: 100%; max-height: 100%; display: none;" />
      <div id="crop-box">
        <div class="resize-handle top-left"></div> <!-- 左上角调整手柄 -->
        <div class="resize-handle top-right"></div> <!-- 右上角调整手柄 -->
        <div class="resize-handle bottom-left"></div> <!-- 左下角调整手柄 -->
        <div class="resize-handle bottom-right"></div> <!-- 右下角调整手柄 -->
      </div>
    </div>
    <canvas id="result" width="200" height="200"></canvas> <!-- 显示裁剪结果的画布 -->
  </div>

  <script>
    const fileInput = document.getElementById('file-input');
    const uploadedImage = document.getElementById('uploaded-image');
    const cropBox = document.getElementById('crop-box');
    const resultCanvas = document.getElementById('result');
    const ctx = resultCanvas.getContext('2d');
    const commitBtn = document.getElementById('commit-btn');

    let image = new Image();
    let cropInfo = { x: 0, y: 0, width: 100, height: 100 }; // 裁剪框的位置和大小
    let scale = 1; // 图片缩放比例

    let isDragging = false; // 是否正在拖动裁剪框
    let isResizing = false; // 是否正在调整裁剪框大小
    let resizingHandle = null; // 当前调整手柄
    const MIN_CROP_SIZE = 20; // 裁剪框的最小尺寸

    fileInput.addEventListener('change', (event) => {
      const file = event.target.files[0];
      const reader = new FileReader();

      reader.onload = function (e) {
        image.src = e.target.result;
        uploadedImage.src = e.target.result;
        uploadedImage.style.display = 'block';
      }

      reader.readAsDataURL(file);
    });

    image.onload = function () {
      // 获取容器宽高
      const container = document.getElementById('image-container');
      const containerWidth = container.offsetWidth;
      const containerHeight = container.offsetHeight;

      scale = Math.min(containerWidth / image.width, containerHeight / image.height); // 计算缩放比例

      uploadedImage.style.width = image.width * scale + 'px'; // 根据缩放比例设置上传图片的宽度
      uploadedImage.style.height = image.height * scale + 'px'; // 根据缩放比例设置上传图片的高度

      // 重置裁剪框的位置和大小
      cropInfo = { x: 0, y: 0, width: 100, height: 100 };

      updateCropBox(); // 更新裁剪框位置和大小
      cropBox.style.display = 'block'; // 显示裁剪框

      drawCrop(); // 绘制裁剪结果
    }

    // 更新裁剪框的位置和大小
    function updateCropBox() {
      cropBox.style.left = cropInfo.x + 'px';
      cropBox.style.top = cropInfo.y + 'px';
      cropBox.style.width = cropInfo.width + 'px';
      cropBox.style.height = cropInfo.height + 'px';
    }

    // 绘制裁剪结果
    function drawCrop() {
      const cropX = cropInfo.x / scale;
      const cropY = cropInfo.y / scale;
      const cropWidth = cropInfo.width / scale;
      const cropHeight = cropInfo.height / scale;

      ctx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); // 清空画布
      ctx.drawImage(image, cropX, cropY, cropWidth, cropHeight, 0, 0, resultCanvas.width, resultCanvas.height); // 绘制裁剪的图片
    }

    cropBox.addEventListener('mousedown', function (e) {
      if (e.target.classList.contains('resize-handle')) {
        // 如果按下的是调整手柄
        isResizing = true;
        resizingHandle = e.target; // 当前调整手柄
      } else {
        // 如果按下的是裁剪框,记录鼠标相对裁剪框的坐标
        isDragging = true;
        offsetX = e.offsetX;
        offsetY = e.offsetY;
      }
      document.addEventListener('mousemove', mouseMove); // 添加鼠标移动事件
      document.addEventListener('mouseup', mouseUp); // 添加鼠标抬起事件
    });

    function mouseMove(e) {
      if (isDragging) {
        // 正在拖动裁剪框,更新裁剪框坐标
        cropInfo.x = e.clientX - offsetX - image.getBoundingClientRect().left;
        cropInfo.y = e.clientY - offsetY - image.getBoundingClientRect().top;
        updateCropBox(); // 更新裁剪框
        drawCrop(); // 绘制裁剪结果
      }

      if (isResizing) {
        // 正在调整裁剪框大小
        const handleClass = resizingHandle.classList[1]; // 获取调整手柄的类名
        if (handleClass.includes('top-left')) {
          // 左上角手柄
          const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
          const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.x = e.clientX - image.getBoundingClientRect().left;
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.y = e.clientY - image.getBoundingClientRect().top;
            cropInfo.height = newHeight;
          }
        } else if (handleClass.includes('top-right')) {
          // 右上角手柄
          const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
          const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.y = e.clientY - image.getBoundingClientRect().top;
            cropInfo.height = newHeight;
          }
        } else if (handleClass.includes('bottom-left')) {
          // 左下角手柄
          const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
          const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.x = e.clientX - image.getBoundingClientRect().left;
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.height = newHeight;
          }
        } else if (handleClass.includes('bottom-right')) {
          // 右下角手柄
          const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
          const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.height = newHeight;
          }
        }

        updateCropBox(); // 更新裁剪框
        drawCrop(); // 绘制裁剪结果
      }
    }

    function mouseUp() {
      isDragging = false; // 停止拖动
      isResizing = false; // 停止调整大小
      document.removeEventListener('mousemove', mouseMove); // 移除鼠标移动事件
      document.removeEventListener('mouseup', mouseUp); // 移除鼠标抬起事件
    }
  </script>
</body>

</html>

3. 上传

通过 canvas 生成 blob 再变为 File

resultCanvas.toBlob((blob) => {
  const file = new File([blob], 'cut.png', { type: 'image/png' });
  console.log(file); // 执行上传逻辑即可
})

整体代码

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图片裁剪预览</title>
  <style>
    .container {
      display: flex;
    }

    #image-container {
      position: relative;
      width: 400px;
      height: 400px;
      border: 1px solid #ccc;
      background-color: #f3f3f3;
    }

    #crop-box {
      position: absolute;
      border: 2px dashed #ff0000;
      cursor: move;
      display: none;
    }

    #crop-box .resize-handle {
      position: absolute;
      width: 10px;
      height: 10px;
      background: #ff0000;
      z-index: 10;
    }

    #crop-box .top-left {
      left: -5px;
      top: -5px;
      cursor: nwse-resize;
    }

    #crop-box .top-right {
      right: -5px;
      top: -5px;
      cursor: nesw-resize;
    }

    #crop-box .bottom-left {
      left: -5px;
      bottom: -5px;
      cursor: nesw-resize;
    }

    #crop-box .bottom-right {
      right: -5px;
      bottom: -5px;
      cursor: nwse-resize;
    }

    #result {
      margin-left: 20px;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
      background-color: #f3f3f3;
    }

    #commit-btn {
      margin-left: 20px;
      border: 1px solid #ccc;
      width: 60px;
      height: 30px;
    }
  </style>
</head>

<body>

  <div>
    <input type="file" id="file-input">
  </div>
  <div class="container">
    <div id="image-container">
      <img id="uploaded-image" style="max-width: 100%; max-height: 100%; display: none;" />
      <div id="crop-box">
        <div class="resize-handle top-left"></div> <!-- 左上角调整手柄 -->
        <div class="resize-handle top-right"></div> <!-- 右上角调整手柄 -->
        <div class="resize-handle bottom-left"></div> <!-- 左下角调整手柄 -->
        <div class="resize-handle bottom-right"></div> <!-- 右下角调整手柄 -->
      </div>
    </div>
    <canvas id="result" width="200" height="200"></canvas> <!-- 显示裁剪结果的画布 -->
    <button id="commit-btn">上传</button>
  </div>

  <script>
    const fileInput = document.getElementById('file-input');
    const uploadedImage = document.getElementById('uploaded-image');
    const cropBox = document.getElementById('crop-box');
    const resultCanvas = document.getElementById('result');
    const ctx = resultCanvas.getContext('2d');
    const commitBtn = document.getElementById('commit-btn');

    let image = new Image();
    let cropInfo = { x: 0, y: 0, width: 100, height: 100 }; // 裁剪框的位置和大小
    let scale = 1; // 图片缩放比例

    let isDragging = false; // 是否正在拖动裁剪框
    let isResizing = false; // 是否正在调整裁剪框大小
    let resizingHandle = null; // 当前调整手柄
    const MIN_CROP_SIZE = 20; // 裁剪框的最小尺寸

    fileInput.addEventListener('change', (event) => {
      const file = event.target.files[0];
      const reader = new FileReader();

      reader.onload = function (e) {
        image.src = e.target.result;
        uploadedImage.src = e.target.result;
        uploadedImage.style.display = 'block';
      }

      reader.readAsDataURL(file);
    });

    image.onload = function () {
      // 获取容器宽高
      const container = document.getElementById('image-container');
      const containerWidth = container.offsetWidth;
      const containerHeight = container.offsetHeight;

      scale = Math.min(containerWidth / image.width, containerHeight / image.height); // 计算缩放比例

      uploadedImage.style.width = image.width * scale + 'px'; // 根据缩放比例设置上传图片的宽度
      uploadedImage.style.height = image.height * scale + 'px'; // 根据缩放比例设置上传图片的高度

      // 重置裁剪框的位置和大小
      cropInfo = { x: 0, y: 0, width: 100, height: 100 };

      updateCropBox(); // 更新裁剪框位置和大小
      cropBox.style.display = 'block'; // 显示裁剪框

      drawCrop(); // 绘制裁剪结果
    }

    // 更新裁剪框的位置和大小
    function updateCropBox() {
      cropBox.style.left = cropInfo.x + 'px';
      cropBox.style.top = cropInfo.y + 'px';
      cropBox.style.width = cropInfo.width + 'px';
      cropBox.style.height = cropInfo.height + 'px';
    }

    // 绘制裁剪结果
    function drawCrop() {
      const cropX = cropInfo.x / scale;
      const cropY = cropInfo.y / scale;
      const cropWidth = cropInfo.width / scale;
      const cropHeight = cropInfo.height / scale;

      ctx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); // 清空画布
      ctx.drawImage(image, cropX, cropY, cropWidth, cropHeight, 0, 0, resultCanvas.width, resultCanvas.height); // 绘制裁剪的图片
    }

    cropBox.addEventListener('mousedown', function (e) {
      if (e.target.classList.contains('resize-handle')) {
        // 如果按下的是调整手柄
        isResizing = true;
        resizingHandle = e.target; // 当前调整手柄
      } else {
        // 如果按下的是裁剪框,记录鼠标相对裁剪框的坐标
        isDragging = true;
        offsetX = e.offsetX;
        offsetY = e.offsetY;
      }
      document.addEventListener('mousemove', mouseMove); // 添加鼠标移动事件
      document.addEventListener('mouseup', mouseUp); // 添加鼠标抬起事件
    });

    function mouseMove(e) {
      if (isDragging) {
        // 正在拖动裁剪框,更新裁剪框坐标
        cropInfo.x = e.clientX - offsetX - image.getBoundingClientRect().left;
        cropInfo.y = e.clientY - offsetY - image.getBoundingClientRect().top;
        updateCropBox(); // 更新裁剪框
        drawCrop(); // 绘制裁剪结果
      }

      if (isResizing) {
        // 正在调整裁剪框大小
        const handleClass = resizingHandle.classList[1]; // 获取调整手柄的类名
        if (handleClass.includes('top-left')) {
          // 左上角手柄
          const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
          const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.x = e.clientX - image.getBoundingClientRect().left;
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.y = e.clientY - image.getBoundingClientRect().top;
            cropInfo.height = newHeight;
          }
        } else if (handleClass.includes('top-right')) {
          // 右上角手柄
          const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
          const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.y = e.clientY - image.getBoundingClientRect().top;
            cropInfo.height = newHeight;
          }
        } else if (handleClass.includes('bottom-left')) {
          // 左下角手柄
          const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
          const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.x = e.clientX - image.getBoundingClientRect().left;
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.height = newHeight;
          }
        } else if (handleClass.includes('bottom-right')) {
          // 右下角手柄
          const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
          const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
          // 如果新宽高合理,则更新裁剪框位置
          if (newWidth > MIN_CROP_SIZE) {
            cropInfo.width = newWidth;
          }
          if (newHeight > MIN_CROP_SIZE) {
            cropInfo.height = newHeight;
          }
        }

        updateCropBox(); // 更新裁剪框
        drawCrop(); // 绘制裁剪结果
      }
    }

    function mouseUp() {
      isDragging = false; // 停止拖动
      isResizing = false; // 停止调整大小
      document.removeEventListener('mousemove', mouseMove); // 移除鼠标移动事件
      document.removeEventListener('mouseup', mouseUp); // 移除鼠标抬起事件
    }

    commitBtn.addEventListener('click', function () {
      resultCanvas.toBlob((blob) => {
        const file = new File([blob], 'cut.png', { type: 'image/png' });
        console.log(file); // 执行上传逻辑即可
      })
    })
  </script>
</body>

</html>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

田本初

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

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

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

打赏作者

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

抵扣说明:

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

余额充值