头像剪切上传

文章说明

本文主要为了学习头像裁剪功能,以及熟悉canvas绘图和转文件的相关操作,参考教程(Web渡一前端–图片裁剪上传原理

核心Api

主要就一个在canvas绘图的操作
context.drawImage(image, imgX, imgY, rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);

以及canvas转为file对象的操作
let formData = new FormData();
const file = new File([blob], data.selectFileName, {type: data.selectFileType})
formData.append(“file”, file);

关于其中的绘制区域的大小缩放以及移动,也算是一个小难点;一般也有另一种裁剪区域风格,即四条线风格,可通过代码进行理解

示例源码

AvatarUpload.vue

<template>
  <div class="avatar-container">
    <div class="img-container">
      <div v-if="!data.selectFile" class="select-file" @click="selectFile">
        <p>jpg/png file with a size less than 5MB<em>click to upload</em></p>
      </div>
      <img v-if="data.selectFile" :src="data.src" :style="{'height' : data.imgHeight }" alt="" class="img"
           draggable="false"/>
      <div v-if="data.selectFile" class="rectangle" @mousedown="dragStart($event)" @mousemove="changePos($event)"
           @mousewheel="wheel($event)"></div>
    </div>

    <canvas class="canvas" height="100" width="100"></canvas>
  </div>
</template>

<script>
import {onBeforeUnmount, onMounted, reactive} from "vue";
import {message} from "@/util/api";

export default {
  setup() {
    const data = reactive({
      src: null,
      imgHeight: "300px",
      selectFile: false,
      selectFileName: "",
      selectFileType: "",
    });

    async function selectFile() {
      const pickerOpts = {
        types: [
          {
            description: "Images",
            accept: {
              "image/*": [".png", ".jpeg", ".jpg"],
            },
          },
        ],
        excludeAcceptAllOption: true,
        multiple: false,
      };
      try {
        const fileHandle = await window.showOpenFilePicker(pickerOpts);
        const file = await fileHandle[0].getFile();
        data.selectFileName = file.name;
        data.selectFileType = file.type;

        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = function (e) {
          data.src = e.target.result;
          data.selectFile = true;

          image = new Image();
          image.src = e.target.result;
          setTimeout(() => {
            data.imgHeight = image.height + "px";

            rectangle = document.getElementsByClassName("rectangle")[0];
            rectangleWidth = rectangle.clientWidth;
            rectangleHeight = rectangle.clientHeight;

            imgWidth = image.width;
            imgHeight = image.height;

            rectangle.style.left = (imgWidth - rectangleWidth) / 2 + "px";
            rectangle.style.top = (imgHeight - rectangleHeight) / 2 + "px";

            context.drawImage(image, (image.width / 2 - rectangleWidth / 2), (image.height / 2 - rectangleHeight / 2),
                rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
          }, 0);
        };
      } catch (e) {
        if (!(e.name === 'AbortError' && e.message === 'The user aborted a request.')) {
          throw e;
        }
      }
    }

    let isDragging = false;
    let mouseUpListener;
    let imgWidth;
    let imgHeight;

    let rectangle;
    let initialX;
    let initialY;
    let rectangleWidth;
    let rectangleHeight;

    let canvas;
    let context;
    let image;

    onMounted(() => {
      mouseUpListener = () => {
        isDragging = false;
      }
      document.addEventListener("mouseup", mouseUpListener);

      canvas = document.getElementsByClassName("canvas")[0];
      context = canvas.getContext("2d");
    });

    onBeforeUnmount(() => {
      document.removeEventListener("mouseup", mouseUpListener);
    });

    function dragStart(e) {
      isDragging = true;

      rectangle = document.getElementsByClassName("rectangle")[0];
      rectangleWidth = rectangle.clientWidth;
      rectangleHeight = rectangle.clientHeight;

      initialX = e.clientX - rectangle.offsetLeft;
      initialY = e.clientY - rectangle.offsetTop;
    }

    function changePos(e) {
      if (!isDragging) {
        return;
      }

      const x = e.clientX - initialX;
      const y = e.clientY - initialY;

      if (x >= 0 && x < imgWidth - rectangleWidth) {
        rectangle.style.left = x + "px";
        context.drawImage(image, x, y, rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
      }
      if (y >= 0 && y < imgHeight - rectangleHeight) {
        rectangle.style.top = y + "px";
        context.drawImage(image, x, y, rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
      }
    }

    const gap = 2;
    const minRange = 50;
    let centerX;
    let centerY;

    function wheel(e) {
      if (!centerX) {
        centerX = image.width / 2;
      }
      if (!centerY) {
        centerY = image.height / 2;
      }

      if (e.deltaY > 0) {
        if (rectangleWidth + gap >= image.width || rectangleHeight + gap >= image.height) {
          return;
        }
        if ((centerX - rectangleWidth / 2 - gap < 0) || (centerY - rectangleHeight / 2 - gap < 0)) {
          return;
        }

        rectangleWidth += gap;
        rectangleHeight += gap;
        rectangle.style.width = rectangleWidth + "px";
        rectangle.style.height = rectangleHeight + "px";
      } else {
        if (rectangleWidth - gap < minRange || rectangleHeight - gap < minRange) {
          return;
        }
        rectangleWidth -= gap;
        rectangleHeight -= gap;
        rectangle.style.width = rectangleWidth + "px";
        rectangle.style.height = rectangleHeight + "px";
      }

      context.drawImage(image, (centerX - rectangleWidth / 2), (centerY - rectangleHeight / 2), rectangleWidth,
          rectangleHeight, 0, 0, canvas.width, canvas.height);
    }

    function getCanvasFile(resolve) {
      if (!data.selectFile) {
        message("请先选择图片", "warning");
        return;
      }
      canvas.toBlob((blob) => {
        let formData = new FormData();
        const file = new File([blob], data.selectFileName, {type: data.selectFileType})
        formData.append("file", file);
        resolve(formData);
      }, data.selectFileType);
    }

    return {
      data,
      selectFile,
      dragStart,
      changePos,
      wheel,
      getCanvasFile,
    }
  }
};
</script>

<style lang="scss" scoped>
.avatar-container {
  margin: 0 auto;
  width: fit-content;
  user-select: none;
  display: flex;
  justify-content: center;
  align-items: center;
  padding-top: 100px;

  .img-container {
    position: relative;
    width: fit-content;

    .select-file {
      width: 500px;
      height: 300px;
      border: 1px dashed #dcdfe6;
      border-radius: 20px;
      display: flex;
      justify-content: center;
      align-items: center;

      &:hover {
        border: 1px dashed #409eff;
        cursor: pointer;
      }

      p {
        font-size: 14px;
        color: #606266;

        em {
          color: #409eff;
          font-style: normal;
          margin-left: 5px;
        }
      }
    }

    .img {
      border: 1px dashed #409eff;
      height: 300px;
    }

    .rectangle {
      width: 100px;
      height: 100px;
      border: 1px dashed #409eff;
      position: absolute;
      z-index: 999;
      cursor: pointer;
    }
  }

  .canvas {
    margin-left: 30px;
    border: 1px dashed #409eff;
    float: left;
    border-radius: 50%;
  }
}
</style>

效果展示

在这里插入图片描述

关于裁剪区域的风格,设置为四条线可移动那种,需要改动一些代码,考虑后续补充

源码下载

参见Gitee链接(WEB-OS-SYSTEM)

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值