uniapp realizes the free rotation and cropping function of the picture finger (custom cropping area)

1. Create a new public component components / cropping / cropping.vue and copy the code into it

 Copy the code into cropping.vue

<template name="yq-avatar">
	<view class="croppage">
		<canvas canvas-id="avatar-canvas" id="avatar-canvas" class="my-canvas" :style="{top: sT, height: csH}"
			:disable-scroll="false"></canvas>
		<canvas canvas-id="oper-canvas" id="oper-canvas" class="oper-canvas" :style="{top: sT1, height: csH1}"
			:disable-scroll="false" @touchstart="fStart" @touchmove="fMove" @touchend="fEnd">
		</canvas>
		<canvas canvas-id="prv-canvas" id="prv-canvas" class="prv-canvas" :disable-scroll="false" @touchstart="fHideImg"
			:style="{ height: csH, top: pT }">
		</canvas>
		<view class="oper-wrapper" :style="{top:tp}">
			<view class="oper">
				<view class="btn-wrapper">
					<view @click="rechoose" hover-class="hover" :style="{width: bW}"><text>重选</text></view>
					<view @click="fUpload" hover-class="hover" :style="{width: bW}"><text>确定</text></view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
	const tH = 50;
	export default {
		data() {
			return {
				sT1num: 0,
				csH1: '0px',
				sT1: 0,
				csH: '0px',
				sD: 'none',
				sT: '-10000px',
				pT: '-10000px',
				iS: {},
				sS: {},
				bW: '19%',
				bD: 'flex',
				tp: 0,
			};
		},
		props: {
			avatarSrc: '',
			avatarStyle: '',
			selWidth: '',
			selHeight: '',
			expWidth: '',
			expHeight: '',
			minScale: '',
			maxScale: '',
			canScale: '',
			canRotate: '',
			lockWidth: '',
			lockHeight: '',
			stretch: '',
			lock: '',
			fileType: '',
			noTab: '',
			inner: '',
			quality: '',
			index: '',
			bgImage: '',
		},
		created() {
			this.cc = uni.createCanvasContext('avatar-canvas', this);
			this.cco = uni.createCanvasContext('oper-canvas', this);
			this.ccp = uni.createCanvasContext('prv-canvas', this);
			this.qlty = parseFloat(this.quality) || 1; //画质
			this.letRotate = (this.canRotate === false || this.inner === true || this.inner === 'true' || this
				.canRotate === 'false') ? 0 : 1; //是否旋转
			this.letScale = (this.canScale === false || this.canScale === 'false') ? 0 : 1; //是否放大
			this.isin = (this.inner === true || this.inner === 'true') ? 1 : 0;
			this.indx = this.index || undefined; //禁止旋转并在图片范围内移动
			this.mnScale = parseFloat(this.minScale) || 0.3; //缩小比例
			this.mxScale = parseFloat(this.maxScale) || 4; //放大比列
			this.noBar = (this.noTab === true || this.noTab === 'true') ? 1 : 0; //是否存在nobar
			this.stc = this.stretch; //自动铺满
			this.lck = this.lock; //锁定图方向 x y long short longSel shortSel
			this.fType = this.fileType === 'jpg' ? 'jpg' : 'png';
			if (this.isin || !this.letRotate) {
				this.bW = '24%';
				this.bD = 'none';
			} else {
				this.bW = '19%';
				this.bD = 'flex';
			}
			if (this.noBar) {
				this.fWindowResize();
			} else {
				uni.showTabBar({
					fail: () => {
						this.noBar = 1;
					},
					success: () => {
						this.noBar = 0;
					},
					complete: (res) => {
						this.fWindowResize();
					}
				});
			}
		},
		methods: {
			//重选
			rechoose() {
				const that = this
				uni.chooseImage({
					count: 1,
					sizeType: ['original', 'compressed'],
					sourceType: ["album"],
					success: (res) => {
						that.sT1 = that.sT
						that.csH1 = that.csH
						that.fSelect(res.tempFilePaths[0])
					}
				});
			},
			//初始化各种数据
			fWindowResize() {
				let sysInfo = uni.getSystemInfoSync();
				this.platform = sysInfo.platform;
				this.wW = sysInfo.windowWidth;
				// #ifndef H5
				let phone = uni.getSystemInfoSync().platform
				if (phone == 'ios') {
					let h = uni.upx2px(60); //将rpx单位值转换成px 裁剪框自己
					let b = uni.upx2px(100); //底部按钮
					this.sT1 = (sysInfo.windowHeight - h - b) / 2 + 'px' //裁剪框距离顶部+px
					this.sT1num = (sysInfo.windowHeight - h - b) / 2 //裁剪框距离顶部不+px
				} else {
					this.drawTop = 0;
				}
				// #endif
				// #ifndef MP-ALIPAY
				this.wH = sysInfo.windowHeight; //设备高
				if (!this.noBar) this.wH += tH; //th=50
				this.csH = this.wH - tH + 'px'; //高=设备高-50(导航)
				if (phone == 'ios') { //适配ios
					this.csH1 = (this.wH - 50) / 2 + 'px'
				} else {
					this.csH1 = this.wH - tH + 'px';
				}
				// #endif
				this.tp = this.csH;
				this.pxRatio = this.wW / 750; //设备宽/750  比列
				this.expWidth && (this.eW = this.expWidth.toString().indexOf('upx') >= 0 ? parseInt(this.expWidth) * this
					.pxRatio : parseInt(this.expWidth));
				this.expHeight && (this.eH = this.expHeight.toString().indexOf('upx') >= 0 ? parseInt(this.expHeight) *
					this.pxRatio : parseInt(this.expHeight));
				this.fHideImg();
			},
			fSelect(r) {
				if (this.fSelecting) return;
				this.fSelecting = true;
				setTimeout(() => {
					this.fSelecting = false;
				}, 500); //防抖
				let path = this.imgPath = r; //需要剪裁的图片路径
				// 获取图片信息
				uni.getImageInfo({
					src: path,
					success: r => {
						this.imgWidth = r.width;
						this.imgHeight = r.height;
						this.path = path;
						if (!this.hasSel) {
							let style = this.sS || {};
							if (this.selWidth && this.selHeight) { //设置的剪裁区域宽高
								let sW = this.selWidth.toString().indexOf('upx') >= 0 ?
									parseInt(this.selWidth) * this.pxRatio : parseInt(
										this.selWidth),
									sH = this.selHeight.toString().indexOf('upx') >= 0 ?
									parseInt(this.selHeight) * this.pxRatio : parseInt(
										this.selHeight);
								style.width = sW + 'px';
								style.height = sH + 'px';
								style.top = ((this.wH - sH - tH) | 0) / 2 + 'px';
								style.left = ((this.wW - sW) | 0) / 2 + 'px';
								// }
							} else {
								uni.showModal({
									title: '裁剪框的宽或高没有设置',
									showCancel: false
								})
								return;
							}
							this.sS = style;
						}
						if (this.noBar) {
							setTimeout(() => {
								this.fDrawInit(true);
							}, 200)
						} else {
							uni.hideTabBar({
								complete: () => {
									setTimeout(() => {
										this.fDrawInit(true);
									}, 200)
								}
							});
						}
					},
					fail: () => {
						uni.showToast({
							title: "请选择正确图片",
							duration: 2000,
						})
					},
					complete() {
						uni.hideLoading();
					}
				});
			},
			//剪裁确定
			fUpload() {
				uni.showLoading({
					title: '图片上传中...',
					mask: true
				});
				if (this.fUploading) return;
				this.fUploading = true;
				setTimeout(() => {
					this.fUploading = false;
				}, 1000)
				let style = this.sS,
					x = parseInt(style.left),
					y = parseInt(style.top),
					width = parseInt(style.width),
					height = parseInt(style.height),
					expWidth = this.eW || (width * this.pixelRatio),
					expHeight = this.eH || (height * this.pixelRatio);
				this.fHideImg();
				// #ifndef MP-ALIPAY
				let phone = uni.getSystemInfoSync().platform
				if (phone == 'ios') {
					y = this.sT1num
				}
				uni.canvasToTempFilePath({
					x: x,
					y: y,
					width: width,
					height: height,
					destWidth: expWidth,
					destHeight: expHeight,
					canvasId: 'avatar-canvas',
					fileType: this.fType,
					quality: this.qlty,
					success: (r) => {
						r = r.tempFilePath;
						// #ifndef H5
						this.$emit("upload", {
							avatar: this.imgSrc,
							path: r,
							index: this.indx,
							data: this.rtn,
							base64: this.base64 || null
						});
						// #endif
					},
					fail: (res) => {
						uni.showToast({
							title: "error1",
							duration: 2000,
						})
					},
					complete: () => {
						uni.hideLoading();
						this.noBar || uni.showTabBar();
						this.$emit("end");
					}
				}, this);
				// #endif
			},
			fDrawInit(ini = false) {
				let allWidth = this.wW, //设备宽
					allHeight = this.wH, //设备高
					imgWidth = this.imgWidth, //图宽
					imgHeight = this.imgHeight, //图高
					imgRadio = imgWidth / imgHeight, //图比
					useWidth = allWidth - 40, //设备宽-40
					useHeight = allHeight - tH - 80, //设备高-80
					useRadio = useWidth / useHeight,
					sW = parseInt(this.sS.width), //图片信息
					sH = parseInt(this.sS.height);
				this.fixWidth = 0;
				this.fixHeight = 0;
				this.lckWidth = 0;
				this.lckHeight = 0;
				switch (this.stc) { //以下是自动铺满的算法
					case 'x':
						this.fixWidth = 1;
						break;
					case 'y':
						this.fixHeight = 1;
						break;
					case 'long':
						if (imgRadio > 1) this.fixWidth = 1;
						else this.fixHeight = 1;
						break;
					case 'short':
						if (imgRadio > 1) this.fixHeight = 1;
						else this.fixWidth = 1;
						break;
					case 'longSel':
						if (sW > sH) this.fixWidth = 1;
						else this.fixHeight = 1;
						break;
					case 'shortSel':
						if (sW > sH) this.fixHeight = 1;
						else this.fixWidth = 1;
						break;
				}
				switch (this.lck) { //以下锁定屏幕的移动方向
					case 'x':
						this.lckWidth = 1;
						break;
					case 'y':
						this.lckHeight = 1;
						break;
					case 'long':
						if (imgRadio > 1) this.lckWidth = 1;
						else this.lckHeight = 1;
						break;
					case 'short':
						if (imgRadio > 1) this.lckHeight = 1;
						else this.lckWidth = 1;
						break;
					case 'longSel':
						if (sW > sH) this.lckWidth = 1;
						else this.lckHeight = 1;
						break;
					case 'shortSel':
						if (sW > sH) this.lckHeight = 1;
						else this.lckWidth = 1;
						break;
				}
				if (this.fixWidth) {
					useWidth = sW;
					useHeight = useWidth / imgRadio;
				} else if (this.fixHeight) {
					useHeight = sH;
					useWidth = useHeight * imgRadio;
				} else if (imgRadio < useRadio) {
					if (imgHeight < useHeight) {
						useWidth = imgWidth;
						useHeight = imgHeight;
					} else {
						useWidth = useHeight * imgRadio;
					}
				} else {
					if (imgWidth < useWidth) {
						useWidth = imgWidth;
						useHeight = imgHeight;
					} else {
						useHeight = useWidth / imgRadio;
					}
				}
				if (this.isin) {
					if (useWidth < sW) {
						useWidth = sW;
						useHeight = useWidth / imgRadio;
						this.lckHeight = 0;
					}
					if (useHeight < sH) {
						useHeight = sH;
						useWidth = useHeight * imgRadio;
						this.lckWidth = 0;
					}
				}
				this.scaleSize = 1;
				this.rotateDeg = 0;
				this.posWidth = (allWidth - useWidth) / 2 | 0;
				this.posHeight = (allHeight - useHeight - tH) / 2 | 0;
				this.useWidth = useWidth | 0;
				this.useHeight = useHeight | 0;
				this.centerX = this.posWidth + useWidth / 2;
				this.centerY = this.posHeight + useHeight / 2;
				this.focusX = 0;
				this.focusY = 0;
				let style = this.sS,
					left = parseInt(style.left),
					top = parseInt(style.top),
					width = parseInt(style.width),
					height = parseInt(style.height),
					canvas = this.canvas,
					canvasOper = this.canvasOper,
					cc = this.cc, //avatar-canvas
					cco = this.cco; //oper-canvas  裁剪
				cco.beginPath(); //开始创建一个路径
				cco.setLineWidth(3); //设置线条的宽度  px
				cco.setGlobalAlpha(1); //设置全局画笔透明度。
				cco.setStrokeStyle('white'); //设置边框颜色。如果没有设置 fillStyle,默认颜色为 black。
				cco.strokeRect(left, top, width, height); //画一个矩形(非填充)。用 setFillStroke() 设置边框颜色,如果没设置默认是黑色。
				cco.setFillStyle('black');
				cco.setGlobalAlpha(0.5);
				cco.fillRect(0, 0, this.wW, top); //填充一个矩形
				cco.fillRect(0, top, left, height);
				cco.fillRect(0, top + height, this.wW, this.wH - height - top - tH);
				cco.fillRect(left + width, top, this.wW - width - left, height);
				cco.setGlobalAlpha(1);
				cco.setStrokeStyle('red');
				cco.moveTo(left + 15, top); //把路径移动到画布中的指定点,不创建线条。用 stroke() 方法来画线条。
				cco.lineTo(left, top); //增加一个新点,然后创建一条从上次指定点到目标点的线。
				cco.lineTo(left, top + 15);
				cco.moveTo(left + width - 15, top);
				cco.lineTo(left + width, top);
				cco.lineTo(left + width, top + 15);
				cco.moveTo(left + 15, top + height);
				cco.lineTo(left, top + height);
				cco.lineTo(left, top + height - 15);
				cco.moveTo(left + width - 15, top + height);
				cco.lineTo(left + width, top + height);
				cco.lineTo(left + width, top + height - 15);
				cco.stroke(); //画线条
				cco.draw(false, () => { //将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中。
					if (ini) {
						this.sT = this.drawTop + 'px';
						this.fDrawImage(true); //绘制背景色
					}
				});
				this.$emit("init");
			},
			//绘制背景色
			fDrawImage(ini = false) {
				let tm_now = Date.now(); //当前时间
				if (tm_now - this.drawTm < 20) return;
				this.drawTm = tm_now;
				let cc = this.cc,
					imgWidth = this.useWidth * this.scaleSize,
					imgHeight = this.useHeight * this.scaleSize;
				if (this.bgImage) { //如果背景图
					// #ifndef MP-ALIPAY
					cc.drawImage(this.bgImage, 0, 0, this.wW, this.wH - tH); //绘制图像到画布。
					// #endif
				} else {
					cc.fillRect(0, 0, this.wW, this.wH - tH); //填充一个矩形
				}
				if (this.isin) { //禁止旋转并在图片范围内移动
					let cx = this.focusX * (this.scaleSize - 1),
						cy = this.focusY * (this.scaleSize - 1);
					cc.translate(this.centerX, this.centerY);
					cc.rotate(this.rotateDeg * Math.PI / 180);
					cc.drawImage(this.imgPath, this.posWidth - this.centerX - cx, this.posHeight - this.centerY - cy,
						imgWidth, imgHeight);
				} else {
					cc.translate(this.posWidth + imgWidth / 2, this.posHeight + imgHeight / 2);
					cc.rotate(this.rotateDeg * Math.PI / 180);
					cc.drawImage(this.imgPath, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
				}
				cc.draw(false);
			},
			//旋转
			fRotate() {
				this.rotateDeg += 90 - this.rotateDeg % 90;
				this.fDrawImage();
			},
			//手指触摸开始
			fStart(e) {
				let touches = e.touches,
					touch0 = touches[0],
					touch1 = touches[1];
				this.touch0 = touch0;
				this.touch1 = touch1;
				if (touch1) {
					let x = touch1.x - touch0.x,
						y = touch1.y - touch0.y;
					this.fgDistance = Math.sqrt(x * x + y * y);
				}
			},
			//手指触摸后移动
			fMove(e) {
				let touches = e.touches,
					touch0 = touches[0],
					touch1 = touches[1];
				if (touch1) {
					let x = touch1.x - touch0.x,
						y = touch1.y - touch0.y,
						fgDistance = Math.sqrt(x * x + y * y),
						scaleSize = 0.005 * (fgDistance - this.fgDistance),
						beScaleSize = this.scaleSize + scaleSize;
					do {
						if (!this.letScale) break;
						if (beScaleSize < this.mnScale) break;
						if (beScaleSize > this.mxScale) break;
						let growX = this.useWidth * scaleSize / 2,
							growY = this.useHeight * scaleSize / 2;
						if (this.isin) {
							let imgWidth = this.useWidth * beScaleSize,
								imgHeight = this.useHeight * beScaleSize,
								l = this.posWidth - growX,
								t = this.posHeight - growY,
								r = l + imgWidth,
								b = t + imgHeight,
								left = parseInt(this.sS.left),
								top = parseInt(this.sS.top),
								width = parseInt(this.sS.width),
								height = parseInt(this.sS.height),
								right = left + width,
								bottom = top + height,
								cx, cy;
							if (imgWidth <= width || imgHeight <= height) break;
							this.cx = cx = this.focusX * beScaleSize - this.focusX,
								this.cy = cy = this.focusY * beScaleSize - this.focusY;
							this.posWidth -= growX;
							this.posHeight -= growY;
							if (this.posWidth - cx > left) {
								this.posWidth = left + cx;
							}
							if (this.posWidth + imgWidth - cx < right) {
								this.posWidth = right - imgWidth + cx;
							}
							if (this.posHeight - cy > top) {
								this.posHeight = top + cy;
							}
							if (this.posHeight + imgHeight - cy < bottom) {
								this.posHeight = bottom - imgHeight + cy;
							}
						} else {
							this.posWidth -= growX;
							this.posHeight -= growY;
						}
						this.scaleSize = beScaleSize;
					} while (0);
					this.fgDistance = fgDistance;
					if (touch1.x !== touch0.x && this.letRotate) {
						x = (this.touch1.y - this.touch0.y) / (this.touch1.x - this.touch0.x);
						y = (touch1.y - touch0.y) / (touch1.x - touch0.x);
						this.rotateDeg += Math.atan((y - x) / (1 + x * y)) * 180 / Math.PI;
						this.touch0 = touch0;
						this.touch1 = touch1;
					}
					this.fDrawImage();
				} else if (this.touch0) {
					let x = touch0.x - this.touch0.x,
						y = touch0.y - this.touch0.y,
						beX = this.posWidth + x,
						beY = this.posHeight + y;
					if (this.isin) {
						let imgWidth = this.useWidth * this.scaleSize,
							imgHeight = this.useHeight * this.scaleSize,
							l = beX,
							t = beY,
							r = l + imgWidth,
							b = t + imgHeight,
							left = parseInt(this.sS.left),
							top = parseInt(this.sS.top),
							right = left + parseInt(this.sS.width),
							bottom = top + parseInt(this.sS.height),
							cx, cy;
						this.cx = cx = this.focusX * this.scaleSize - this.focusX;
						this.cy = cy = this.focusY * this.scaleSize - this.focusY;
						if (!this.lckWidth && Math.abs(x) < 100) {
							if (left < l - cx) {
								this.posWidth = left + cx;
							} else if (right > r - cx) {
								this.posWidth = right - imgWidth + cx;
							} else {
								this.posWidth = beX;
								this.focusX -= x;
							}
						}
						if (!this.lckHeight && Math.abs(y) < 100) {
							if (top < t - cy) {
								this.focusY -= (top + cy - this.posHeight);
								this.posHeight = top + cy;
							} else if (bottom > b - cy) {
								this.focusY -= (bottom + cy - (this.posHeight + imgHeight));
								this.posHeight = bottom - imgHeight + cy;
							} else {
								this.posHeight = beY;
								this.focusY -= y;
							}
						}
					} else {
						if (Math.abs(x) < 100 && !this.lckWidth) this.posWidth = beX;
						if (Math.abs(y) < 100 && !this.lckHeight) this.posHeight = beY;
						this.focusX -= x;
						this.focusY -= y;
					}
					this.touch0 = touch0;
					this.fDrawImage();
				}
			},
			//手指触摸动作结束
			fEnd(e) {
				let touches = e.touches,
					touch0 = touches && touches[0],
					touch1 = touches && touches[1];
				if (touch0) {
					this.touch0 = touch0;
				} else {
					this.touch0 = null;
					this.touch1 = null;
				}
			},
			fHideImg() {
				this.prvImg = '';
				this.pT = '-10000px';
				this.prvImgData = null;
				this.target = null;
			},
		}
	}
</script>
<style>
	.croppage {
		width: 100%;
		height: 100%;
		box-sizing: border-box;
		background-color: #000000;
		position: relative;
	}
	.my-canvas {
		/* display: flex; */
		position: absolute !important;
		left: 0;
		z-index: 100000;
		width: 100%;
	}
	.my-avatar {
		width: 150upx;
		height: 150upx;
		border-radius: 100%;
	}
	.oper-canvas {
		/* display: flex; */
		position: absolute !important;
		left: 0;
		z-index: 100000;
		width: 100%;
	}
	.prv-canvas {
		display: flex;
		position: fixed !important;
		background: #000000;
		left: 0;
		z-index: 200000;
		width: 100%;
	}
	.oper-wrapper {
		height: 100rpx;
		position: fixed !important;
		box-sizing: border-box;
		border: 1px solid #F1F1F1;
		background: #ffffff;
		width: 100%;
		left: 0;
		bottom: 0;
		z-index: 99999999;
		flex-direction: row;
	}
	.oper {
		display: flex;
		flex-direction: column;
		justify-content: center;
		padding: 10upx 20upx;
		width: 100%;
		height: 100%;
		box-sizing: border-box;
		align-self: center;
	}
	.btn-wrapper {
		display: flex;
		flex-direction: row;
		flex-grow: 1;
		height: 50px;
		justify-content: space-between;
	}

	.btn-wrapper view {
		display: flex;
		align-items: center;
		justify-content: center;
		font-size: 16px;
		color: #3b7ffb;
		border-radius: 6%;
	}
	.hover {
		background: #f1f1f1;
		border-radius: 6%;
	}
	.clr-wrapper {
		display: flex;
		flex-direction: row;
		flex-grow: 1;
	}
	.clr-wrapper view {
		display: flex;
		align-items: center;
		justify-content: center;
		font-size: 16px;
		color: #333;
		border: 1px solid #f1f1f1;
		border-radius: 6%;
	}
	.my-slider {
		flex-grow: 1;
	}
</style>

2. Introduce components on the pages that need to be used

<template>
	<view>
		<Cropping @upload="doUpload" ref="cropping" :selWidth="screenWidth" selHeight="60rpx" />
	</view>
</template>

<script>
	import Cropping from "@/components/cropping/cropping.vue";
	export default {
		components: {
			Cropping
		},
		data() {
			return {
				result:'',
				screenWidth: ''
			}
		},
		onLoad(option) {

	let getWindowInfo = uni.getWindowInfo()
	this.screenWidth = getWindowInfo.screenWidth
      //screenWidth定义需要裁剪的宽度 selHeight为高度 自行定义传入
			this.$nextTick(()=>{
                //调用组件的fSelect方法传入需要裁剪的图片路径
                 //option.img为需要裁剪的图片路径 自行定义传入
				this.$refs.cropping.fSelect(option.img)
			})
		},
		methods: {
             //剪裁确认
			doUpload(rsp) {
                console.log(rsp)
				//rsp.path为截取的图片路径
			},
		}
	}
</script>

Guess you like

Origin blog.csdn.net/weixin_69666355/article/details/131633975