feat: add architecture section with animated illustrations

- Created HomeArchitecture component showcasing decentralized network principles with SVG animations
- Added animated illustrations for mesh networking, no extraction, no control, and no central servers
- Enhanced CallToAction with cyan radial glow effect for visual emphasis
- Fixed audience gallery image paths to use correct /images/audiences/ directory
This commit is contained in:
2025-11-12 15:47:58 +01:00
parent 0d9f357881
commit eba1bb7047
19 changed files with 1440 additions and 51 deletions

View File

@@ -0,0 +1,247 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import clsx from "clsx";
type Props = {
className?: string;
accent?: string;
bg?: string;
};
const W = 760;
const H = 420;
/** Node = local data cluster */
const Node = ({
x,
y,
r = 10,
accent = "#00b8db",
pulse = false,
delay = 0,
}: {
x: number;
y: number;
r?: number;
accent?: string;
pulse?: boolean;
delay?: number;
}) => {
const prefers = useReducedMotion();
return (
<>
{/* outer glow */}
<motion.circle
cx={x}
cy={y}
r={r + 8}
stroke="#1F2937"
strokeWidth={2}
fill="none"
initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }}
transition={{ delay, duration: 0.6 }}
/>
{/* data core */}
<motion.circle
cx={x}
cy={y}
r={r}
fill={accent}
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: pulse && !prefers ? [1, 1.15, 1] : 1,
}}
transition={{
delay,
duration: pulse && !prefers ? 1.6 : 0.6,
repeat: pulse && !prefers ? Infinity : 0,
repeatType: "mirror",
ease: [0.22, 1, 0.36, 1],
}}
/>
</>
);
};
/** A data particle traveling along a given path */
const Particle = ({
path,
delay = 0,
accent = "#00b8db",
duration = 2,
reverse = false,
}: {
path: string;
delay?: number;
accent?: string;
duration?: number;
reverse?: boolean;
}) => {
const prefers = useReducedMotion();
return (
<motion.circle
r={4}
fill={accent}
initial={{ offsetDistance: reverse ? "100%" : "0%", opacity: 0 }}
animate={{
offsetDistance: reverse ? ["100%", "0%"] : ["0%", "100%"],
opacity: [0, 1, 0],
}}
transition={{
delay,
duration,
repeat: !prefers ? Infinity : 0,
repeatType: "mirror",
ease: "easeInOut",
}}
style={{
offsetPath: `path('${path}')`,
}}
/>
);
};
export default function NoExtraction({
className,
accent = "#00b8db",
bg = "#0a0a0a",
}: Props) {
const center = { x: 380, y: 210 };
const WBOX = 360;
const HBOX = 220;
const boxX = center.x - WBOX / 2;
const boxY = center.y - HBOX / 2;
// local nodes within boundary
const nodes = [
{ x: center.x - 80, y: center.y - 40 },
{ x: center.x + 60, y: center.y - 50 },
{ x: center.x, y: center.y + 50 },
{ x: center.x - 50, y: center.y + 30 },
];
// internal circulation paths
const internalPaths = [
`M ${center.x - 80} ${center.y - 40} Q ${center.x} ${center.y - 80} ${center.x + 60} ${center.y - 50}`,
`M ${center.x - 50} ${center.y + 30} Q ${center.x} ${center.y + 70} ${center.x} ${center.y + 50}`,
];
// escape attempt path
const attemptPath = `M ${center.x} ${center.y} Q ${center.x + 200} ${center.y - 50} ${center.x + 130} ${center.y}`;
return (
<div
className={clsx("relative overflow-hidden", className)}
aria-hidden="true"
role="img"
aria-label="No data extraction illustration"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
{/* Background grid */}
<defs>
<pattern id="grid-dark" width="28" height="28" patternUnits="userSpaceOnUse">
<path d="M 28 0 L 0 0 0 28" fill="none" stroke="#0f0f0f" strokeWidth="1" />
</pattern>
{/* Cyan glow gradient */}
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor={accent} stopOpacity="0.15" />
<stop offset="100%" stopColor={accent} stopOpacity="0" />
</radialGradient>
</defs>
<rect width={W} height={H} fill="url(#grid-dark)" />
{/* Cyan ambient glow field */}
<circle
cx={center.x}
cy={center.y}
r={160}
fill="url(#glow)"
opacity={0.3}
/>
{/* Secure boundary box */}
<motion.rect
x={boxX}
y={boxY}
width={WBOX}
height={HBOX}
rx={16}
stroke="#1F2937"
strokeWidth={3}
fill="none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
/>
{/* Cyan encryption border */}
<motion.rect
x={boxX + 4}
y={boxY + 4}
width={WBOX - 8}
height={HBOX - 8}
rx={14}
stroke={accent}
strokeWidth={2}
strokeDasharray="10 6"
fill="none"
animate={{ opacity: [0.4, 0.9, 0.4] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Internal data circulation */}
{internalPaths.map((p, i) => (
<Particle
key={`int-${i}`}
path={p}
delay={i * 0.6}
accent={accent}
duration={3}
/>
))}
{/* Local data nodes */}
{nodes.map((n, i) => (
<Node
key={i}
x={n.x}
y={n.y}
r={10}
accent={accent}
pulse={i === 2}
delay={i * 0.2}
/>
))}
{/* Data packet attempting to leave */}
<Particle path={attemptPath} delay={0.5} accent={accent} duration={2.2} />
{/* Impact glow at boundary */}
<motion.circle
cx={center.x + 130}
cy={center.y - 10}
r={10}
fill="#FF4D4D"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: [0, 1.2, 0.8], opacity: [0, 1, 0] }}
transition={{
delay: 0.9,
duration: 1.4,
repeat: Infinity,
repeatDelay: 1.5,
ease: "easeOut",
}}
/>
</svg>
</div>
);
}