前言
这段时间开发的 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;
}
这样就可以再保持裁剪框样式的前提下,同时禁止裁剪框缩放
总结
图片裁剪大致实现思路就是这样,核心代码也不是很多,如果有其他疑问,欢迎交流🎉