这个组件是我的新个人博客站点中的一个小组件,觉得还挺好玩的单拿出来写一下,是一个动态的小幽灵

主要用的是React去写,因为我博客用的是Next.js,如果有大佬的话希望给孩子弄一个Vue版本也可以! (全部代码放在最后了) (你的新博客呢? 后端难产了,rust+axum还是太吃操作了,再等等孩子吧)
大概我们要做出来一个这样子的东东:
先来个背景吧 我们使用 @paper-design/shaders-react 提供的 MeshGradient 组件,生成实时变化的背景。 通过 SVG 的 clipPath,把渐变背景裁剪成小幽灵形状。
tsx<foreignObject width="231" height="289" clipPath="url(#shapeClip)">
<MeshGradient colors={colors} className="w-full h-full" speed={1} />
</foreignObject>
再来个漂浮动画,这里用framer做
tsx<motion.div
animate={{ y: [0, -8, 0], scaleY: [1, 1.08, 1] }}
transition={{ duration: 2.8, repeat: Infinity, ease: "easeInOut" }}
>
{/* SVG 内容 */}
</motion.div>
这里通过 关键帧数组 [0, -8, 0] 控制 y 轴偏移,让元素上下往返。 然后做眼睛跟随鼠标这一部分,首先要监听一个鼠标移动时间,然后计算鼠标和svg中心偏差得到偏移量
tsxuseEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
useEffect(() => {
const rect = document.querySelector('svg')?.getBoundingClientRect();
if (rect) {
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = (mousePosition.x - centerX) * 0.08;
const deltaY = (mousePosition.y - centerY) * 0.08;
const maxOffset = 8;
setEyeOffset({
x: Math.max(-maxOffset, Math.min(maxOffset, deltaX)),
y: Math.max(-maxOffset, Math.min(maxOffset, deltaY)),
});
}
}, [mousePosition]);
最后用 motion.ellipse 来平滑移动眼睛位置:
tsx<motion.ellipse
animate={{ cx: 80 + eyeOffset.x, cy: 120 + eyeOffset.y }}
transition={{ type: "spring", stiffness: 150, damping: 15 }}
/>
眨眼睛用css写就行了,这个也很简单,我们去修改椭圆的ry值
tsx<style jsx>{`
.animate-blink {
animation: blink 3s infinite ease-in-out;
}
@keyframes blink {
0%,
90%,
100% {
ry: 30;
}
95% {
ry: 3;
}
}
`}</style>
如果你是 Next.js 开发者,可以粘贴直接用
如果你是react router or react + vite开发者,去掉use client标识即可
期待大佬完成vue版本
tsx'use client';
import { MeshGradient } from '@paper-design/shaders-react';
import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
export function MeshGradientSVG() {
const colors = [
'#FFB3D9', // 粉色
'#87CEEB', // 天蓝
'#4A90E2', // 中蓝
'#2C3E50', // 深灰蓝
'#1A1A2E', // 极深蓝
];
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
useEffect(() => {
const rect = document.querySelector('svg')?.getBoundingClientRect();
if (rect) {
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = (mousePosition.x - centerX) * 0.08;
const deltaY = (mousePosition.y - centerY) * 0.08;
const maxOffset = 8;
setEyeOffset({
x: Math.max(-maxOffset, Math.min(maxOffset, deltaX)),
y: Math.max(-maxOffset, Math.min(maxOffset, deltaY)),
});
}
}, [mousePosition]);
return (
<motion.div
className="relative w-full max-w-sm mx-auto p-8 rounded-lg"
animate={{
y: [0, -8, 0],
scaleY: [1, 1.08, 1],
}}
transition={{
duration: 2.8,
repeat: Number.POSITIVE_INFINITY,
ease: 'easeInOut',
}}
style={{ transformOrigin: 'top center' }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="231"
height="289"
viewBox="0 0 231 289"
className="w-full h-auto"
>
<defs>
<clipPath id="shapeClip">
<path d="M230.809 115.385V249.411C230.809 269.923 214.985 287.282 194.495 288.411C184.544 288.949 175.364 285.718 168.26 280C159.746 273.154 147.769 273.461 139.178 280.23C132.638 285.384 124.381 288.462 115.379 288.462C106.377 288.462 98.1451 285.384 91.6055 280.23C82.912 273.385 70.9353 273.385 62.2415 280.23C55.7532 285.334 47.598 288.411 38.7246 288.462C17.4132 288.615 0 270.667 0 249.359V115.385C0 51.6667 51.6756 0 115.404 0C179.134 0 230.809 51.6667 230.809 115.385Z" />
</clipPath>
</defs>
<foreignObject width="231" height="289" clipPath="url(#shapeClip)">
<div className="w-full h-full">
<MeshGradient colors={colors} className="w-full h-full" speed={1} />
</div>
</foreignObject>
<motion.ellipse
rx="20"
ry="30"
fill="currentColor"
className="animate-blink"
animate={{
cx: 80 + eyeOffset.x,
cy: 120 + eyeOffset.y,
}}
transition={{ type: 'spring', stiffness: 150, damping: 15 }}
/>
<motion.ellipse
rx="20"
ry="30"
fill="currentColor"
className="animate-blink"
animate={{
cx: 150 + eyeOffset.x,
cy: 120 + eyeOffset.y,
}}
transition={{ type: 'spring', stiffness: 150, damping: 15 }}
/>
</svg>
<style jsx>{`
.animate-blink {
animation: blink 3s infinite ease-in-out;
}
@keyframes blink {
0%,
90%,
100% {
ry: 30;
}
95% {
ry: 3;
}
}
`}</style>
</motion.div>
);
}


本文作者:MapleCity
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!