<template>
	<div class="hand-track">
		<video
			id="gum-local"
			class="video"
			width="100%"
			height="100%"
			autoplay
			playsinline
			webkit-playsinline
			x5-playsinline
			muted
			ref="myVideo"
		></video>

		<!-- 获取权限 -->
		<button
			id="showVideo"
			class="btn"
			ref="showVideoBtn"
			@click="initVideo($event)"
		>
			开启
		</button>

		<!-- info -->
		<div class="info-dialog" ref="info">
			<div class="progress" ref="progress">
				当前进度: {{ progressText }}..
			</div>
			<div class="status">
				当前手势: <span>{{ handStatus }}</span>
			</div>
		</div>

		<!-- progress -->
		<div class="progress-bar" ref="progressBar">
			<div class="bar">
				<div class="progress" :style="'width:' + progress + '%'"></div>
			</div>
		</div>

		<!-- canvas -->
		<canvas class="canvas" ref="canvas"></canvas>

		<!-- hand -->
		<div
			class="hand-dialog"
			ref="handDialog"
			v-if="handStep > 0 && handStep < 3"
		>
			<p>请在镜头前尝试做出以下手势</p>
			<div class="hand" :class="'hand-' + handStep" ref="hand"></div>
		</div>

		<!-- unsuit-dialog -->
		<div class="des-dialog" ref="unsuit"></div>
	</div>
</template>
<script>
// @ is an alias to /src
import gsap from "gsap";
import {
	IEngine,
	Load,
	IHandTrackingApi,
	MediaStreamErrorEnum,
	EventEnum,
} from "@handtracking.io/yoha";
import * as THREE from "three";

