Record-跟做一个眼睛的动效

56 阅读6分钟

跟做的原文链接# 😈当一个摆子前端太闲的时候会做什么:

今天在掘金上溜达的时候,无意间看到这篇文章,觉得还挺有意思的,就打算用在项目中跟做一下,因为项目使用的框架为React,所以就用React跟做一下;

具体的跟做步骤参考原文;

首先,跟做的第一部分休眠状态就暴漏了一个问题,关于在react中使用定时器:setInterval();

react中使用定时器setInterval

错误的第1版:

useEffect(() => {
        const sleepingTimer = setInterval(() => {
                if (eyeLength > 0) {
                        console.log("眼球的长度", eyeLength);
                        setEyeLength(+(eyeLength - 0.1).toFixed(2));
                        setRouteAngle(+(routeAngle + 0.1).toFixed(2));
                        handleUpEcharts();
                } else {
                        clearInterval(sleepingTimer);
                }
        }, 10);
        return () => {
                clearInterval(sleepingTimer);
        };
}, []);

这一版的表现效果就是我的定时器一直在执行,但是眼睛的长度eyeLength根本没有变化,一直都是初始值;后来在这篇文章中了解到是因为词法作用域导致的,这篇文章的图画的比较清晰,然后我就采用了第一种方案试了下;

错误的第2版:

useEffect(() => {
        const sleepingTimer = setInterval(() => {
                if (eyeLength > 0) {
                        console.log("眼球的长度", eyeLength);
                        setEyeLength(preValue => +(preValue - 0.1).toFixed(2));
                        // setRouteAngle(+(routeAngle + 0.1).toFixed(2));
                        handleUpEcharts();
                } else {
                        clearInterval(sleepingTimer);
                }
        }, 100);
        return () => {
                clearInterval(sleepingTimer);
        };
}, []);

但是我发现我的定时器里面有判断,判断的时候取得长度eyeLength还是存在问题,还是直接取最外层初始值,导致我的定时器管不了,且我的眼睛长度没有变化但是呼吸的动效是正常的;

所以如果你需要根据状态值进行判断,那就必须依赖状态值的变化才可以,方案一不适合;

正确的版本1:

useEffect(() => {
        const sleepingTimer = setInterval(() => {
                if (eyeLength > 0) {
                        console.log("眼球的长度", eyeLength);
                        setEyeLength(+(eyeLength - 0.1).toFixed(2));
                        setRouteAngle(+(routeAngle + 0.1).toFixed(2));
                        handleUpEcharts();
                } else {
                        clearInterval(sleepingTimer);
                }
        }, 10);
        return () => {
                clearInterval(sleepingTimer);
        };
}, [eyeLength]);

显示的效果:

1.gif

正确的版本2:

其实到这,想要的效果已经实现了,但是看完### 如果我的 effect 的依赖频繁变化,我该怎么办?这一块之后,

image.png

首先定时器每次都会被重置且在清除前会调用一次,那其实最好不要去让定时器重置,所以最好还是利用setState的函数式更新:

    1. 定时器不想被重置;
    1. 利用setState的函数式更新;
    1. 更新echarts的属性值,它主要依赖于眼睛长度的状态值;

所以最后又改了一版:

useEffect(() => {
        const sleepingTimer = setInterval(() => {
                setEyeLength(preEyeLength => {
                        console.log("眼球的长度", preEyeLength);
                        if (preEyeLength <= 0) {
                                clearInterval(sleepingTimer);
                        }
                        return +(preEyeLength - 0.1).toFixed(2);
                });
                setRouteAngle(preRouteAngle => +(preRouteAngle + 0.1).toFixed(2));
        }, 20);
        return () => {
                clearInterval(sleepingTimer);
        };
}, []);

useEffect(() => {
        handleUpEcharts();
}, [eyeLength]);

但是这样写,总感觉不优雅了;

完成代码

除了最后的懒惰状态,以下是实现的全部代码:

import { useEffect, useRef, useState } from "react";
import * as echarts from "echarts";

import "./index.less";

