使用canvas图片裁剪

最近在做项目中需要裁剪图片上某一处位置的头像,这边就记录下我的实现的操作

演示效果:

 直接上代码:

<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>

就这样!🤪

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值