Files
netrunner-landing/components/sections/Hero.tsx
T
2026-04-24 13:16:52 +07:00

335 lines
12 KiB
TypeScript

"use client";
import { useState, useRef, useEffect } from "react";
import { Switch } from "@/components/ui/switch";
import { motion, Variants } from "framer-motion";
import { Shield, EyeOff } from "lucide-react";
import { Dictionary } from "@/lib/IDict";
import { CyberButton } from "../ui/cyber-button";
import { NetrunnerMatrix } from "../graphics/NetrunnerMatrix";
import { useTheme } from "next-themes";
function CyberEye({
isClosed,
vpnOn,
gaze,
}: {
isClosed: boolean;
vpnOn: boolean;
gaze: { x: number; y: number; dir: string };
}) {
const eyeVariants: Variants = {
open: {
height: 72,
width: 120,
borderRadius: 36,
borderWidth: 4,
transition: { type: "spring", stiffness: 400, damping: 15 },
},
closed: {
height: 6,
width: 140,
borderRadius: 8,
borderWidth: 3,
transition: { type: "spring", stiffness: 500, damping: 15 },
},
};
const borderColor = vpnOn
? "border-purple-600 dark:border-primary shadow-[0_0_15px_rgba(139,61,255,0.3)]"
: "border-cyan-600 dark:border-secondary shadow-[0_0_15px_rgba(0,229,242,0.3)]";
return (
<motion.div
variants={eyeVariants}
initial="open"
animate={isClosed ? "closed" : "open"}
className={`relative flex items-center justify-center overflow-hidden bg-background ${borderColor} z-10 select-none`}
>
<motion.div
animate={{
x: isClosed ? 0 : gaze.x,
y: isClosed ? 0 : gaze.y,
scale: isClosed ? 0.3 : 1,
opacity: isClosed ? 0 : 1,
}}
// Физика саккады: резкий прыжок с легкой микро-упругостью в конце
transition={{ type: "spring", stiffness: 700, damping: 25, mass: 0.5 }}
className="relative flex items-center justify-center text-foreground pointer-events-none"
>
<span
className={`absolute -top-4 text-xs transition-all duration-300 ${
gaze.dir === "up"
? "opacity-100 scale-125 drop-shadow-md"
: "opacity-20"
}`}
>
</span>
<span
className={`absolute -bottom-4 text-xs transition-all duration-300 ${
gaze.dir === "down"
? "opacity-100 scale-125 drop-shadow-md"
: "opacity-20"
}`}
>
</span>
<span
className={`absolute -left-5 text-xs transition-all duration-300 ${
gaze.dir === "left"
? "opacity-100 scale-125 drop-shadow-md"
: "opacity-20"
}`}
>
</span>
<span
className={`absolute -right-5 text-xs transition-all duration-300 ${
gaze.dir === "right"
? "opacity-100 scale-125 drop-shadow-md"
: "opacity-20"
}`}
>
</span>
<span className="text-3xl z-10 drop-shadow-md"></span>
</motion.div>
{/* Лазерная полоса при закрытом глазе */}
<motion.div
className={`absolute h-0.5 w-full ${
vpnOn
? "bg-purple-600 dark:bg-primary shadow-[0_0_8px_var(--primary)]"
: "bg-cyan-600 dark:bg-secondary"
}`}
initial={{ opacity: 0 }}
animate={{ opacity: isClosed ? 1 : 0 }}
transition={{ duration: 0.2 }}
/>
</motion.div>
);
}
function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const [isBlinking, setIsBlinking] = useState(false);
const [gaze, setGaze] = useState({ x: 0, y: 0, dir: "center" });
const timerRef = useRef<any>(null);
const lastInteraction = useRef<number>(0);
// Классическое случайное моргание
useEffect(() => {
const schedule = () => {
timerRef.current = setTimeout(
() => {
setIsBlinking(true);
setTimeout(() => {
setIsBlinking(false);
schedule();
}, 120); // Чуть ускорили моргание
},
Math.random() * 4000 + 1000,
);
};
if (!vpnOn) schedule();
return () => clearTimeout(timerRef.current);
}, [vpnOn]);
// Единая логика взгляда (Unified Gaze Vector)
useEffect(() => {
const updateGaze = (targetX: number, targetY: number) => {
if (!wrapperRef.current) return;
// Вычисляем центр относительно "переносицы" (контейнера), а не каждого глаза
const rect = wrapperRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = targetX - centerX;
const dy = targetY - centerY;
let dir = "center";
// Увеличили мертвую зону, чтобы взгляд чаще казался "сосредоточенным"
if (Math.abs(dx) < 60 && Math.abs(dy) < 60) {
dir = "center";
} else if (Math.abs(dx) > Math.abs(dy)) {
dir = dx > 0 ? "right" : "left";
} else {
dir = dy > 0 ? "down" : "up";
}
const angle = Math.atan2(dy, dx);
const maxRadius = 20;
// Небольшой коэффициент, чтобы глаза не бились о края орбиты слишком быстро
const distance = Math.min(maxRadius, Math.hypot(dx, dy) * 0.1);
setGaze({
x: Math.cos(angle) * distance,
y: Math.sin(angle) * distance,
dir,
});
};
const handleInteract = (e: MouseEvent | TouchEvent) => {
lastInteraction.current = Date.now();
let clientX, clientY;
if ("touches" in e && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = (e as MouseEvent).clientX;
clientY = (e as MouseEvent).clientY;
}
updateGaze(clientX, clientY);
};
window.addEventListener("mousemove", handleInteract);
window.addEventListener("touchstart", handleInteract, { passive: true });
window.addEventListener("pointerdown", handleInteract);
// Логика "живого" блуждания для мобилок
const randomLookInterval = setInterval(() => {
const isMobile = window.innerWidth <= 768 || "ontouchstart" in window;
if (isMobile && Date.now() - lastInteraction.current > 2500) {
if (Math.random() > 0.2) {
// Выбираем новую точку
const rx =
window.innerWidth * 0.1 + Math.random() * (window.innerWidth * 0.8);
const ry =
window.innerHeight * 0.1 +
Math.random() * (window.innerHeight * 0.8);
updateGaze(rx, ry);
// Микро-моргание при скачке взгляда (очень органичный эффект)
if (Math.random() > 0.6) {
setIsBlinking(true);
setTimeout(() => setIsBlinking(false), 80);
}
} else {
// Иногда смотрим прямо
updateGaze(window.innerWidth / 2, window.innerHeight / 2);
}
}
}, 1800); // Чуть чаще меняем взгляд на телефоне
return () => {
window.removeEventListener("mousemove", handleInteract);
window.removeEventListener("touchstart", handleInteract);
window.removeEventListener("pointerdown", handleInteract);
clearInterval(randomLookInterval);
};
}, []);
const shouldBeClosed = vpnOn || isBlinking;
return (
<div
ref={wrapperRef}
className="flex gap-6 md:gap-10 justify-center mb-12 h-20 items-center select-none pointer-events-none"
>
{/* Теперь оба глаза получают абсолютно одинаковое смещение */}
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} gaze={gaze} />
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} gaze={gaze} />
</div>
);
}
export function Hero({
dict,
lang,
}: {
dict: Dictionary["hero"];
lang: string;
}) {
const [isVpnOn, setIsVpnOn] = useState(false);
const { resolvedTheme } = useTheme();
return (
<section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden select-none">
{/* --- RUST BACKGROUND --- */}
<div className="absolute inset-0 -z-10 bg-background pointer-events-auto">
<NetrunnerMatrix isSecure={isVpnOn} />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_20%,_var(--background)_120%)] opacity-80 pointer-events-none" />
<div className="absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-background via-transparent to-transparent opacity-90 pointer-events-none" />
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-4xl mx-auto pt-40 md:pt-48 pb-20 pointer-events-none"
>
<div className="pointer-events-auto flex flex-col items-center">
<motion.div
layout
className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full border text-sm font-bold mb-8 transition-colors duration-500 shadow-sm cursor-default ${
isVpnOn
? "bg-primary/10 border-primary/50 text-primary"
: "bg-secondary/10 border-secondary/50 text-secondary"
}`}
>
{isVpnOn ? (
<Shield className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
<span>{isVpnOn ? dict.badgeSecure : dict.badge}</span>
</motion.div>
{/* НАШИ ИДЕАЛЬНЫЕ ГЛАЗА */}
<CyberWatcherEyes vpnOn={isVpnOn} />
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-foreground mb-6 font-mono cursor-default">
{dict.titleStart}{" "}
<span
className={`transition-all duration-500 ${
isVpnOn
? "text-primary drop-shadow-[0_0_15px_rgba(139,61,255,0.4)]"
: "text-secondary drop-shadow-[0_0_15px_rgba(0,229,242,0.4)]"
}`}
>
{dict.titleHighlight}
</span>
</h1>
<p className="text-muted-foreground font-medium text-lg md:text-xl max-w-2xl mx-auto mb-12 cursor-default">
{dict.subtitle}
</p>
<div className="flex flex-col items-center gap-8 mb-12">
<div className="flex items-center gap-4 bg-card/80 backdrop-blur-xl border border-border p-4 rounded-3xl shadow-md transition-colors duration-300">
<Switch
checked={isVpnOn}
onCheckedChange={setIsVpnOn}
className="data-[state=checked]:bg-primary data-[state=unchecked]:bg-secondary"
/>
<span
className={`text-base font-bold font-mono transition-colors ${
isVpnOn ? "text-primary" : "text-foreground"
}`}
>
Netrunner VPN
</span>
</div>
<div className="flex flex-col sm:flex-row gap-6 justify-center w-full sm:w-auto font-mono">
<CyberButton
href={`/${lang}/download`}
variant={isVpnOn ? "primary" : "secondary"}
>
{dict.ctaPrimary}
</CyberButton>
<CyberButton href={`/${lang}/nodes`} variant="secondary">
{dict.ctaSecondary}
</CyberButton>
</div>
</div>
</div>
</motion.div>
</section>
);
}