const Eye = () => {
	const eyeBallRef = useRef<HTMLDivElement>(null);
	const eyeRef = useRef<HTMLDivElement>(null);
	// 声明Echarts实例
	const eyeEcharts = useRef<echarts.EChartsType>();

	// 眼睛的长度
	const [eyeLength, setEyeLength] = useState(20);
	// 眼睛旋转的角度
	const [routeAngle, setRouteAngle] = useState(0);

	// 眼球色系
	const [eyeBallColor, setEyeBallColor] = useState("#69c2d9");
	const [eyeThemeStyle, setEyeThemeStyle] = useState<any>({});
	const [angryClass, setAngryClass] = useState<string>("");

	// 蓝色系
	const setNormal = () => {
		setEyeThemeStyle({
			"--eye-out-color": "#69c2d9",
			"--eye-mid-color": "#133abc",
			"--eye-in-color": "#13256b",
			"--eye-bg-color": "#d7d7d7"
		});
		setEyeBallColor("#69c2d9");
		setAngryClass("");
	};

	// 紫色系
	const setAngry = () => {
		setEyeThemeStyle({
			"--eye-out-color": "#ea25e8",
			"--eye-mid-color": "#4e0b85",
			"--eye-in-color": "#270343",
			"--eye-bg-color": "#fff5ff"
		});
		setEyeBallColor("#ea25e8");
		setAngryClass("angry");
	};

	const handleUpEcharts = () => {
		eyeEcharts?.current?.setOption({
			series: [
				{
					type: "gauge", // 仪表盘类型
					name: "bling bling eye",
					radius: "-10%", // 仪表盘的半径,设置成负的,分割线从内向外延伸
					center: ["50%", "50%"],
					clockwise: false, // 是否是顺时针
					startAngle: `${0 + routeAngle * 5}`, // 起始角度
					endAngle: `${270 + routeAngle * 5}`, // 结束角度
					splitNumber: 3, // 分割数量
					detail: {
						// 仪表盘详情
						show: false
					},
					axisLine: {
						// 仪表盘轴线
						show: true
					},
					axisTick: false, //刻度样式
					splitLine: {
						show: true,
						length: eyeLength, // 分割线长度
						lineStyle: {
							shadowBlur: 20, // 阴影渐变
							shadowColor: eyeBallColor, // 阴影颜色
							shadowOffsetY: "0",
							color: eyeBallColor, // 分割线颜色
							width: 4 // 分割线宽度
						}
					},
					axisLabel: false
				},
				{
					type: "gauge", // 仪表盘类型
					name: "bling bling eye",
					radius: "-10%", // 仪表盘的半径,设置成负的,分割线从内向外延伸
					center: ["50%", "50%"],
					clockwise: false, // 是否是顺时针
					startAngle: `${-45 + routeAngle * 5}`, // 起始角度
					endAngle: `${225 + routeAngle * 5}`, // 结束角度
					splitNumber: 3, // 分割数量
					detail: {
						// 仪表盘详情
						show: false
					},
					axisLine: {
						// 仪表盘轴线
						show: true
					},
					axisTick: false, //刻度样式
					splitLine: {
						show: true,
						length: eyeLength, // 分割线长度
						lineStyle: {
							shadowBlur: 20, // 阴影渐变
							shadowColor: eyeBallColor, // 阴影颜色
							shadowOffsetY: "0",
							color: eyeBallColor, // 分割线颜色
							width: 4 // 分割线宽度
						}
					},
					axisLabel: false
				}
			]
		});
	};

	useEffect(() => {
		if (eyeBallRef?.current) {
			eyeEcharts.current = echarts.init(eyeBallRef.current);
			handleUpEcharts();
		}
	}, [eyeBallRef]);

	const foucusMouse = (e: MouseEvent) => {
		let bodyW = document.body.clientWidth;
		let bodyH = document.body.clientHeight;
		let origin = [bodyW / 2, bodyH / 2];
		// 鼠标坐标
		let mouseLocation = [e.clientX - origin[0], e.clientY - origin[1]];
		// 旋转角度
		let eyeXDeg = (mouseLocation[1] / bodyW) * 90; // 这里的80代表的是最上下边缘大眼X轴旋转角度
		let eyeYDeg = (mouseLocation[0] / bodyH) * 70;
		if (eyeRef?.current && eyeBallRef?.current) {
			eyeRef.current.style.transform = `rotateY(${eyeYDeg}deg) rotateX(${eyeXDeg}deg)`;
			eyeBallRef.current.style.transform = `translate(${eyeYDeg / 1.5}px, ${-eyeXDeg / 1.5}px)`;
		}
	};

	/* 
		闭眼主要逻辑就是:
			1. 想让眼球的长度慢慢减小直到0;并且添加旋转的效果
			2. 眼球减小为0的时候,添加呼吸的效果;
	 */
	useEffect(() => {
		setNormal();
		const sleepingTimer = setInterval(() => {
			setEyeLength(preEyeLength => {
				// console.log("眼球的长度", preEyeLength);
				if (preEyeLength <= 0) {
					clearInterval(sleepingTimer);
				}
				return +(preEyeLength - 0.1).toFixed(2);
			});
			setRouteAngle(preRouteAngle => +(preRouteAngle + 0.1).toFixed(2));
		}, 20);
		document.addEventListener("mousemove", foucusMouse);
		return () => {
			clearInterval(sleepingTimer);
			document.removeEventListener("mousemove", foucusMouse);
		};
	}, []);

	useEffect(() => {
		handleUpEcharts();
	}, [eyeLength]);

	const handleWakeup = () => {
		if (eyeLength >= 0) return;
		setAngry();
		const wakeTimer = setInterval(() => {
			setEyeLength(preEyeLength => {
				// console.log("眼球的长度", preEyeLength);
				if (preEyeLength >= 50) {
					clearInterval(wakeTimer);
					handleNormal(preEyeLength);
				}
				const length = +(preEyeLength + 0.2).toFixed(2);
				return length > 50 ? 50 : length;
			});
			setRouteAngle(preRouteAngle => +(preRouteAngle + 0.2).toFixed(2));
		}, 20);
	};

	const handleNormal = (length: number) => {
		console.log("handleNormal", length);
		if (length <= 20) return;
		setNormal();
		const normalTimer = setInterval(() => {
			setEyeLength(preEyeLength => {
				// console.log("眼球的长度", preEyeLength);
				if (preEyeLength <= 20) {
					clearInterval(normalTimer);
				}
				const length = +(preEyeLength - 0.2).toFixed(2);
				return length <= 20 ? 20 : length;
			});
			setRouteAngle(preRouteAngle => +(preRouteAngle - 0.2).toFixed(2));
		}, 20);
	};

	return (
		<div className="eye-wrap" style={eyeThemeStyle}>
			<div
				className={["eye-container", eyeLength > 0 ? "" : "eye-sleeping", angryClass].join(" ")}
				ref={eyeRef}
				onClick={handleWakeup}
			>
				<div className="eye-ball" ref={eyeBallRef}></div>
				<div className="filter">
					<div className="eye-container" id="eye-filter"></div>
				</div>
			</div>

			{/* Svg滤镜*/}
			<svg width="0">
				<filter id="filter">
					<feTurbulence baseFrequency="1">
						<animate id="animate1" attributeName="baseFrequency" dur="1s" from="0.5" to="0.55" begin="0s;animate1.end"></animate>
						<animate id="animate2" attributeName="baseFrequency" dur="1s" from="0.55" to="0.5" begin="animate2.end"></animate>
					</feTurbulence>
					<feDisplacementMap in="SourceGraphic" scale="50" xChannelSelector="R" yChannelSelector="B" />
				</filter>
			</svg>
		</div>
	);
};

