移动端图片裁剪 vue3+Cropper.js

924 阅读3分钟

前言

这段时间开发的 h5 项目,需要上传人脸照片,上传图片不可避免需要用到图片裁剪,简单记录一下开发过程

准备

项目使用 vite 脚手架创建的 vue3 项目,裁剪插件使用的是 Cropper.js,版本 v1.6.2

npm install cropperjs

功能实现

要实现图片裁剪大致流程就是显示图像视频流,点击拍照后绘制当前视频图像,最后使用裁剪插件编辑图片,确定裁剪输出最终图片

视频流

使用video创建视频流,当使用前置摄像头时,需要在 x 轴方向反转画面,避免镜像反射(style),拍照后需要隐藏video(class)

<!-- 视频预览 -->
<video
  ref="videoRef"
  class="video-preview"
  autoplay
  playsinline
  :style="{ transform: state.isFrontCamera ? 'scaleX(-1)' : 'none' }"
  :class="{ hidden: state.imgUrl }"
  ></video>

画布(canvas)

点击拍照后,使用canvas绘制当前视频图像,这个canvas仅用来绘制图像,不需要在页面显示,需要隐藏

<!-- 拍照画布 -->
<canvas ref="canvasRef" class="photo-canvas" style="display: none"></canvas>

裁剪(Cropper.js)

引入依赖

<template>
  <!-- 裁剪区域 -->
  <div v-show="state.imgUrl" class="cropper-container">
    <img ref="cropperRef" :src="state.imgUrl" class="cropper-img" alt="拍摄结果" />
  </div>
</template>

<script setup lang="ts">
  import 'cropperjs/dist/cropper.css'
  import Cropper from 'cropperjs'
</script>

代码实现

进入拍照页面,初始化摄像头

const initCamera = async () => {
  try {
    // 先停止之前的视频流
    if (state.mediaStream) {
      state.mediaStream.getTracks().forEach((track) => track.stop())
    }

    state.mediaStream = await navigator.mediaDevices.getUserMedia({
      video: {
        facingMode: state.isFrontCamera ? 'user' : 'environment',
        width: { ideal: 1280 },
        height: { ideal: 720 },
      },
    })

    if (videoRef.value) {
      videoRef.value.srcObject = state.mediaStream
    }
  } catch (error) {
    console.error('摄像头访问失败:', error)
    alert('无法访问摄像头,请检查权限设置')
  }
}

点击拍照按钮,进行拍照。拍照完成后,初始化cropper对象,setCropBoxData设置裁剪框绝对位置和宽高,其他配置参数可以参考Cropper.js文档

const takePhoto = () => {
  if (!videoRef.value || !canvasRef.value) return

  const video = videoRef.value
  const canvas = canvasRef.value

  // 计算适当的缩放比例以保持宽高比
  const videoAspectRatio = video.videoWidth / video.videoHeight
  const screenAspectRatio = window.innerWidth / window.innerHeight

  let drawWidth,
    drawHeight,
    offsetX = 0,
    offsetY = 0

  if (videoAspectRatio > screenAspectRatio) {
    // 视频更宽,以高度为基准
    drawHeight = window.innerHeight
    drawWidth = drawHeight * videoAspectRatio
    offsetX = (drawWidth - window.innerWidth) / 2
  } else {
    // 视频更高,以宽度为基准
    drawWidth = window.innerWidth
    drawHeight = drawWidth / videoAspectRatio
    offsetY = (drawHeight - window.innerHeight) / 2
  }

  // 设置画布尺寸为屏幕尺寸
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight

  const ctx = canvas.getContext('2d')
  if (!ctx) return

  // 处理前置摄像头的镜像效果
  if (state.isFrontCamera) {
    ctx.translate(canvas.width, 0)
    ctx.scale(-1, 1)
  }

  // 绘制时考虑偏移量,确保图像居中
  ctx.drawImage(video, -offsetX, -offsetY, drawWidth, drawHeight)

  // 如果是前置摄像头,恢复画布变换
  if (state.isFrontCamera) {
    ctx.setTransform(1, 0, 0, 1, 0, 0)
  }

  state.imgUrl = canvas.toDataURL('image/jpeg', 0.8)
  stopCamera()

  // 初始化裁剪器
  nextTick(() => {
    if (cropperRef.value) {
      state.cropper = new Cropper(cropperRef.value, {
        viewMode: 0,
        dragMode: 'move',
        aspectRatio: 1,
        modal: true,
        guides: true,
        restore: false,
        cropBoxMovable: false,
        // cropBoxResizable: false,
        // toggleDragModeOnDblclick: false,
        autoCropArea: 1,
        // minContainerWidth: window.innerWidth,
        // minContainerHeight: window.innerHeight,
        ready() {
          state.cropper.setCropBoxData({
            width: 300,
            height: 300,
            left: (this.cropper.containerData.width - 300) / 2,
            top: 125,
          })
        },
      })
    }
  })
}

确认裁剪,输出base64格式图片

const handleConfirm = () => {
  if (state.cropper) {
    const canvas = state.cropper.getCroppedCanvas({
      width: 300, // 设置裁剪后的尺寸
      height: 300,
    })
    const croppedImage = canvas.toDataURL('image/jpeg', 0.8)
    state.imgUrl = ''
  }
}

一些建议

  • 自定义裁剪框样式

如果自定义了裁剪框样式,同时禁止裁剪框缩放。这时候当设置cropBoxResizable:true禁止缩放,会发现裁剪框的样式不见了,看 css 可以知道当禁止缩放时候,裁剪框样式类发生变化,新增了如下样式

.cropper-hidden {
    display: none !important;
}

裁剪框缩放是通过拖拉边框实现的,我们可以设置允许缩放,在元素交互设置 css 禁止事件,需要同时设置 8 个方向的样式

:deep(.cropper-point.point-n),
:deep(.cropper-point.point-s) {
  margin-left: -11px; /* 水平居中 */
  width: 22px;
  height: 5px;
  pointer-events: none;
}

这样就可以再保持裁剪框样式的前提下,同时禁止裁剪框缩放

总结

图片裁剪大致实现思路就是这样,核心代码也不是很多,如果有其他疑问,欢迎交流🎉