最近在做项目中需要裁剪图片上某一处位置的头像,这边就记录下我的实现的操作
演示效果:
直接上代码:
<template>
<div v-if="croppingUrl" class="model">
<h5 style="margin: 0 0 1rem;">{{title ? title : '裁剪图片'}}</h5>
<div style="display: flex;">
<div class="cutting" :style="{width: croppingImageSize.width + 'px', height: croppingImageSize.height + 'px'}">
<img :src="croppingUrl" alt="..." :style="{left: croppingImageAttr.x + 'px', top: croppingImageAttr.y + 'px', width: croppingImageAttr.width + 'px', height: croppingImageAttr.height + 'px'}" />
<div id="cropping-mask"></div>
<div class="region" :style="{'clip-path':
`polygon(
${croppingRegionAttr.x}px ${croppingRegionAttr.y}px,
${croppingRegionAttr.x + croppingRegionAttr.width}px ${croppingRegionAttr.y}px,
${croppingRegionAttr.x + croppingRegionAttr.width}px ${croppingRegionAttr.y + croppingRegionAttr.height}px,
${croppingRegionAttr.x}px ${croppingRegionAttr.y + croppingRegionAttr.height}px
)`}">
<img :src="croppingUrl" alt="..." :style="{left: croppingImageAttr.x + 'px', top: croppingImageAttr.y + 'px', width: croppingImageAttr.width + 'px', height: croppingImageAttr.height + 'px'}" />
<div id="cropping-region" :style="{width: croppingRegionAttr.width + 'px', height: croppingRegionAttr.height + 'px', left: croppingRegionAttr.x + 'px', top: croppingRegionAttr.y + 'px'}"></div>
</div>
</div>
<div style="margin-left: 1rem;">
<div style="margin-bottom: 1rem;">预览</div>
<div style="background-repeat: no-repeat;" :style="{
'background-image': `url(${croppingUrl})`,
width: `${croppingRegionAttr.width}px`,
height: `${croppingRegionAttr.height}px`,
'background-size': `${croppingImageAttr.width}px ${croppingImageAttr.height}px`,
'background-position': `-${croppingRegionAttr.x}px -${croppingRegionAttr.y}px`
}">
</div>
</div>
</div>
<button @click="clickSave" style="margin-top: 10px;">保存</button>
</div>
</template>
<script lang="ts">
import {defineComponent, onUnmounted, reactive, toRefs, watch} from 'vue';
export default defineComponent({
props: {
/** 弹框标题 */
title: {
type: String,
default: ""
},
/** 需要裁剪的图片文件 */
file: {
type: File|Object,
default: null,
}
},
setup(props, { emit }) {
const state = reactive({
// 裁剪图片弹框标题
title: props.title,
// 当前是否允许拖拽 - 裁剪区域
isCroppingRegionDrag: false,
// 当前是否允许拖拽 - 裁剪图片
isCroppingImgDrag: false,
// 裁剪区域可移动的范围
croppingImageSize: {
width: 400,
height: 400 / 1.59,
},
// 裁剪区域的属性
croppingRegionAttr: {
width: 145,
height: 180,
x: 230,
y: 25,
},
// 记录上一次的位置
recordPrevPosition: null as any,
// 记录裁剪的图片文件
recordCroppingFile: null as any,
// 裁剪图片的url
croppingUrl: "" as any,
// 被裁剪图片的属性
croppingImageAttr: {
width: 0,
height: 0,
x: 0,
y: 0,
},
// 裁剪后的图片 - base64
croppingAfterImage: "",
});
watch(() => props.file, async newVal => {
state.recordCroppingFile = newVal;
if(newVal) {
state.croppingUrl = await getBase64ByFile(state.recordCroppingFile);
nextTick(() => init());
}
});
onUnmounted(() => {
destroy();
});
/** 初始化操作 */
const init = () => {
document.getElementById("cropping-region")?.addEventListener("mousedown", croppingRegionOnmousedown);
document.getElementById("cropping-region")?.addEventListener("mousemove", croppingRegionOnmousemove);
document.getElementById("cropping-mask")?.addEventListener("mousedown", croppingImgOnmousedown);
document.getElementById("cropping-mask")?.addEventListener("mousemove", croppingImgOnmousemove);
document.addEventListener("mouseup", onmouseup);
}
/** 结束的操作 */
const destroy = () => {
document.getElementById("cropping-region")?.removeEventListener("mousedown", croppingRegionOnmousedown);
document.getElementById("cropping-region")?.removeEventListener("mousemove", croppingRegionOnmousemove);
document.getElementById("cropping-mask")?.removeEventListener("mousedown", croppingImgOnmousedown);
document.getElementById("cropping-mask")?.removeEventListener("mousemove", croppingImgOnmousemove);
document.removeEventListener("mouseup", onmouseup);
}
/** 监听鼠标按下事件 - 裁剪区域 */
const croppingRegionOnmousedown = event => {
event.preventDefault();
state.isCroppingRegionDrag = true;
}
/** 监听鼠标移动事件 - 裁剪区域 */
const croppingRegionOnmousemove = event => {
if(state.isCroppingRegionDrag && state.croppingUrl) {
const { clientX, clientY } = event;
let poorX = 0;
let poorY = 0;
// 获取移动的差
if(state.recordPrevPosition) {
poorX = clientX - state.recordPrevPosition.x;
poorY = clientY - state.recordPrevPosition.y;
}
// 记录移动的位置
state.recordPrevPosition = { x: clientX, y: clientY };
// 判断是否允许移动
const { x, y, width, height } = state.croppingRegionAttr;
// 设置移动的值
state.croppingRegionAttr.x = x + poorX < 0 ? 0 : x + poorX + width > state.croppingImageSize.width ? state.croppingImageSize.width - width : x + poorX;
state.croppingRegionAttr.y = y + poorY < 0 ? 0 : y + poorY + height > state.croppingImageSize.height ? state.croppingImageSize.height - height : y + poorY;
}
}
/** 监听鼠标抬起事件 */
const onmouseup = () => {
state.isCroppingRegionDrag = false;
state.isCroppingImgDrag = false;
state.recordPrevPosition = null;
}
/** 监听鼠标按下事件 - 裁剪图片 */
const croppingImgOnmousedown = event => {
event.preventDefault();
state.isCroppingImgDrag = true;
}
/** 监听鼠标移动事件 - 裁剪图片 */
const croppingImgOnmousemove = event => {
if(state.isCroppingImgDrag && state.croppingUrl) {
const { clientX, clientY } = event;
let poorX = 0;
let poorY = 0;
// 获取移动的差
if(state.recordPrevPosition) {
poorX = clientX - state.recordPrevPosition.x;
poorY = clientY - state.recordPrevPosition.y;
}
// 记录移动的位置
state.recordPrevPosition = { x: clientX, y: clientY };
// 判断是否允许移动...
const { x, y, width, height } = state.croppingImageAttr;
if(width >= height && x + poorX <= 0 && x + poorX >= -width + state.croppingImageSize.width) {
state.croppingImageAttr.x = x + poorX ;
}
else if(width < height && y + poorY <= 0 && y + poorY >= -height + state.croppingImageSize.height) {
state.croppingImageAttr.y = y + poorY;
}
}
}
/** 将File类型转换为base64 */
const getBase64ByFile = file => {
return new Promise(resolve => {
// 获取FileReader对象
const reader = new FileReader() as any;
// 读取完成后的回调
reader.onload = ({ target: { result: src } }) => {
// 创建图片对象
const image = new Image();
// 设置路径
image.src = src;
// 图片加载完成后
image.onload = () => {
// 记录裁剪图片的尺寸...
let ratio = image.height >= image.width ? image.width / state.croppingImageSize.width : image.height / state.croppingImageSize.height;
state.croppingImageAttr = {x: 0, y: 0, width: image.width / ratio, height: image.height / ratio};
// 创建canvas
const canvas = document.createElement('canvas');
// 设置canvas的宽高...
canvas.width = image.width;
canvas.height = image.height;
// 获取上下文对象
const context = canvas.getContext('2d') as any;
// 绘制图片
context.drawImage(image, 0, 0, image.width, image.height);
// 返回图片的url
resolve(canvas.toDataURL(file.type));
}
};
// 读取文件
reader.readAsDataURL(file);
});
}
/** 裁剪头像 */
const croppingHeadImage = () => {
return new Promise(resolve => {
// 创建图片对象
const image = new Image();
// 设置路径
image.src = state.croppingUrl;
// 图片加载完成后
image.onload = () => {
// 获取裁剪的数据
const { x: region_x, y: region_y, width, height } = state.croppingRegionAttr;
const { x: img_x, y: img_y } = state.croppingImageAttr;
// 图片压缩的比例
let ratio = image.height >= image.width ? image.width / state.croppingImageSize.width : image.height / state.croppingImageSize.height;
// 创建canvas
const canvas = document.createElement('canvas');
// 设置canvas的宽高...
canvas.width = width;
canvas.height = height;
// 获取上下文对象
const context = canvas.getContext('2d') as any;
// 绘制图片
context.drawImage(image, (region_x + Math.abs(img_x)) * ratio, (region_y + Math.abs(img_y)) * ratio, width * ratio, height * ratio, 0, 0, canvas.width, canvas.height);
// 得到base64
const base64 = canvas.toDataURL(state.recordCroppingFile);
// 转换文件...
const buffer = atob(base64.split(',')[1]);
let length = buffer.length;
const bufferArray = new Uint8Array(new ArrayBuffer(length));
while (length--) {
bufferArray[length] = buffer.charCodeAt(length);
}
// 保存裁剪后的base64
state.croppingAfterImage = base64;
// 返回...
resolve(new File([bufferArray], state.recordCroppingFile.name, { type: state.recordCroppingFile }));
}
});
}
/** 点击保存按钮 */
const clickSave = async () => {
const file = await croppingHeadImage();
emit("croppingAfter", { file, base64: state.croppingAfterImage});
state.croppingUrl = "";
state.croppingAfterImage = "";
destroy();
}
return {
...toRefs(state),
clickSave,
}
}
});
</script>
<style scoped lang="scss">
.cutting {
position: relative;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
img {
position: absolute;
}
}
#cropping-region {
position: absolute;
cursor: pointer;
background-origin: content-box;
background-image:
linear-gradient(#212529 0.125rem, transparent 0.125rem, transparent calc(100% - 0.125rem), #212529 calc(100% - 0.125rem), #212529 100%),
linear-gradient(90deg, #212529 2px, transparent 0.125rem, transparent calc(100% - 0.125rem), #212529 calc(100% - 0.125rem), #212529 100%),
linear-gradient(#212529 0.125rem, transparent 0.125rem, transparent calc(100% - 0.125rem), #212529 calc(100% - 0.125rem), #212529 100%),
linear-gradient(90deg, #212529 0.125rem, transparent 0.125rem, transparent calc(100% - 0.125rem), #212529 calc(100% - 0.125rem), #212529 100%);
background-repeat: no-repeat;
background-position: top left, top left, bottom right, bottom right;
background-size: 0.625rem 100%, 100% 0.625rem;
}
#cropping-mask, .region {
position: absolute;
width: 100%;
height: 100%;
}
#cropping-mask {
background: rgba(255, 255, 255, .6);
}
.model {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border-radius: 0.375rem;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);
padding: 1.25rem;
}
</style>
使用:
<template>
<div style="margin-bottom: 10px;">
<text>快上传:</text>
<input :value="data" @change="uploadFile" type="file" />
</div>
<img :src="base64" alt="..." />
<croppingImage @croppingAfter="croppingAfter" :file="file" />
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive } from 'vue';
import croppingImage from "@/components/croppingImage/cropping-image.vue";
export default defineComponent({
components: {
croppingImage,
},
setup() {
const state = reactive({
file: null as any,
data: null,
base64: "",
});
/** 上传图片 */
const uploadFile = async event => {
state.file = event.target.files[0];
// 代码为了防止change事件同文件不触发,如果用组件库上传可以删除代码,忽略
nextTick(() => {
state.data = null;
console.log(state.data)
})
}
const croppingAfter = data => {
state.file = null;
state.base64 = data.base64;
console.log(data);
}
return {
...toRefs(state),
uploadFile,
croppingAfter,
}
}
});
</script>
<style scoped lang="scss">
</style>
就这样!🤪