78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
|
|
|
type StackItem = {
|
|
id: string;
|
|
imageUrl: string;
|
|
prompt: string;
|
|
modelId: string;
|
|
};
|
|
|
|
export function HoverStack({ items }: { items: StackItem[] }) {
|
|
const offsetX = useMotionValue(0);
|
|
const offsetY = useMotionValue(0);
|
|
|
|
const smoothX = useSpring(offsetX, { stiffness: 180, damping: 20, mass: 0.5 });
|
|
const smoothY = useSpring(offsetY, { stiffness: 180, damping: 20, mass: 0.5 });
|
|
|
|
const rotateY = useTransform(smoothX, [-160, 160], [-8, 8]);
|
|
const rotateX = useTransform(smoothY, [-120, 120], [7, -7]);
|
|
|
|
const cards = items.slice(0, 3);
|
|
|
|
if (cards.length === 0) {
|
|
return (
|
|
<div className="rounded-2xl border border-white/15 bg-black/30 p-6 text-sm text-zinc-300">
|
|
暂无可展示样例
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
className="relative h-[290px] w-full [perspective:1200px]"
|
|
onMouseLeave={() => {
|
|
offsetX.set(0);
|
|
offsetY.set(0);
|
|
}}
|
|
onMouseMove={(event) => {
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
const x = event.clientX - (rect.left + rect.width / 2);
|
|
const y = event.clientY - (rect.top + rect.height / 2);
|
|
offsetX.set(x);
|
|
offsetY.set(y);
|
|
}}
|
|
style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
|
|
>
|
|
{cards.map((item, index) => {
|
|
const offset = index * 18;
|
|
|
|
return (
|
|
<motion.article
|
|
key={item.id}
|
|
animate={{ y: [offset, offset - 7, offset], scale: [1, 1.01, 1] }}
|
|
className="absolute left-0 right-0 mx-auto w-[92%] overflow-hidden rounded-2xl border border-white/15 bg-black/35 shadow-[0_20px_60px_-24px_rgba(14,165,233,0.55)]"
|
|
style={{
|
|
zIndex: cards.length - index,
|
|
transform: `translateZ(${(cards.length - index) * 28}px)`,
|
|
top: `${index * 24}px`,
|
|
}}
|
|
transition={{
|
|
duration: 4 + index,
|
|
ease: "easeInOut",
|
|
repeat: Number.POSITIVE_INFINITY,
|
|
}}
|
|
>
|
|
<img alt={item.prompt} className="h-36 w-full object-cover" src={item.imageUrl} />
|
|
<div className="space-y-1 p-3">
|
|
<p className="line-clamp-2 text-xs text-zinc-200">{item.prompt}</p>
|
|
<p className="text-[11px] text-zinc-400">{item.modelId}</p>
|
|
</div>
|
|
</motion.article>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
);
|
|
}
|