Spaces:
Running on Zero
Running on Zero
Commit ·
4ed93e5
1
Parent(s): 4346a4c
Home Page
Browse files- frontend/app/globals.css +4 -5
- frontend/app/page.tsx +61 -52
- frontend/components/Footer.tsx +6 -4
- frontend/components/Navbar.tsx +40 -58
- frontend/components/ParallaxScene.tsx +226 -35
- frontend/components/StaggeredMenu.css +0 -1
- frontend/components/StaggeredMenu.tsx +0 -68
frontend/app/globals.css
CHANGED
|
@@ -29,10 +29,9 @@ body {
|
|
| 29 |
|
| 30 |
a { color: inherit; text-decoration: none; }
|
| 31 |
|
| 32 |
-
|
| 33 |
-
::-webkit-scrollbar
|
| 34 |
-
|
| 35 |
-
::-webkit-scrollbar-thumb:hover { background: #a0a5ad; }
|
| 36 |
|
| 37 |
/* ===== NEUMORPHISM ===== */
|
| 38 |
.neu-card {
|
|
@@ -282,7 +281,7 @@ a { color: inherit; text-decoration: none; }
|
|
| 282 |
display: block;
|
| 283 |
width: 22px;
|
| 284 |
height: 2px;
|
| 285 |
-
background: #
|
| 286 |
border-radius: 2px;
|
| 287 |
transition: all 0.3s ease;
|
| 288 |
}
|
|
|
|
| 29 |
|
| 30 |
a { color: inherit; text-decoration: none; }
|
| 31 |
|
| 32 |
+
/* Hide scrollbars globally */
|
| 33 |
+
::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
| 34 |
+
* { scrollbar-width: none; -ms-overflow-style: none; }
|
|
|
|
| 35 |
|
| 36 |
/* ===== NEUMORPHISM ===== */
|
| 37 |
.neu-card {
|
|
|
|
| 281 |
display: block;
|
| 282 |
width: 22px;
|
| 283 |
height: 2px;
|
| 284 |
+
background: #4a5568;
|
| 285 |
border-radius: 2px;
|
| 286 |
transition: all 0.3s ease;
|
| 287 |
}
|
frontend/app/page.tsx
CHANGED
|
@@ -71,23 +71,17 @@ export default function Home() {
|
|
| 71 |
{mounted && <ParallaxScene />}
|
| 72 |
|
| 73 |
<div style={{ position: "relative", zIndex: 1, textAlign: "center", maxWidth: "720px" }}>
|
| 74 |
-
<
|
| 75 |
-
<span className="badge-blue" style={{ padding: "6px 16px", borderRadius: "9999px", fontSize: "0.7rem", fontWeight: 700, letterSpacing: "0.1em" }}>
|
| 76 |
-
NEW PLATFORM
|
| 77 |
-
</span>
|
| 78 |
-
</div>
|
| 79 |
-
|
| 80 |
-
<h1 className="anim-fade-up delay-2" style={{ fontSize: "clamp(2rem, 5vw, 3.2rem)", fontWeight: 900, color: "#f1f5f9", lineHeight: 1.1, marginBottom: "18px", letterSpacing: "-0.02em" }}>
|
| 81 |
OBJECT
|
| 82 |
<span style={{ display: "block", color: "#1E90FF" }}>INTELLIGENCE</span>
|
| 83 |
</h1>
|
| 84 |
|
| 85 |
-
<p className="anim-fade-up delay-
|
| 86 |
Real-time multi-pipeline AI detection and recognition platform.
|
| 87 |
Powered by RT-DETR, YOLO-World, and visual embeddings.
|
| 88 |
</p>
|
| 89 |
|
| 90 |
-
<div className="anim-fade-up delay-
|
| 91 |
<Link href="/live" className="neu-btn-primary" style={{ padding: "14px 28px", fontSize: "0.85rem" }}>
|
| 92 |
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
| 93 |
Live Testing
|
|
@@ -103,11 +97,14 @@ export default function Home() {
|
|
| 103 |
{/* ===== SCROLLING BANNER ===== */}
|
| 104 |
<div
|
| 105 |
style={{
|
| 106 |
-
background: "
|
| 107 |
-
borderTop: "
|
| 108 |
-
borderBottom: "
|
| 109 |
-
|
|
|
|
| 110 |
padding: "14px 0",
|
|
|
|
|
|
|
| 111 |
}}
|
| 112 |
>
|
| 113 |
<div
|
|
@@ -125,7 +122,7 @@ export default function Home() {
|
|
| 125 |
fontSize: "0.75rem",
|
| 126 |
fontWeight: 700,
|
| 127 |
letterSpacing: "0.15em",
|
| 128 |
-
color: "
|
| 129 |
whiteSpace: "nowrap",
|
| 130 |
display: "flex",
|
| 131 |
alignItems: "center",
|
|
@@ -133,7 +130,7 @@ export default function Home() {
|
|
| 133 |
}}
|
| 134 |
>
|
| 135 |
{item}
|
| 136 |
-
<span style={{ width: "4px", height: "4px", borderRadius: "50%", background: "
|
| 137 |
</span>
|
| 138 |
))}
|
| 139 |
</div>
|
|
@@ -145,6 +142,9 @@ export default function Home() {
|
|
| 145 |
`}</style>
|
| 146 |
</div>
|
| 147 |
|
|
|
|
|
|
|
|
|
|
| 148 |
{/* ===== PIPELINE CARDS ===== */}
|
| 149 |
<section style={{ maxWidth: "1200px", margin: "0 auto", padding: "80px 24px" }}>
|
| 150 |
<div style={{ textAlign: "center", marginBottom: "48px" }}>
|
|
@@ -203,14 +203,17 @@ export default function Home() {
|
|
| 203 |
</div>
|
| 204 |
</section>
|
| 205 |
|
|
|
|
|
|
|
|
|
|
| 206 |
{/* ===== AI PIPELINE ===== */}
|
| 207 |
-
<section style={{ background: "
|
| 208 |
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
|
| 209 |
<div style={{ textAlign: "center", marginBottom: "48px" }}>
|
| 210 |
<p className="anim-fade-up" style={{ fontSize: "0.7rem", fontWeight: 700, letterSpacing: "0.2em", color: "#1E90FF", marginBottom: "8px", textTransform: "uppercase" }}>
|
| 211 |
How It Works
|
| 212 |
</p>
|
| 213 |
-
<h2 className="anim-fade-up delay-1" style={{ fontSize: "clamp(1.5rem, 3vw, 2rem)", fontWeight: 800, color: "#
|
| 214 |
AI Processing Pipeline
|
| 215 |
</h2>
|
| 216 |
</div>
|
|
@@ -219,7 +222,7 @@ export default function Home() {
|
|
| 219 |
{STEPS.map((s, i) => (
|
| 220 |
<div
|
| 221 |
key={s.num}
|
| 222 |
-
className={`neu-card
|
| 223 |
style={{ padding: "28px 20px", textAlign: "center", position: "relative" }}
|
| 224 |
>
|
| 225 |
<div
|
|
@@ -237,7 +240,7 @@ export default function Home() {
|
|
| 237 |
<span style={{ fontSize: "0.6rem", fontWeight: 700, letterSpacing: "0.15em", color: "#1E90FF", display: "block", marginBottom: "10px" }}>
|
| 238 |
{s.num}
|
| 239 |
</span>
|
| 240 |
-
<h4 style={{ fontSize: "0.8rem", fontWeight: 700, color: "#
|
| 241 |
{s.title}
|
| 242 |
</h4>
|
| 243 |
<p style={{ fontSize: "0.7rem", color: "#64748b" }}>{s.desc}</p>
|
|
@@ -247,6 +250,9 @@ export default function Home() {
|
|
| 247 |
</div>
|
| 248 |
</section>
|
| 249 |
|
|
|
|
|
|
|
|
|
|
| 250 |
{/* ===== LOCK MODE PREVIEW ===== */}
|
| 251 |
<section style={{ maxWidth: "1200px", margin: "0 auto", padding: "80px 24px" }}>
|
| 252 |
<div
|
|
@@ -339,40 +345,43 @@ export default function Home() {
|
|
| 339 |
</section>
|
| 340 |
|
| 341 |
{/* ===== BOTTOM CTA ===== */}
|
| 342 |
-
<
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
|
|
|
|
|
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
| 376 |
</div>
|
| 377 |
</div>
|
| 378 |
</section>
|
|
|
|
| 71 |
{mounted && <ParallaxScene />}
|
| 72 |
|
| 73 |
<div style={{ position: "relative", zIndex: 1, textAlign: "center", maxWidth: "720px" }}>
|
| 74 |
+
<h1 className="anim-fade-up delay-1" style={{ fontSize: "clamp(2rem, 5vw, 3.2rem)", fontWeight: 900, color: "#f1f5f9", lineHeight: 1.1, marginBottom: "18px", letterSpacing: "-0.02em" }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
OBJECT
|
| 76 |
<span style={{ display: "block", color: "#1E90FF" }}>INTELLIGENCE</span>
|
| 77 |
</h1>
|
| 78 |
|
| 79 |
+
<p className="anim-fade-up delay-2" style={{ color: "#94a3b8", fontSize: "1rem", lineHeight: 1.7, maxWidth: "560px", margin: "0 auto 32px" }}>
|
| 80 |
Real-time multi-pipeline AI detection and recognition platform.
|
| 81 |
Powered by RT-DETR, YOLO-World, and visual embeddings.
|
| 82 |
</p>
|
| 83 |
|
| 84 |
+
<div className="anim-fade-up delay-3" style={{ display: "flex", gap: "12px", justifyContent: "center", flexWrap: "wrap" }}>
|
| 85 |
<Link href="/live" className="neu-btn-primary" style={{ padding: "14px 28px", fontSize: "0.85rem" }}>
|
| 86 |
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
| 87 |
Live Testing
|
|
|
|
| 97 |
{/* ===== SCROLLING BANNER ===== */}
|
| 98 |
<div
|
| 99 |
style={{
|
| 100 |
+
background: "#0a0e17",
|
| 101 |
+
borderTop: "2px solid #1E90FF",
|
| 102 |
+
borderBottom: "3px solid #1E90FF",
|
| 103 |
+
boxShadow: "0 4px 24px rgba(30, 144, 255, 0.25)",
|
| 104 |
+
overflow: "visible",
|
| 105 |
padding: "14px 0",
|
| 106 |
+
position: "relative",
|
| 107 |
+
zIndex: 5,
|
| 108 |
}}
|
| 109 |
>
|
| 110 |
<div
|
|
|
|
| 122 |
fontSize: "0.75rem",
|
| 123 |
fontWeight: 700,
|
| 124 |
letterSpacing: "0.15em",
|
| 125 |
+
color: "#1E90FF",
|
| 126 |
whiteSpace: "nowrap",
|
| 127 |
display: "flex",
|
| 128 |
alignItems: "center",
|
|
|
|
| 130 |
}}
|
| 131 |
>
|
| 132 |
{item}
|
| 133 |
+
<span style={{ width: "4px", height: "4px", borderRadius: "50%", background: "#64B5F6" }} />
|
| 134 |
</span>
|
| 135 |
))}
|
| 136 |
</div>
|
|
|
|
| 142 |
`}</style>
|
| 143 |
</div>
|
| 144 |
|
| 145 |
+
{/* Divider */}
|
| 146 |
+
<div style={{ height: "2px", background: "linear-gradient(90deg, transparent 0%, #1E90FF 20%, #1E90FF 80%, transparent 100%)", margin: "0 auto", maxWidth: "400px", opacity: 0.2 }} />
|
| 147 |
+
|
| 148 |
{/* ===== PIPELINE CARDS ===== */}
|
| 149 |
<section style={{ maxWidth: "1200px", margin: "0 auto", padding: "80px 24px" }}>
|
| 150 |
<div style={{ textAlign: "center", marginBottom: "48px" }}>
|
|
|
|
| 203 |
</div>
|
| 204 |
</section>
|
| 205 |
|
| 206 |
+
{/* Divider */}
|
| 207 |
+
<div style={{ height: "2px", background: "linear-gradient(90deg, transparent 0%, #1E90FF 20%, #1E90FF 80%, transparent 100%)", margin: "0 auto", maxWidth: "400px", opacity: 0.2 }} />
|
| 208 |
+
|
| 209 |
{/* ===== AI PIPELINE ===== */}
|
| 210 |
+
<section style={{ background: "linear-gradient(180deg, #e8edf5 0%, #dbe4f0 100%)", padding: "80px 24px" }}>
|
| 211 |
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
|
| 212 |
<div style={{ textAlign: "center", marginBottom: "48px" }}>
|
| 213 |
<p className="anim-fade-up" style={{ fontSize: "0.7rem", fontWeight: 700, letterSpacing: "0.2em", color: "#1E90FF", marginBottom: "8px", textTransform: "uppercase" }}>
|
| 214 |
How It Works
|
| 215 |
</p>
|
| 216 |
+
<h2 className="anim-fade-up delay-1" style={{ fontSize: "clamp(1.5rem, 3vw, 2rem)", fontWeight: 800, color: "#1a202c" }}>
|
| 217 |
AI Processing Pipeline
|
| 218 |
</h2>
|
| 219 |
</div>
|
|
|
|
| 222 |
{STEPS.map((s, i) => (
|
| 223 |
<div
|
| 224 |
key={s.num}
|
| 225 |
+
className={`neu-card anim-scale-in delay-${i + 1}`}
|
| 226 |
style={{ padding: "28px 20px", textAlign: "center", position: "relative" }}
|
| 227 |
>
|
| 228 |
<div
|
|
|
|
| 240 |
<span style={{ fontSize: "0.6rem", fontWeight: 700, letterSpacing: "0.15em", color: "#1E90FF", display: "block", marginBottom: "10px" }}>
|
| 241 |
{s.num}
|
| 242 |
</span>
|
| 243 |
+
<h4 style={{ fontSize: "0.8rem", fontWeight: 700, color: "#1a202c", marginBottom: "6px", letterSpacing: "0.08em" }}>
|
| 244 |
{s.title}
|
| 245 |
</h4>
|
| 246 |
<p style={{ fontSize: "0.7rem", color: "#64748b" }}>{s.desc}</p>
|
|
|
|
| 250 |
</div>
|
| 251 |
</section>
|
| 252 |
|
| 253 |
+
{/* Divider */}
|
| 254 |
+
<div style={{ height: "2px", background: "linear-gradient(90deg, transparent 0%, #1E90FF 20%, #1E90FF 80%, transparent 100%)", margin: "0 auto", maxWidth: "400px", opacity: 0.2 }} />
|
| 255 |
+
|
| 256 |
{/* ===== LOCK MODE PREVIEW ===== */}
|
| 257 |
<section style={{ maxWidth: "1200px", margin: "0 auto", padding: "80px 24px" }}>
|
| 258 |
<div
|
|
|
|
| 345 |
</section>
|
| 346 |
|
| 347 |
{/* ===== BOTTOM CTA ===== */}
|
| 348 |
+
<div style={{ height: "2px", background: "linear-gradient(90deg, transparent 0%, #1E90FF 20%, #1E90FF 80%, transparent 100%)", margin: "0 auto", maxWidth: "400px", opacity: 0.2 }} />
|
| 349 |
+
<section style={{ background: "linear-gradient(180deg, #e8edf5 0%, #dbe4f0 100%)", padding: "80px 24px" }}>
|
| 350 |
+
<div style={{ maxWidth: "700px", margin: "0 auto" }}>
|
| 351 |
+
<div className="neu-card-dark" style={{ padding: "48px 40px", textAlign: "center", border: "1px solid rgba(30, 144, 255, 0.15)" }}>
|
| 352 |
+
<h2 className="anim-fade-up" style={{ fontSize: "clamp(1.5rem, 3vw, 2rem)", fontWeight: 800, color: "#f1f5f9", marginBottom: "12px" }}>
|
| 353 |
+
Start Detecting
|
| 354 |
+
</h2>
|
| 355 |
+
<p className="anim-fade-up delay-1" style={{ color: "#94a3b8", fontSize: "0.9rem", marginBottom: "32px" }}>
|
| 356 |
+
Launch real-time detection in your browser.
|
| 357 |
+
</p>
|
| 358 |
|
| 359 |
+
<div className="anim-fade-up delay-2" style={{ display: "flex", gap: "16px", justifyContent: "center", flexWrap: "wrap" }}>
|
| 360 |
+
<Link
|
| 361 |
+
href="/live"
|
| 362 |
+
className="neu-btn-primary"
|
| 363 |
+
style={{ padding: "16px 32px", fontSize: "0.9rem", borderRadius: "14px" }}
|
| 364 |
+
>
|
| 365 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
| 366 |
+
Live Testing
|
| 367 |
+
</Link>
|
| 368 |
+
<Link
|
| 369 |
+
href="/objects"
|
| 370 |
+
className="neu-btn-primary"
|
| 371 |
+
style={{ padding: "16px 32px", fontSize: "0.9rem", borderRadius: "14px" }}
|
| 372 |
+
>
|
| 373 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
| 374 |
+
Object Studio
|
| 375 |
+
</Link>
|
| 376 |
+
<Link
|
| 377 |
+
href="/guides"
|
| 378 |
+
className="neu-btn-primary"
|
| 379 |
+
style={{ padding: "16px 32px", fontSize: "0.9rem", borderRadius: "14px" }}
|
| 380 |
+
>
|
| 381 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
| 382 |
+
Read Guides
|
| 383 |
+
</Link>
|
| 384 |
+
</div>
|
| 385 |
</div>
|
| 386 |
</div>
|
| 387 |
</section>
|
frontend/components/Footer.tsx
CHANGED
|
@@ -26,11 +26,13 @@ export default function Footer() {
|
|
| 26 |
top: "50%",
|
| 27 |
left: "50%",
|
| 28 |
transform: "translate(-50%, -50%)",
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
fontWeight: 900,
|
| 32 |
-
letterSpacing: "0.
|
| 33 |
-
color: "rgba(30, 144, 255, 0.
|
| 34 |
userSelect: "none",
|
| 35 |
pointerEvents: "none",
|
| 36 |
lineHeight: 1,
|
|
|
|
| 26 |
top: "50%",
|
| 27 |
left: "50%",
|
| 28 |
transform: "translate(-50%, -50%)",
|
| 29 |
+
width: "1400px",
|
| 30 |
+
maxWidth: "calc(100% - 48px)",
|
| 31 |
+
textAlign: "center",
|
| 32 |
+
fontSize: "clamp(2.5rem, 8vw, 7rem)",
|
| 33 |
fontWeight: 900,
|
| 34 |
+
letterSpacing: "0.08em",
|
| 35 |
+
color: "rgba(30, 144, 255, 0.035)",
|
| 36 |
userSelect: "none",
|
| 37 |
pointerEvents: "none",
|
| 38 |
lineHeight: 1,
|
frontend/components/Navbar.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import Link from "next/link";
|
|
|
|
| 4 |
import { useState, useEffect } from "react";
|
| 5 |
|
| 6 |
const NAV_LINKS = [
|
|
@@ -12,20 +13,9 @@ const NAV_LINKS = [
|
|
| 12 |
];
|
| 13 |
|
| 14 |
export default function Navbar() {
|
| 15 |
-
const
|
| 16 |
const [menuOpen, setMenuOpen] = useState(false);
|
| 17 |
|
| 18 |
-
useEffect(() => {
|
| 19 |
-
const check = () =>
|
| 20 |
-
fetch("/backend-api/health", { method: "GET" })
|
| 21 |
-
.then((r) => r.ok)
|
| 22 |
-
.then(setOnline)
|
| 23 |
-
.catch(() => setOnline(false));
|
| 24 |
-
check();
|
| 25 |
-
const id = setInterval(check, 8000);
|
| 26 |
-
return () => clearInterval(id);
|
| 27 |
-
}, []);
|
| 28 |
-
|
| 29 |
useEffect(() => {
|
| 30 |
if (menuOpen) {
|
| 31 |
document.body.style.overflow = "hidden";
|
|
@@ -42,9 +32,9 @@ export default function Navbar() {
|
|
| 42 |
position: "sticky",
|
| 43 |
top: 0,
|
| 44 |
zIndex: 100,
|
| 45 |
-
background: "rgba(
|
| 46 |
-
backdropFilter: "blur(
|
| 47 |
-
borderBottom: "1px solid rgba(30, 144, 255, 0.
|
| 48 |
padding: "0 24px",
|
| 49 |
height: "60px",
|
| 50 |
display: "flex",
|
|
@@ -81,55 +71,47 @@ export default function Navbar() {
|
|
| 81 |
<path d="M2 12l10 5 10-5" />
|
| 82 |
</svg>
|
| 83 |
</div>
|
| 84 |
-
<span style={{ color: "#
|
| 85 |
OBJECT INTELLIGENCE
|
| 86 |
</span>
|
| 87 |
</Link>
|
| 88 |
|
| 89 |
{/* Desktop nav right */}
|
| 90 |
<div className="desktop-nav" style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
| 91 |
-
{NAV_LINKS.map((l) =>
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
boxShadow: online ? "0 0 8px rgba(16, 185, 129, 0.6)" : "0 0 8px rgba(239, 68, 68, 0.6)",
|
| 126 |
-
animation: online ? "none" : "none",
|
| 127 |
-
}}
|
| 128 |
-
/>
|
| 129 |
-
<span style={{ fontSize: "0.65rem", color: "#64748b", fontWeight: 600, letterSpacing: "0.05em" }}>
|
| 130 |
-
{online ? "ONLINE" : "OFFLINE"}
|
| 131 |
-
</span>
|
| 132 |
-
</div>
|
| 133 |
</div>
|
| 134 |
|
| 135 |
{/* Mobile hamburger */}
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import Link from "next/link";
|
| 4 |
+
import { usePathname } from "next/navigation";
|
| 5 |
import { useState, useEffect } from "react";
|
| 6 |
|
| 7 |
const NAV_LINKS = [
|
|
|
|
| 13 |
];
|
| 14 |
|
| 15 |
export default function Navbar() {
|
| 16 |
+
const pathname = usePathname();
|
| 17 |
const [menuOpen, setMenuOpen] = useState(false);
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
useEffect(() => {
|
| 20 |
if (menuOpen) {
|
| 21 |
document.body.style.overflow = "hidden";
|
|
|
|
| 32 |
position: "sticky",
|
| 33 |
top: 0,
|
| 34 |
zIndex: 100,
|
| 35 |
+
background: "rgba(30, 144, 255, 0.06)",
|
| 36 |
+
backdropFilter: "blur(12px)",
|
| 37 |
+
borderBottom: "1px solid rgba(30, 144, 255, 0.1)",
|
| 38 |
padding: "0 24px",
|
| 39 |
height: "60px",
|
| 40 |
display: "flex",
|
|
|
|
| 71 |
<path d="M2 12l10 5 10-5" />
|
| 72 |
</svg>
|
| 73 |
</div>
|
| 74 |
+
<span style={{ color: "#1a202c", fontWeight: 800, fontSize: "0.95rem", letterSpacing: "0.08em" }}>
|
| 75 |
OBJECT INTELLIGENCE
|
| 76 |
</span>
|
| 77 |
</Link>
|
| 78 |
|
| 79 |
{/* Desktop nav right */}
|
| 80 |
<div className="desktop-nav" style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
| 81 |
+
{NAV_LINKS.map((l) => {
|
| 82 |
+
const isActive = l.href === "/" ? pathname === "/" : pathname.startsWith(l.href);
|
| 83 |
+
return (
|
| 84 |
+
<Link
|
| 85 |
+
key={l.href}
|
| 86 |
+
href={l.href}
|
| 87 |
+
style={{
|
| 88 |
+
padding: "8px 14px",
|
| 89 |
+
fontSize: "0.75rem",
|
| 90 |
+
fontWeight: 700,
|
| 91 |
+
letterSpacing: "0.1em",
|
| 92 |
+
color: isActive ? "#1E90FF" : "#64748b",
|
| 93 |
+
borderRadius: "10px",
|
| 94 |
+
transition: "all 0.2s ease",
|
| 95 |
+
background: isActive ? "rgba(30, 144, 255, 0.1)" : "transparent",
|
| 96 |
+
boxShadow: isActive ? "inset 2px 2px 4px #b8bec7, inset -2px -2px 4px #ffffff" : "none",
|
| 97 |
+
}}
|
| 98 |
+
onMouseEnter={(e) => {
|
| 99 |
+
if (!isActive) {
|
| 100 |
+
e.currentTarget.style.color = "#1E90FF";
|
| 101 |
+
e.currentTarget.style.background = "rgba(30, 144, 255, 0.06)";
|
| 102 |
+
}
|
| 103 |
+
}}
|
| 104 |
+
onMouseLeave={(e) => {
|
| 105 |
+
if (!isActive) {
|
| 106 |
+
e.currentTarget.style.color = "#64748b";
|
| 107 |
+
e.currentTarget.style.background = "transparent";
|
| 108 |
+
}
|
| 109 |
+
}}
|
| 110 |
+
>
|
| 111 |
+
{l.label}
|
| 112 |
+
</Link>
|
| 113 |
+
);
|
| 114 |
+
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
</div>
|
| 116 |
|
| 117 |
{/* Mobile hamburger */}
|
frontend/components/ParallaxScene.tsx
CHANGED
|
@@ -3,6 +3,22 @@
|
|
| 3 |
import { useEffect, useRef } from "react";
|
| 4 |
import * as THREE from "three";
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
export default function ParallaxScene() {
|
| 7 |
const mountRef = useRef<HTMLDivElement>(null);
|
| 8 |
const mouseRef = useRef({ x: 0, y: 0 });
|
|
@@ -21,50 +37,178 @@ export default function ParallaxScene() {
|
|
| 21 |
renderer.setClearColor(0x000000, 0);
|
| 22 |
el.appendChild(renderer.domElement);
|
| 23 |
|
| 24 |
-
const blueMat = new THREE.MeshBasicMaterial({ color: 0x1e90ff, wireframe: true, transparent: true, opacity: 0.
|
| 25 |
-
const
|
| 26 |
-
const accentMat = new THREE.MeshBasicMaterial({ color: 0x64b5f6, wireframe: true, transparent: true, opacity: 0.
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
const shapes:
|
| 29 |
|
| 30 |
-
const addShape = (geo: THREE.BufferGeometry, mat: THREE.Material, x: number, y: number, z: number, speed: number
|
| 31 |
-
const mesh = new THREE.Mesh(geo, mat);
|
| 32 |
mesh.position.set(x, y, z);
|
|
|
|
| 33 |
scene.add(mesh);
|
| 34 |
-
shapes.push({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
};
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
addShape(new THREE.IcosahedronGeometry(
|
| 42 |
-
addShape(new THREE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
//
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
//
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
//
|
| 55 |
const particleGeo = new THREE.BufferGeometry();
|
| 56 |
-
const particleCount =
|
| 57 |
const positions = new Float32Array(particleCount * 3);
|
|
|
|
| 58 |
for (let i = 0; i < particleCount; i++) {
|
| 59 |
-
positions[i * 3] = (Math.random() - 0.5) *
|
| 60 |
-
positions[i * 3 + 1] = (Math.random() - 0.5) *
|
| 61 |
-
positions[i * 3 + 2] = (Math.random() - 0.5) *
|
|
|
|
| 62 |
}
|
| 63 |
particleGeo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
| 64 |
-
|
|
|
|
| 65 |
const particles = new THREE.Points(particleGeo, particleMat);
|
| 66 |
scene.add(particles);
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
const onMouseMove = (e: MouseEvent) => {
|
| 69 |
const rect = el.getBoundingClientRect();
|
| 70 |
mouseRef.current.x = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
|
|
@@ -72,28 +216,75 @@ export default function ParallaxScene() {
|
|
| 72 |
};
|
| 73 |
window.addEventListener("mousemove", onMouseMove);
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
let raf: number;
|
| 76 |
const animate = () => {
|
| 77 |
raf = requestAnimationFrame(animate);
|
| 78 |
-
const t = Date.now() * 0.001;
|
| 79 |
const mx = mouseRef.current.x;
|
| 80 |
const my = mouseRef.current.y;
|
|
|
|
| 81 |
|
| 82 |
shapes.forEach((s) => {
|
| 83 |
-
s.mesh.
|
| 84 |
-
s.mesh.
|
| 85 |
-
|
| 86 |
-
s.mesh.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
});
|
| 88 |
|
| 89 |
-
// Parallax camera
|
| 90 |
camera.position.x += (mx * 2 - camera.position.x) * 0.03;
|
| 91 |
camera.position.y += (-my * 1.5 - camera.position.y) * 0.03;
|
| 92 |
camera.lookAt(0, 0, 0);
|
| 93 |
|
| 94 |
-
particles.rotation.y += 0.
|
| 95 |
particles.rotation.x += 0.0002;
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
renderer.render(scene, camera);
|
| 98 |
};
|
| 99 |
animate();
|
|
@@ -110,6 +301,7 @@ export default function ParallaxScene() {
|
|
| 110 |
cancelAnimationFrame(raf);
|
| 111 |
window.removeEventListener("mousemove", onMouseMove);
|
| 112 |
window.removeEventListener("resize", onResize);
|
|
|
|
| 113 |
el.removeChild(renderer.domElement);
|
| 114 |
renderer.dispose();
|
| 115 |
};
|
|
@@ -122,7 +314,6 @@ export default function ParallaxScene() {
|
|
| 122 |
position: "absolute",
|
| 123 |
inset: 0,
|
| 124 |
zIndex: 0,
|
| 125 |
-
pointerEvents: "none",
|
| 126 |
}}
|
| 127 |
/>
|
| 128 |
);
|
|
|
|
| 3 |
import { useEffect, useRef } from "react";
|
| 4 |
import * as THREE from "three";
|
| 5 |
|
| 6 |
+
interface Shape {
|
| 7 |
+
mesh: THREE.Mesh;
|
| 8 |
+
vx: number;
|
| 9 |
+
vy: number;
|
| 10 |
+
vz: number;
|
| 11 |
+
rvx: number;
|
| 12 |
+
rvy: number;
|
| 13 |
+
baseX: number;
|
| 14 |
+
baseY: number;
|
| 15 |
+
baseZ: number;
|
| 16 |
+
speed: number;
|
| 17 |
+
scatterX: number;
|
| 18 |
+
scatterY: number;
|
| 19 |
+
scatterZ: number;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
export default function ParallaxScene() {
|
| 23 |
const mountRef = useRef<HTMLDivElement>(null);
|
| 24 |
const mouseRef = useRef({ x: 0, y: 0 });
|
|
|
|
| 37 |
renderer.setClearColor(0x000000, 0);
|
| 38 |
el.appendChild(renderer.domElement);
|
| 39 |
|
| 40 |
+
const blueMat = new THREE.MeshBasicMaterial({ color: 0x1e90ff, wireframe: true, transparent: true, opacity: 0.15 });
|
| 41 |
+
const blueSolid = new THREE.MeshBasicMaterial({ color: 0x1e90ff, transparent: true, opacity: 0.06 });
|
| 42 |
+
const accentMat = new THREE.MeshBasicMaterial({ color: 0x64b5f6, wireframe: true, transparent: true, opacity: 0.1 });
|
| 43 |
+
const accentSolid = new THREE.MeshBasicMaterial({ color: 0x64b5f6, transparent: true, opacity: 0.04 });
|
| 44 |
+
const lineBlue = new THREE.LineBasicMaterial({ color: 0x1e90ff, transparent: true, opacity: 0.12 });
|
| 45 |
+
const lineAccent = new THREE.LineBasicMaterial({ color: 0x64b5f6, transparent: true, opacity: 0.08 });
|
| 46 |
|
| 47 |
+
const shapes: Shape[] = [];
|
| 48 |
|
| 49 |
+
const addShape = (geo: THREE.BufferGeometry, mat: THREE.Material | THREE.LineBasicMaterial, x: number, y: number, z: number, speed: number) => {
|
| 50 |
+
const mesh = "isLine" in mat ? new THREE.Line(geo as THREE.BufferGeometry, mat as THREE.LineBasicMaterial) : new THREE.Mesh(geo as THREE.BufferGeometry, mat as THREE.MeshBasicMaterial);
|
| 51 |
mesh.position.set(x, y, z);
|
| 52 |
+
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
|
| 53 |
scene.add(mesh);
|
| 54 |
+
shapes.push({
|
| 55 |
+
mesh: mesh as unknown as THREE.Mesh,
|
| 56 |
+
vx: (Math.random() - 0.5) * 0.008 * speed,
|
| 57 |
+
vy: (Math.random() - 0.5) * 0.006 * speed,
|
| 58 |
+
vz: (Math.random() - 0.5) * 0.004 * speed,
|
| 59 |
+
rvx: (Math.random() - 0.5) * 0.01 * speed,
|
| 60 |
+
rvy: (Math.random() - 0.5) * 0.012 * speed,
|
| 61 |
+
baseX: x,
|
| 62 |
+
baseY: y,
|
| 63 |
+
baseZ: z,
|
| 64 |
+
speed,
|
| 65 |
+
scatterX: 0,
|
| 66 |
+
scatterY: 0,
|
| 67 |
+
scatterZ: 0,
|
| 68 |
+
});
|
| 69 |
};
|
| 70 |
|
| 71 |
+
const spread = 35;
|
| 72 |
+
const spreadY = 20;
|
| 73 |
+
|
| 74 |
+
// === LARGE BACKGROUND SHAPES (7) ===
|
| 75 |
+
addShape(new THREE.IcosahedronGeometry(5, 0), blueSolid, -spread, spreadY, -20, 0.3);
|
| 76 |
+
addShape(new THREE.OctahedronGeometry(4, 0), blueMat, spread, -spreadY * 0.8, -18, 0.25);
|
| 77 |
+
addShape(new THREE.TetrahedronGeometry(3.5, 0), accentMat, -spread * 0.7, -spreadY, -16, 0.35);
|
| 78 |
+
addShape(new THREE.IcosahedronGeometry(3, 1), blueMat, spread * 0.8, spreadY * 0.7, -14, 0.4);
|
| 79 |
+
addShape(new THREE.BoxGeometry(4, 4, 4), blueSolid, 0, -spreadY * 0.6, -22, 0.2);
|
| 80 |
+
addShape(new THREE.DodecahedronGeometry(3.5, 0), accentMat, -spread * 0.9, spreadY * 0.3, -17, 0.28);
|
| 81 |
+
addShape(new THREE.IcosahedronGeometry(2.5, 0), blueSolid, spread * 0.95, -spreadY * 0.4, -19, 0.32);
|
| 82 |
+
|
| 83 |
+
// === MID-LAYER SHAPES (14) ===
|
| 84 |
+
addShape(new THREE.OctahedronGeometry(2.2, 0), blueMat, -spread * 0.5, spreadY * 0.6, -6, 0.5);
|
| 85 |
+
addShape(new THREE.TetrahedronGeometry(2, 0), accentMat, spread * 0.4, spreadY * 0.3, -5, 0.55);
|
| 86 |
+
addShape(new THREE.IcosahedronGeometry(1.8, 0), blueSolid, -spread * 0.3, -spreadY * 0.5, -4, 0.45);
|
| 87 |
+
addShape(new THREE.BoxGeometry(1.8, 1.8, 1.8), accentMat, spread * 0.35, -spreadY * 0.4, -7, 0.48);
|
| 88 |
+
addShape(new THREE.DodecahedronGeometry(1.6, 0), blueMat, -spread * 0.75, spreadY * 0.15, -8, 0.52);
|
| 89 |
+
addShape(new THREE.OctahedronGeometry(1.4, 0), accentSolid, spread * 0.85, spreadY * 0.5, -6, 0.6);
|
| 90 |
+
addShape(new THREE.TetrahedronGeometry(1.2, 0), blueMat, -spread * 0.15, spreadY * 0.8, -5, 0.58);
|
| 91 |
+
addShape(new THREE.IcosahedronGeometry(1.5, 0), blueSolid, spread * 0.2, -spreadY * 0.7, -9, 0.42);
|
| 92 |
+
addShape(new THREE.BoxGeometry(1.4, 1.4, 1.4), accentMat, -spread * 0.6, -spreadY * 0.2, -7, 0.5);
|
| 93 |
+
addShape(new THREE.OctahedronGeometry(1.3, 0), blueMat, spread * 0.65, spreadY * 0.1, -4, 0.53);
|
| 94 |
+
addShape(new THREE.DodecahedronGeometry(1.1, 0), accentMat, -spread * 0.4, spreadY * 0.45, -8, 0.47);
|
| 95 |
+
addShape(new THREE.TetrahedronGeometry(1, 0), blueSolid, spread * 0.5, -spreadY * 0.55, -6, 0.56);
|
| 96 |
+
addShape(new THREE.IcosahedronGeometry(1.2, 0), blueMat, -spread * 0.25, -spreadY * 0.35, -5, 0.44);
|
| 97 |
+
addShape(new THREE.BoxGeometry(1, 1, 1), accentMat, spread * 0.15, spreadY * 0.65, -7, 0.51);
|
| 98 |
+
|
| 99 |
+
// === FRONT LAYER - SMALL FAST (18) ===
|
| 100 |
+
for (let i = 0; i < 18; i++) {
|
| 101 |
+
const geos = [
|
| 102 |
+
new THREE.BoxGeometry(0.5, 0.5, 0.5),
|
| 103 |
+
new THREE.OctahedronGeometry(0.4, 0),
|
| 104 |
+
new THREE.TetrahedronGeometry(0.35, 0),
|
| 105 |
+
new THREE.IcosahedronGeometry(0.3, 0),
|
| 106 |
+
new THREE.DodecahedronGeometry(0.35, 0),
|
| 107 |
+
];
|
| 108 |
+
const mats = [blueMat, accentMat, blueSolid, accentSolid];
|
| 109 |
+
const x = (Math.random() - 0.5) * spread * 2;
|
| 110 |
+
const y = (Math.random() - 0.5) * spreadY * 2;
|
| 111 |
+
const z = Math.random() * 8 - 2;
|
| 112 |
+
addShape(geos[i % geos.length], mats[i % mats.length], x, y, z, 0.7 + Math.random() * 0.5);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// === TINY SCATTERED SHAPES (20) ===
|
| 116 |
+
for (let i = 0; i < 20; i++) {
|
| 117 |
+
const geos = [
|
| 118 |
+
new THREE.BoxGeometry(0.2, 0.2, 0.2),
|
| 119 |
+
new THREE.TetrahedronGeometry(0.15, 0),
|
| 120 |
+
new THREE.OctahedronGeometry(0.18, 0),
|
| 121 |
+
];
|
| 122 |
+
const mats = [accentMat, blueSolid, accentSolid];
|
| 123 |
+
const x = (Math.random() - 0.5) * spread * 2;
|
| 124 |
+
const y = (Math.random() - 0.5) * spreadY * 2;
|
| 125 |
+
const z = Math.random() * 10;
|
| 126 |
+
addShape(geos[i % geos.length], mats[i % mats.length], x, y, z, 0.9 + Math.random() * 0.4);
|
| 127 |
+
}
|
| 128 |
|
| 129 |
+
// === CV-RELATED: Bounding box outlines (6) ===
|
| 130 |
+
for (let i = 0; i < 6; i++) {
|
| 131 |
+
const size = 1.5 + Math.random() * 3;
|
| 132 |
+
const corners = new Float32Array([
|
| 133 |
+
-size, -size, 0, size, -size, 0,
|
| 134 |
+
size, -size, 0, size, size, 0,
|
| 135 |
+
size, size, 0, -size, size, 0,
|
| 136 |
+
-size, size, 0, -size, -size, 0,
|
| 137 |
+
]);
|
| 138 |
+
const geo = new THREE.BufferGeometry();
|
| 139 |
+
geo.setAttribute("position", new THREE.BufferAttribute(corners, 3));
|
| 140 |
+
const x = (Math.random() - 0.5) * spread * 1.8;
|
| 141 |
+
const y = (Math.random() - 0.5) * spreadY * 1.8;
|
| 142 |
+
const z = Math.random() * 6 - 3;
|
| 143 |
+
addShape(geo, i % 2 === 0 ? lineBlue : lineAccent, x, y, z, 0.4 + Math.random() * 0.3);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// === CV-RELATED: Crosshairs (4) ===
|
| 147 |
+
for (let i = 0; i < 4; i++) {
|
| 148 |
+
const s = 0.8 + Math.random() * 1.5;
|
| 149 |
+
const pts = new Float32Array([
|
| 150 |
+
-s, 0, 0, s, 0, 0,
|
| 151 |
+
0, -s, 0, 0, s, 0,
|
| 152 |
+
]);
|
| 153 |
+
const geo = new THREE.BufferGeometry();
|
| 154 |
+
geo.setAttribute("position", new THREE.BufferAttribute(pts, 3));
|
| 155 |
+
const x = (Math.random() - 0.5) * spread * 1.6;
|
| 156 |
+
const y = (Math.random() - 0.5) * spreadY * 1.6;
|
| 157 |
+
const z = Math.random() * 5 - 1;
|
| 158 |
+
addShape(geo, lineBlue, x, y, z, 0.6 + Math.random() * 0.3);
|
| 159 |
+
}
|
| 160 |
|
| 161 |
+
// === CV-RELATED: Diamond detection markers (4) ===
|
| 162 |
+
for (let i = 0; i < 4; i++) {
|
| 163 |
+
const s = 0.4 + Math.random() * 0.8;
|
| 164 |
+
const pts = new Float32Array([
|
| 165 |
+
0, s, 0, s, 0, 0,
|
| 166 |
+
s, 0, 0, 0, -s, 0,
|
| 167 |
+
0, -s, 0, -s, 0, 0,
|
| 168 |
+
-s, 0, 0, 0, s, 0,
|
| 169 |
+
]);
|
| 170 |
+
const geo = new THREE.BufferGeometry();
|
| 171 |
+
geo.setAttribute("position", new THREE.BufferAttribute(pts, 3));
|
| 172 |
+
const x = (Math.random() - 0.5) * spread * 1.5;
|
| 173 |
+
const y = (Math.random() - 0.5) * spreadY * 1.5;
|
| 174 |
+
const z = Math.random() * 4;
|
| 175 |
+
addShape(geo, lineAccent, x, y, z, 0.5 + Math.random() * 0.3);
|
| 176 |
+
}
|
| 177 |
|
| 178 |
+
// === PARTICLES (300) ===
|
| 179 |
const particleGeo = new THREE.BufferGeometry();
|
| 180 |
+
const particleCount = 300;
|
| 181 |
const positions = new Float32Array(particleCount * 3);
|
| 182 |
+
const sizes = new Float32Array(particleCount);
|
| 183 |
for (let i = 0; i < particleCount; i++) {
|
| 184 |
+
positions[i * 3] = (Math.random() - 0.5) * 90;
|
| 185 |
+
positions[i * 3 + 1] = (Math.random() - 0.5) * 60;
|
| 186 |
+
positions[i * 3 + 2] = (Math.random() - 0.5) * 35 - 5;
|
| 187 |
+
sizes[i] = 0.08 + Math.random() * 0.12;
|
| 188 |
}
|
| 189 |
particleGeo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
| 190 |
+
particleGeo.setAttribute("size", new THREE.BufferAttribute(sizes, 1));
|
| 191 |
+
const particleMat = new THREE.PointsMaterial({ color: 0x1e90ff, size: 0.14, transparent: true, opacity: 0.5 });
|
| 192 |
const particles = new THREE.Points(particleGeo, particleMat);
|
| 193 |
scene.add(particles);
|
| 194 |
|
| 195 |
+
// === TWINKLING STARS (120) ===
|
| 196 |
+
const starGeo = new THREE.BufferGeometry();
|
| 197 |
+
const starCount = 120;
|
| 198 |
+
const starPositions = new Float32Array(starCount * 3);
|
| 199 |
+
const starPhases = new Float32Array(starCount);
|
| 200 |
+
for (let i = 0; i < starCount; i++) {
|
| 201 |
+
starPositions[i * 3] = (Math.random() - 0.5) * 100;
|
| 202 |
+
starPositions[i * 3 + 1] = (Math.random() - 0.5) * 70;
|
| 203 |
+
starPositions[i * 3 + 2] = (Math.random() - 0.5) * 20 - 10;
|
| 204 |
+
starPhases[i] = Math.random() * Math.PI * 2;
|
| 205 |
+
}
|
| 206 |
+
starGeo.setAttribute("position", new THREE.BufferAttribute(starPositions, 3));
|
| 207 |
+
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.08, transparent: true, opacity: 0.6 });
|
| 208 |
+
const stars = new THREE.Points(starGeo, starMat);
|
| 209 |
+
scene.add(stars);
|
| 210 |
+
|
| 211 |
+
// Mouse
|
| 212 |
const onMouseMove = (e: MouseEvent) => {
|
| 213 |
const rect = el.getBoundingClientRect();
|
| 214 |
mouseRef.current.x = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
|
|
|
|
| 216 |
};
|
| 217 |
window.addEventListener("mousemove", onMouseMove);
|
| 218 |
|
| 219 |
+
// Click scatter
|
| 220 |
+
const onClick = (e: MouseEvent) => {
|
| 221 |
+
const rect = el.getBoundingClientRect();
|
| 222 |
+
const cx = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
|
| 223 |
+
const cy = -((e.clientY - rect.top) / rect.height - 0.5) * 2;
|
| 224 |
+
shapes.forEach((s) => {
|
| 225 |
+
const dx = s.mesh.position.x - cx * 15;
|
| 226 |
+
const dy = s.mesh.position.y - cy * 10;
|
| 227 |
+
const dist = Math.sqrt(dx * dx + dy * dy) + 1;
|
| 228 |
+
const force = 4 / dist;
|
| 229 |
+
s.scatterX += (dx / dist) * force * s.speed;
|
| 230 |
+
s.scatterY += (dy / dist) * force * s.speed;
|
| 231 |
+
s.scatterZ += (Math.random() - 0.5) * force;
|
| 232 |
+
});
|
| 233 |
+
};
|
| 234 |
+
el.addEventListener("click", onClick);
|
| 235 |
+
|
| 236 |
let raf: number;
|
| 237 |
const animate = () => {
|
| 238 |
raf = requestAnimationFrame(animate);
|
|
|
|
| 239 |
const mx = mouseRef.current.x;
|
| 240 |
const my = mouseRef.current.y;
|
| 241 |
+
const t = Date.now();
|
| 242 |
|
| 243 |
shapes.forEach((s) => {
|
| 244 |
+
s.mesh.position.x += s.vx + Math.sin(t * 0.0003 * s.speed + s.baseX) * 0.006;
|
| 245 |
+
s.mesh.position.y += s.vy + Math.cos(t * 0.0004 * s.speed + s.baseY) * 0.005;
|
| 246 |
+
s.mesh.position.z += s.vz;
|
| 247 |
+
s.mesh.rotation.x += s.rvx;
|
| 248 |
+
s.mesh.rotation.y += s.rvy;
|
| 249 |
+
|
| 250 |
+
s.scatterX *= 0.95;
|
| 251 |
+
s.scatterY *= 0.95;
|
| 252 |
+
s.scatterZ *= 0.95;
|
| 253 |
+
s.mesh.position.x += s.scatterX;
|
| 254 |
+
s.mesh.position.y += s.scatterY;
|
| 255 |
+
s.mesh.position.z += s.scatterZ;
|
| 256 |
+
|
| 257 |
+
const dx = s.mesh.position.x - mx * 15;
|
| 258 |
+
const dy = s.mesh.position.y - (-my) * 10;
|
| 259 |
+
const dist = Math.sqrt(dx * dx + dy * dy) + 1;
|
| 260 |
+
if (dist < 8) {
|
| 261 |
+
const push = (8 - dist) * 0.015 * s.speed;
|
| 262 |
+
s.mesh.position.x += (dx / dist) * push;
|
| 263 |
+
s.mesh.position.y += (dy / dist) * push;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
const bx = spread + 5, by = spreadY + 5, bz = 25;
|
| 267 |
+
if (s.mesh.position.x > bx) s.mesh.position.x = -bx;
|
| 268 |
+
if (s.mesh.position.x < -bx) s.mesh.position.x = bx;
|
| 269 |
+
if (s.mesh.position.y > by) s.mesh.position.y = -by;
|
| 270 |
+
if (s.mesh.position.y < -by) s.mesh.position.y = by;
|
| 271 |
+
if (s.mesh.position.z > 10) s.mesh.position.z = -bz;
|
| 272 |
+
if (s.mesh.position.z < -bz) s.mesh.position.z = 10;
|
| 273 |
});
|
| 274 |
|
|
|
|
| 275 |
camera.position.x += (mx * 2 - camera.position.x) * 0.03;
|
| 276 |
camera.position.y += (-my * 1.5 - camera.position.y) * 0.03;
|
| 277 |
camera.lookAt(0, 0, 0);
|
| 278 |
|
| 279 |
+
particles.rotation.y += 0.0004;
|
| 280 |
particles.rotation.x += 0.0002;
|
| 281 |
|
| 282 |
+
// Twinkle stars
|
| 283 |
+
const starPos = stars.geometry.attributes.position as THREE.BufferAttribute;
|
| 284 |
+
for (let i = 0; i < starCount; i++) {
|
| 285 |
+
starMat.opacity = 0.3 + Math.sin(t * 0.002 + starPhases[i]) * 0.35;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
renderer.render(scene, camera);
|
| 289 |
};
|
| 290 |
animate();
|
|
|
|
| 301 |
cancelAnimationFrame(raf);
|
| 302 |
window.removeEventListener("mousemove", onMouseMove);
|
| 303 |
window.removeEventListener("resize", onResize);
|
| 304 |
+
el.removeEventListener("click", onClick);
|
| 305 |
el.removeChild(renderer.domElement);
|
| 306 |
renderer.dispose();
|
| 307 |
};
|
|
|
|
| 314 |
position: "absolute",
|
| 315 |
inset: 0,
|
| 316 |
zIndex: 0,
|
|
|
|
| 317 |
}}
|
| 318 |
/>
|
| 319 |
);
|
frontend/components/StaggeredMenu.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.sm-wrap{position:fixed;inset:0;width:100dvw;height:100dvh;z-index:55;pointer-events:none;overflow:hidden}.sm-layers,.sm-panel{position:absolute;inset:0;left:auto;width:100%;height:100%;pointer-events:none;transform:translateX(100%)}.sm-layers{z-index:1}.sm-layer{position:absolute;inset:0;background:#fff}.sm-cream{background:#fff1e4}.sm-peach{background:#ffcf9e}.sm-orange{background:#ff7a00}.sm-panel{z-index:2;background:#fff;pointer-events:auto}.sm-inner{height:100%;padding:7.5rem 2rem 2rem;display:flex;flex-direction:column}.sm-kicker{font-size:.7rem;letter-spacing:.32em;font-weight:800;color:#ff7a00;margin:0 0 2.5rem}.sm-item-wrap{overflow:hidden;border-bottom:1px solid rgba(255,122,0,.2)}.sm-item{position:relative;display:inline-block;padding:1rem 3rem 1rem 0;color:#1d1712;text-decoration:none;font-family:Impact,Haettenschweiler,sans-serif;font-size:clamp(2.6rem,12vw,4.6rem);line-height:.95;letter-spacing:.03em}.sm-item:after{content:attr(data-number);position:absolute;right:.25rem;top:1rem;font:500 .7rem ui-monospace;color:#ff7a00}.sm-item:hover{color:#ff7a00}.sm-foot{margin-top:auto;color:#78695e;font-size:.65rem;letter-spacing:.13em;line-height:1.8}.sm-foot b{color:#ff7a00}.sm-toggle{position:fixed;z-index:60;top:15px;right:16px;width:38px;height:38px;border:0;background:transparent;pointer-events:auto;cursor:pointer;display:grid;place-content:center;gap:5px}.sm-toggle i{display:block;width:22px;height:2px;background:#1d1712;border-radius:2px}.sm-wrap[data-open] .sm-toggle i:nth-child(1){transform:translateY(7px) rotate(45deg)}.sm-wrap[data-open] .sm-toggle i:nth-child(2){opacity:0}.sm-wrap[data-open] .sm-toggle i:nth-child(3){transform:translateY(-7px) rotate(-45deg)}@media(min-width:768px){.sm-panel,.sm-layers{width:440px}.sm-toggle{right:calc((100vw - min(1200px,92vw))/2)} }
|
|
|
|
|
|
frontend/components/StaggeredMenu.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
import Link from "next/link";
|
| 3 |
-
import { gsap } from "gsap";
|
| 4 |
-
import { useLayoutEffect, useRef, useState, useEffect } from "react";
|
| 5 |
-
import "./StaggeredMenu.css";
|
| 6 |
-
type Item = { label: string; ariaLabel: string; link: string };
|
| 7 |
-
export default function StaggeredMenu({ items }: { items: Item[] }) {
|
| 8 |
-
const [open, setOpen] = useState(false);
|
| 9 |
-
const [mounted, setMounted] = useState(false);
|
| 10 |
-
const panel = useRef<HTMLDivElement>(null);
|
| 11 |
-
const layers = useRef<HTMLDivElement>(null);
|
| 12 |
-
const button = useRef<HTMLButtonElement>(null);
|
| 13 |
-
const labels = useRef<HTMLSpanElement[]>([]);
|
| 14 |
-
|
| 15 |
-
useEffect(() => {
|
| 16 |
-
setMounted(true);
|
| 17 |
-
}, []);
|
| 18 |
-
|
| 19 |
-
useLayoutEffect(() => {
|
| 20 |
-
if (!mounted) return;
|
| 21 |
-
const all = [panel.current, ...Array.from(layers.current?.children || [])].filter(Boolean);
|
| 22 |
-
gsap.set(all, { xPercent: 100 });
|
| 23 |
-
gsap.set(labels.current, { yPercent: 140, rotate: 10 });
|
| 24 |
-
}, [mounted]);
|
| 25 |
-
|
| 26 |
-
function toggle() {
|
| 27 |
-
const next = !open;
|
| 28 |
-
setOpen(next);
|
| 29 |
-
const layerEls = Array.from(layers.current?.children || []);
|
| 30 |
-
if (next) {
|
| 31 |
-
const tl = gsap.timeline();
|
| 32 |
-
tl.to(layerEls, { xPercent: 0, duration: .5, ease: "power4.out", stagger: .07 })
|
| 33 |
-
.to(panel.current, { xPercent: 0, duration: .65, ease: "power4.out" }, .15)
|
| 34 |
-
.to(labels.current, { yPercent: 0, rotate: 0, duration: 1, ease: "power4.out", stagger: .1 }, .35)
|
| 35 |
-
.to(button.current, { rotate: 180, duration: .5, ease: "power4.out" }, 0);
|
| 36 |
-
} else {
|
| 37 |
-
gsap.to([panel.current, ...layerEls], { xPercent: 100, duration: .32, ease: "power3.in" });
|
| 38 |
-
gsap.to(labels.current, { yPercent: 140, rotate: 10, duration: .15 });
|
| 39 |
-
gsap.to(button.current, { rotate: 0, duration: .35 });
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
return (
|
| 44 |
-
<div className="sm-wrap" data-open={open || undefined} style={{ display: mounted ? undefined : 'none' }}>
|
| 45 |
-
<div ref={layers} className="sm-layers" aria-hidden="true">
|
| 46 |
-
<div className="sm-layer sm-cream" />
|
| 47 |
-
<div className="sm-layer sm-peach" />
|
| 48 |
-
<div className="sm-layer sm-orange" />
|
| 49 |
-
</div>
|
| 50 |
-
<aside ref={panel} className="sm-panel" aria-hidden={!open}>
|
| 51 |
-
<div className="sm-inner">
|
| 52 |
-
<p className="sm-kicker">FaceAI // NAVIGATION</p>
|
| 53 |
-
<nav>{items.map((item, i) => (
|
| 54 |
-
<div className="sm-item-wrap" key={item.link}>
|
| 55 |
-
<Link href={item.link} aria-label={item.ariaLabel} onClick={toggle} className="sm-item" data-number={`0${i + 1}`}>
|
| 56 |
-
<span ref={el => { if (el) labels.current[i] = el }}>{item.label}</span>
|
| 57 |
-
</Link>
|
| 58 |
-
</div>
|
| 59 |
-
))}</nav>
|
| 60 |
-
<p className="sm-foot">CONSENTED BIOMETRIC RECOGNITION<br/><b>FaceAI //</b></p>
|
| 61 |
-
</div>
|
| 62 |
-
</aside>
|
| 63 |
-
<button ref={button} className="sm-toggle" aria-label={open ? 'Close menu' : 'Open menu'} aria-expanded={open} onClick={toggle}>
|
| 64 |
-
<i /><i /><i />
|
| 65 |
-
</button>
|
| 66 |
-
</div>
|
| 67 |
-
);
|
| 68 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|