export default Eye;

css:

.eye-wrap {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100vw;
	height: 100vh;
	overflow: hidden;
	background-color: #faeee5;

	/* 蓝色系 */
	--eye-out-color: #69c2d9;
	--eye-mid-color: #133abc;
	--eye-in-color: #13256b;
	--eye-bg-color: #d7d7d7;

	/* 紫色系  */

	/* @eyeOutColor: #ea25e8;
	@eyeMidColor: #4e0b85;
	@eyeInColor: #270343;
	@eyeBgColor: #fff5ff; */
	.filter {
		width: 100%;
		height: 100%;
		filter: url("#filter");
		opacity: 0;
	}
	.angry {
		animation: looking 2.5s;
		.filter {
			opacity: 1;
		}
	}
	.filter .eye-container {
		top: calc(50% - 120px);
		left: calc(50% - 120px);
	}
	.eye-container,
	.filter .eye-container {
		position: relative;
		width: 200px;

		/* height: 150px; */
		aspect-ratio: 1; /* 纵横比 */
		cursor: pointer;
		border: 4px solid var(--eye-mid-color);
		border-radius: 50%;
	}
	.eye-container {
		/* 眼球 */
		.eye-ball {
			position: absolute;
			width: 100%;
			height: 100%;
			background-color: var(--eye-bg-color);
			border-radius: 50%;
		}
	}
	.eye-container::after,
	.eye-container::before {
		position: absolute;
		top: 50%;
		left: 50%;
		box-sizing: border-box;
		content: "";
		border-radius: 50%;
		transform: translate(-50%, -50%);
	}
	.eye-container::before {
		width: calc(100% + 20px);
		height: calc(100% + 20px);
		border: 6px solid var(--eye-out-color);
	}
	.eye-container::after {
		width: 100%;
		height: 100%;
		border: 4px solid var(--eye-in-color);
		box-shadow: inset 0 0 30px var(--eye-in-color);
	}

	/* 休眠状态 */
	.eye-sleeping {
		animation: sleeping 6s infinite;
	}

	/* 有种呼吸的感觉 */
	@keyframes sleeping {
		0% {
			transform: scale(1);
		}
		50% {
			transform: scale(1.2);
		}
		100% {
			transform: scale(1);
		}
	}

	/* 寻找 */
	@keyframes looking {
		0% {
			transform: translateX(0) rotateY(0);
		}
		10% {
			transform: translateX(0) rotateY(0);
		}
		40% {
			transform: translateX(70px) rotateY(30deg);
		}
		80% {
			transform: translateX(-70px) rotateY(-30deg);
		}
		100% {
			transform: translateX(0) rotateY(0);
		}
	}
}

效果

1.gif