export default {
	name: "handTrack",
	components: {},
	data() {
		return {
			progress: 0,
			progressText: "0%", //进度文字
			BORDER_PADDING_FACTOR: 0.02, //边界阀值
			VIDEO_WIDTH_FACTOR: 0.8, //宽度阀值
			videoRealSize: {},
			handStatus: "未检测到手",
			handStep: 1,
			meshAdded: false,
		};
	},
	mounted() {
		this.video = this.$refs.myVideo;
		this.canvas = this.$refs.canvas;

		this.initTHREELayer();
		this.checkUserDeviceSuitable();
	},
	methods: {
		// 检测用户手机是否兼容
		// > 14.3 ios ok
		checkUserDeviceSuitable() {
			if (
				window.deviceInfo.system === "IOS"
				// && window.deviceInfo.app === "WX"
			) {
				let iosV = this.getIOSVersion();
				if (
					!this.versionStringCompare(iosV, "14.3")
					//  && this.versionStringCompare(iosV, "14.0")
				) {
					// 	this.$Utils.showTips({
					// 		message: "请点击右上角 ...</br>在浏览器打开",
					// 		autoClose: false,
					// 	});
					// } else if (!this.versionStringCompare(iosV, "14.0")) {
					this.$Utils.showTips({
						message:
							"😭 很抱歉</br>⚠️ 当前系统版本过低</br>无法体验该Demo",
						autoClose: false,
					});
				}
			}
		},
		// 版本号比较
		versionStringCompare(preVersion = "", lastVersion = "") {
			// 将版本号格式化为相同位数数字字符串
			function toNum(a) {
				var a = a.toString();
				//也可以这样写 var c=a.split(/\./);
				var c = a.split(".");
				// eslint-disable-next-line camelcase
				var num_place = ["", "0", "00", "000", "0000"],
					// eslint-disable-next-line camelcase
					r = num_place.reverse();
				for (var i = 0; i < c.length; i++) {
					var len = c[i].length;
					c[i] = r[len] + c[i];
				}
				var res = c.join("");
				return res;
			}

			var _a = toNum(preVersion),
				_b = toNum(lastVersion);
			console.log(_a, _b);
			if (_a == _b || _a > _b) return true;
			else return false;
		},
		// 获取设备权限
		getDevicePermission(constraints) {
			if (navigator.mediaDevices.getUserMedia === undefined) {
				navigator.mediaDevices.getUserMedia = function (constraints) {
					var getUserMedia =
						navigator.getUserMedia ||
						navigator.webkitGetUserMedia ||
						navigator.mozGetUserMedia ||
						navigator.msGetUserMedia ||
						navigator.oGetUserMedia;
					if (!getUserMedia) {
						return Promise.reject(
							new Error(
								"getUserMedia is not implemented in this browser"
							)
						);
					}
					return new Promise(function (resolve, reject) {
						getUserMedia.call(
							navigator,
							constraints,
							resolve,
							reject
						);
					});
				};
			}

			return navigator.mediaDevices
				.getUserMedia(constraints)
				.then((stream) => {
					if (stream) {
						const tracks = stream.getTracks();
						tracks.forEach(function (track) {
							track.stop();
						});

						// stopStreamTracks(stream);
						return true;
					}
					return Promise.reject(new Error("EmptyStreamError"));
				})
				.catch((errMsg) => {
					if (errMsg && "NotAllowedError" === errMsg.name) {
						return false;
					}
					return Promise.reject(errMsg);
				});
		},
		// IOS 用户获取授权
		requestPermission() {
			return (
				this.getDevicePermission({ video: true })
					// .catch(() =>
					// 	this.getDevicePermission({ video: false, audio: true })
					// )
					// .catch(() =>
					// 	this.getDevicePermission({ video: true, audio: true })
					// )
					.catch(() => true)
			);
		},
		// 获取摄像头设备id
		async getBackCameraDeviceId() {
			await this.requestPermission();
			let source = new Array();
			console.log("start get camera devices");
			let devices = await navigator.mediaDevices.enumerateDevices();
			console.log("devices array:", devices);
			if (devices.length > 0) {
				devices.forEach(function (device) {
					if (device.kind == "videoinput") {
						source.push(device);
					}
				});
			}
			console.log("devices source:", source);
			return source[source.length - 1].deviceId;
		},
		// 初始化 video
		async initVideo(e) {
			const constraints = {
				video: {
					deviceId: { exact: await this.getBackCameraDeviceId() },
					facingMode: "environment",
					// width: {
					// 	min: 480,
					// },
					// height: {
					// 	min: 640,
					// },
				},
				// video: {
				// 	facingMode: { exact: "environment" },
				// 	width: { min: 375, ideal: 375, max: 750 },
				// 	height: { min: 590, ideal: 590, max: 1624 },
				// },
			};

			try {
				this.stream = await navigator.mediaDevices.getUserMedia(
					constraints
				);

				gsap.to(e.target, { autoAlpha: 0 });
				e.target.disabled = true;

				// 处理成功回调
				this.handleSuccess(this.stream, constraints);
			} catch (e) {
				this.handleError(e);
			}
		},
		// 处理stream获取成功
		async handleSuccess(stream, constraints) {
			const videoTracks = stream.getVideoTracks();
			this.stream = stream; // make variable available to browser console
			let streamSize = this.GetStreamDimensions(stream);

			// 设置视频
			this.responseVideoAndCanvas();

			let handledSize = this.ScaleResolutionToWidth(
				{ width: this.video.width, height: this.video.height },
				window.innerWidth
			);

			this.videoRealSize = handledSize;

			console.log("Got stream with constraints:", constraints);
			console.log(`Using video device: ${videoTracks[0].label}`);
			console.log("Got stream size:", videoTracks);
			console.log("Got stream size:", streamSize);
			console.log(
				"Got streamHandled size:",
				window.innerHeight
				// handledSize
			);

			this.video.srcObject = stream;

			this.video.play();

			// 开始识别
			this.startDraw();
		},
		// 处理stream获取错误
		handleError(error) {
			console.error(error);
		},
		// 视频设置
		responseVideoAndCanvas(size) {
			this.video.width = window.innerWidth;
			this.video.height = window.innerHeight;
			// init canvcas
			this.ctx = this.canvas.getContext("2d");
			this.canvas.width = window.innerWidth;
			this.canvas.height = window.innerHeight;
		},
		// 获得stream大小
		GetStreamDimensions(stream) {
			return {
				width: stream.getVideoTracks()[0].getSettings().width,
				height: stream.getVideoTracks()[0].getSettings().height,
			};
		},
		// 等比缩放视频 宽
		ScaleResolutionToWidth(resolution, width) {
			const cw = resolution.width;
			const ch = resolution.height;
			const tw = width;

			return {
				width: tw,
				height: ch / (cw / tw),
			};
		},
		// 等比缩放视频 高
		ScaleResolutionToHeight(resolution, height) {
			const cw = resolution.width;
			const ch = resolution.height;
			const th = height;

			return {
				width: cw / (ch / th),
				height: th,
			};
		},
		// 创建手势识别引擎
		async CreateEngine() {
			let API = IHandTrackingApi;
			if (API) {
				return API.CreateEngine();
			}
			API = await Load("./yoha");
			return API.CreateEngine();
		},
		// 开始识别
		async startDraw() {
			console.log("engine start load");
			gsap.to(this.$refs.progressBar, {
				autoAlpha: 1,
				delay: 0.5,
			});
			this.engine = await this.CreateEngine();

			console.log("engine model start load");
			await this.engine.DownloadModel((progress) => {
				this.progress = Math.round(progress * 100);
				this.progressText = `${this.progress}%`;
			});
			console.log("engine model loaded");

			this.progressText = "引擎准备中…";
			this.engine.Configure({
				// Webcam video is usually flipped so we want the coordinates to be flipped as well.
				flipX: false,
				// Crop away a small area at the border to prevent the user to move out of view
				// when reaching for border areas on the canvas.
				padding: this.BORDER_PADDING_FACTOR,
			});

			this.engine.SetUpCustomTrackSource(this.video);
			// await this.engine.SetUpCameraTrackSource();

			// ios需要预热
			if (window.deviceInfo.system === "IOS") {
				await this.engine.Warmup();
			}

			this.progressText = "引擎已就绪";
			gsap.to([this.$refs.progress, this.$refs.progressBar], {
				autoAlpha: 0,
				delay: 0.5,
				onComplete: () => {
					this.$refs.progress.style.display = "none";
					this.$refs.myVideo.style.display = "block";
				},
			});
			gsap.to([this.$refs.handDialog, this.$refs.info], {
				autoAlpha: 1,
				delay: 0.5,
			});
			// 识别引擎启动
			this.engineStart();
		},
		// 识别引擎启动
		engineStart() {
			this.engine.Start((e) => {
				let cursorPos;
				if (e.coordinates) {
					cursorPos = this.ExponentialCoordinateAverage(
						this.ComputeCursorPositionFromCoordinates(e.coordinates)
					);
					console.log(cursorPos);
				}

				// console.log(e.type);

				if (e.type === EventEnum.RESULT) {
					this.drawPoint(cursorPos[0], cursorPos[1]);
					this.Render();
					if (e.poses.fist) {
						this.handStatus = "握拳";
						if (this.handStep == 1) {
							this.handStep++;
						}
						// document.getElementById("status").innerText = "握拳";
					} else if (e.poses.pinch) {
						this.handStatus = "捏合";
					} else {
						this.handStatus = "张开";
						if (this.handStep == 2) {
							this.handStep++;
						}
						// document.getElementById("status").innerText = "张开";
					}
				} else if (e.type === EventEnum.LOST) {
					this.handStatus = "未检测到手";
					// document.getElementById("status").innerText = "未检测到手";
				}
			});
		},
		// 获取ios版本号
		getIOSVersion() {
			let str = navigator.userAgent.toLowerCase();
			let ver = str.match(/cpu iphone os (.*?) like mac os/);
			if (!ver) {
				console.log("请在Ios系统中打开");
			} else {
				console.log(
					"你当前的Ios系统版本为：" + ver[1].replace(/_/g, ".")
				);
				return ver[1].replace(/_/g, ".");
			}
		},
		// 计算坐标值平均数
		ExponentialMovingAverage(value, alpha) {
			if (this.curValue_ === undefined || this.curValue_ === null) {
				this.curValue_ = value;
			} else {
				this.curValue_ = this.curValue_ * (1 - alpha) + value * alpha;
			}
			return this.curValue_;
		},
		// 计算出手指四个点
		ExponentialCoordinateAverage(coord, alpha = 0.85) {
			let xAvg_ = this.ExponentialMovingAverage(coord[0], alpha),
				yAvg_ = this.ExponentialMovingAverage(coord[1], alpha),
				zAbg_ = this.ExponentialMovingAverage(coord[2], alpha),
				aAvg_ = this.ExponentialMovingAverage(coord[3], alpha);

			return [xAvg_, yAvg_, zAbg_, aAvg_];
		},
		// 计算几个点的中间值
		ComputeCursorPositionFromCoordinates(coords) {
			// return [
			// 	(coords[3][0] + coords[7][0]) / 2,
			// 	(coords[3][1] + coords[7][1]) / 2,
			// ];
			if (coords[17][0] < coords[2][0]) {
				// console.log('Right')
				return [
					((coords[0][0] + coords[9][0]) / 2) * 1,
					(coords[0][1] + coords[9][1]) / 2,
					coords[3][0] - coords[18][0],
					coords[3][1] - coords[18][1],
				];
			} else {
				// console.log('Left')
				return [
					((coords[0][0] + coords[9][0]) / 2) * 1.1,
					(coords[0][1] + coords[9][1]) / 2,
					coords[3][0] - coords[18][0],
					coords[3][1] - coords[18][1],
				];
			}
		},
		// init THREELayer
		initTHREELayer() {
			this.camera_ = new THREE.OrthographicCamera(
				0,
				this.videoRealSize.width,
				0,
				this.videoRealSize.height,
				0,
				1000
			);
			this.renderer_ = new THREE.WebGLRenderer({
				alpha: true,
				powerPreference: "high-performance",
				failIfMajorPerformanceCaveat: false,
				antialias: true,
			});
			this.renderer_.domElement = this.canvas;
			this.renderer_.debug.checkShaderErrors = false;
			this.renderer_.setSize(
				this.videoRealSize.width,
				this.videoRealSize.height
			);
			this.renderer_.setPixelRatio(window.devicePixelRatio);
			this.renderer_.setClearColor(0x000000, 0.0);
			this.renderer_.autoClear = true;
			this.scene_ = new THREE.Scene();
		},
		// draw pointer
		drawPoint(x, y) {
			if (!this.meshAdded) {
				this.geo_ = new THREE.RingGeometry(0, 8, 30);
				this.mat_ = new THREE.MeshBasicMaterial({
					color: "red",
					side: THREE.DoubleSide,
				});
				this.mesh_ = new THREE.Mesh(this.geo_, this.mat_);
				this.scene_.add(this.mesh_);
				this.meshAdded = true;
			}

			this.mesh_.position.x = x * this.videoRealSize.width;
			this.mesh_.position.y = y * this.videoRealSize.height;
			this.mesh_.position.z = 0;
		},
		// THREE canvas render
		Render() {
			this.renderer_.render(this.scene_, this.camera_);
		},
	},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
.hand-track {
	.prLayout(100%,100vh);
	overflow: hidden;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-content: center;
	align-items: center;
	.video {
		.paLayout(50%,50%,auto,100%,0);
		transform: translate(-50%, -50%);
		// transform: scaleX(-1);
		// width: 750px;
		display: none;
		// height: 700px;
	}
	.info-dialog {
		visibility: hidden;
		.paLayout(0,0, 50%,130px,2);
		background-color: rgba(255, 255, 255, 0.25);
		border-radius: 0 0 30px 0;
		padding: 20px;
		font-size: 28px;
		display: flex;
		flex-direction: column;
		align-content: center;
		align-items: center;
		text-align: left;
		text-indent: 10px;
		justify-content: center;
		div {
			width: 100%;
			line-height: 40px;
			color: #fff;
			span {
				font-weight: bold;
				color: #f00;
			}
		}
		.progress {
		}
	}
	.hand-dialog {
		visibility: hidden;
		.paLayout(0,0,100%,100%,11);
		background-color: rgba(0, 0, 0, 0.4);
		color: #fff;
		text-align: center;
		line-height: 50px;
		font-size: 30px;
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-content: center;
		align-items: center;
		pointer-events: none;
		.hand {
			margin-top: 20px;
			.prLayout(160px*2.5,277px*2.5);
			.bg-norepeat("hand_1","png");
			background-size: contain;
			&.hand-1 {
				.bg-norepeat("hand_1","png");
				background-size: contain;
			}
			&.hand-2 {
				.bg-norepeat("hand_2","png");
				background-size: contain;
			}
		}
	}
	.canvas {
		.paLayout(50%,50%,auto,100%,100);
		transform: translate(-50%, -50%);
		pointer-events: none;
		background-color: rgba(255, 255, 255, 0.1);
	}
	.btn {
		.paCenterBottom(50%,280px,80px,2);
		background-color: rgba(255, 255, 255, 0.85);
		line-height: 40px;
		text-align: center;
		border-radius: 8px;
		font-size: 25px;
	}
	.progress-bar {
		visibility: hidden;
		.paCenter(50%,80%,40px,12);
		margin-top: -20px;
		border: 1px solid #fff;
		border-radius: 25px;
		padding: 4px;
		.bar {
			.prLayout(100%,100%);
			border-radius: 12px;
			overflow: hidden;
			.progress {
				width: 0%;
				height: 100%;
				background-color: #fff;
			}
		}
	}
	.unsuit-dialog {
		visibility: hidden;
		.paCenterBottom(0, 100%,80px,10);
		position: fixed;
		background-color: rgba(255, 255, 255, 0.35);
		color: #fff;
		line-height: 80px;
		text-align: center;
		font-size: 40px;
	}
}
</style>