forked from emre/www_projectmycelium_com
refactor: rename and update pod animations with improved visuals and consistency
- Renamed NoCentral to DataControl with enhanced data ownership visualization - Renamed NoExtraction to Connectivity with full mesh P2P topology - Renamed NoSinglePoint to Security with improved styling - Renamed NoControl to Resilience with consistent styling - Added lg:-mt-12 margin to Security and Resilience animations for alignment - Updated DataControl to show explicit permission gates and bi-directional replication
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Eyebrow, H3, P } from "@/components/Texts";
|
import { Eyebrow, H3, P } from "@/components/Texts";
|
||||||
import NoExtraction from "./animations/NoExtraction";
|
import DataControl from "./animations/DataControl";
|
||||||
import NoControl from "./animations/NoControl";
|
import Connectivity from "./animations/Connectivity";
|
||||||
import NoCentral from "./animations/NoCentral";
|
import Security from "./animations/Security";
|
||||||
import NoSinglePoint from "./animations/NoSinglePoint";
|
import Resilience from "./animations/Resilience";
|
||||||
|
|
||||||
const deterministicCards = [
|
const deterministicCards = [
|
||||||
{
|
{
|
||||||
@@ -26,7 +26,7 @@ const deterministicCards = [
|
|||||||
title: "Your Data Lives on Your Pods",
|
title: "Your Data Lives on Your Pods",
|
||||||
description:
|
description:
|
||||||
"Full control of where your data is stored and how it’s shared.",
|
"Full control of where your data is stored and how it’s shared.",
|
||||||
animation: <NoCentral className="lg:-mt-12" />,
|
animation: <DataControl className="lg:-mt-12" />,
|
||||||
colSpan: "lg:col-span-3",
|
colSpan: "lg:col-span-3",
|
||||||
rowSpan: "lg:row-span-1",
|
rowSpan: "lg:row-span-1",
|
||||||
rounded: "lg:rounded-tr-4xl max-lg:rounded-t-4xl",
|
rounded: "lg:rounded-tr-4xl max-lg:rounded-t-4xl",
|
||||||
@@ -40,7 +40,7 @@ const deterministicCards = [
|
|||||||
title: "Direct Pod-to-Pod Networking",
|
title: "Direct Pod-to-Pod Networking",
|
||||||
description:
|
description:
|
||||||
"Direct connections between Pods for faster, private communication.",
|
"Direct connections between Pods for faster, private communication.",
|
||||||
animation: <NoExtraction className="lg:-mt-12" />,
|
animation: <Connectivity className="lg:-mt-12" />,
|
||||||
colSpan: "lg:col-span-2",
|
colSpan: "lg:col-span-2",
|
||||||
rowSpan: "lg:row-span-1",
|
rowSpan: "lg:row-span-1",
|
||||||
rounded: "lg:rounded-bl-4xl max-lg:rounded-b-4xl",
|
rounded: "lg:rounded-bl-4xl max-lg:rounded-b-4xl",
|
||||||
@@ -54,7 +54,7 @@ const deterministicCards = [
|
|||||||
title: "No One Can Spy or Shut You Down",
|
title: "No One Can Spy or Shut You Down",
|
||||||
description:
|
description:
|
||||||
"Independence from corporate servers or cloud outages.",
|
"Independence from corporate servers or cloud outages.",
|
||||||
animation: <NoSinglePoint />,
|
animation: <Security className="lg:-mt-12" />,
|
||||||
colSpan: "lg:col-span-2",
|
colSpan: "lg:col-span-2",
|
||||||
rowSpan: "lg:row-span-1",
|
rowSpan: "lg:row-span-1",
|
||||||
rounded: "",
|
rounded: "",
|
||||||
@@ -67,7 +67,7 @@ const deterministicCards = [
|
|||||||
title: "Resilient Even if Nodes Disconnect",
|
title: "Resilient Even if Nodes Disconnect",
|
||||||
description:
|
description:
|
||||||
"Continuous availability even if one node disconnects.",
|
"Continuous availability even if one node disconnects.",
|
||||||
animation: <NoControl />,
|
animation: <Resilience className="lg:-mt-12" />,
|
||||||
colSpan: "lg:col-span-2",
|
colSpan: "lg:col-span-2",
|
||||||
rowSpan: "lg:row-span-1",
|
rowSpan: "lg:row-span-1",
|
||||||
rounded: "lg:rounded-br-4xl max-lg:rounded-b-4xl",
|
rounded: "lg:rounded-br-4xl max-lg:rounded-b-4xl",
|
||||||
|
|||||||
176
src/pages/pods/animations/Connectivity.tsx
Normal file
176
src/pages/pods/animations/Connectivity.tsx
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
accent?: string;
|
||||||
|
bg?: string;
|
||||||
|
gridStroke?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const W = 720; // 4:3 width
|
||||||
|
const H = 540; // 4:3 height
|
||||||
|
|
||||||
|
export default function Connectivity({
|
||||||
|
className,
|
||||||
|
accent = "#00b8db",
|
||||||
|
bg = "#0b0b0b",
|
||||||
|
gridStroke = "#2b2a2a",
|
||||||
|
}: Props) {
|
||||||
|
const prefers = useReducedMotion();
|
||||||
|
|
||||||
|
// Hex-like P2P topology — all peers connect *directly*
|
||||||
|
const pods = [
|
||||||
|
{ x: 250, y: 160 },
|
||||||
|
{ x: 470, y: 160 },
|
||||||
|
{ x: 580, y: 290 },
|
||||||
|
{ x: 470, y: 420 },
|
||||||
|
{ x: 250, y: 420 },
|
||||||
|
{ x: 140, y: 290 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Every pod connects to every other pod = full direct mesh
|
||||||
|
const links: [number, number][] = [];
|
||||||
|
for (let i = 0; i < pods.length; i++) {
|
||||||
|
for (let j = i + 1; j < pods.length; j++) links.push([i, j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = (i: number, j: number) => {
|
||||||
|
const a = pods[i], b = pods[j];
|
||||||
|
return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx("relative overflow-hidden", className)}>
|
||||||
|
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full" style={{ background: bg }}>
|
||||||
|
|
||||||
|
{/* ========= GRID + FILTERS ========= */}
|
||||||
|
<defs>
|
||||||
|
<pattern id="conn-grid" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||||
|
<path
|
||||||
|
d="M 28 0 L 0 0 0 28"
|
||||||
|
fill="none"
|
||||||
|
stroke={gridStroke}
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.35"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<filter id="conn-glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="b" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
{/* Cyan soft gradient for pods */}
|
||||||
|
<radialGradient id="conn-pod" cx="50%" cy="50%" r="60%">
|
||||||
|
<stop offset="0%" stopColor={accent} stopOpacity="0.9" />
|
||||||
|
<stop offset="100%" stopColor={accent} stopOpacity="0.06" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width={W} height={H} fill="url(#conn-grid)" />
|
||||||
|
|
||||||
|
{/* ========= BASE CONNECTION LINES (Full Mesh) ========= */}
|
||||||
|
{links.map(([i, j], idx) => (
|
||||||
|
<motion.path
|
||||||
|
key={`base-${idx}`}
|
||||||
|
d={line(i, j)}
|
||||||
|
stroke="#1f1f1f"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.2 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 0.5 }}
|
||||||
|
transition={{ duration: 0.6, delay: idx * 0.03 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ========= ACCENT DASH (Active P2P links) ========= */}
|
||||||
|
{links.map(([i, j], idx) => (
|
||||||
|
<motion.path
|
||||||
|
key={`dash-${idx}`}
|
||||||
|
d={line(i, j)}
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray="12 10"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.9 }}
|
||||||
|
animate={{ pathLength: 1, opacity: [0.9, 0.6, 0.9] }}
|
||||||
|
transition={{
|
||||||
|
duration: 1,
|
||||||
|
delay: idx * 0.04,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
filter="url(#conn-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ========= MOVING SIGNALS (Direct P2P) ========= */}
|
||||||
|
{!prefers &&
|
||||||
|
links.map(([i, j], idx) => (
|
||||||
|
<motion.circle
|
||||||
|
key={`pulse-${idx}`}
|
||||||
|
r={5}
|
||||||
|
fill={accent}
|
||||||
|
style={{ offsetPath: `path('${line(i, j)}')` }}
|
||||||
|
initial={{ offsetDistance: "0%", opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
offsetDistance: ["0%", "100%"],
|
||||||
|
opacity: [0.2, 1, 0.2],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2.3,
|
||||||
|
delay: idx * 0.2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
filter="url(#conn-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ========= CLOUD PODS ========= */}
|
||||||
|
{pods.map((p, i) => (
|
||||||
|
<g key={`pod-${i}`}>
|
||||||
|
{/* Outer soft cyan aura */}
|
||||||
|
{!prefers && (
|
||||||
|
<motion.ellipse
|
||||||
|
cx={p.x}
|
||||||
|
cy={p.y}
|
||||||
|
rx={42}
|
||||||
|
ry={28}
|
||||||
|
fill="none"
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
opacity={0.15}
|
||||||
|
animate={{ scale: [1, 1.1, 1] , opacity: [0.1, 0.25, 0.1] }}
|
||||||
|
transition={{ duration: 2.3, delay: i * 0.15, repeat: Infinity }}
|
||||||
|
filter="url(#conn-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pod body */}
|
||||||
|
<ellipse
|
||||||
|
cx={p.x}
|
||||||
|
cy={p.y}
|
||||||
|
rx={36}
|
||||||
|
ry={24}
|
||||||
|
fill="url(#conn-pod)"
|
||||||
|
stroke="#1f1f1f"
|
||||||
|
strokeWidth={2}
|
||||||
|
filter="url(#conn-glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Inner dark core */}
|
||||||
|
<ellipse cx={p.x} cy={p.y} rx={18} ry={12} fill="#0f0f0f" stroke="#292929" strokeWidth={1.5} />
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
239
src/pages/pods/animations/DataControl.tsx
Normal file
239
src/pages/pods/animations/DataControl.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
accent?: string; // cyan
|
||||||
|
bg?: string; // solid dark
|
||||||
|
gridStroke?: string; // grid color
|
||||||
|
};
|
||||||
|
|
||||||
|
const W = 720; // 4:3
|
||||||
|
const H = 540; // 4:3
|
||||||
|
|
||||||
|
export default function DataControl({
|
||||||
|
className,
|
||||||
|
accent = "#00b8db",
|
||||||
|
bg = "#0b0b0b",
|
||||||
|
gridStroke = "#2b2a2a",
|
||||||
|
}: Props) {
|
||||||
|
const prefers = useReducedMotion();
|
||||||
|
|
||||||
|
// Central "Your Pod"
|
||||||
|
const center = { x: W / 2, y: H / 2 + 10 };
|
||||||
|
|
||||||
|
// Personal pods (owned) — left-bottom cluster
|
||||||
|
const owned = [
|
||||||
|
{ x: 200, y: 380 },
|
||||||
|
{ x: 260, y: 320 },
|
||||||
|
{ x: 140, y: 320 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// External peers (share targets) — top-right cluster
|
||||||
|
const peers = [
|
||||||
|
{ x: 520, y: 180 },
|
||||||
|
{ x: 600, y: 140 },
|
||||||
|
{ x: 580, y: 240 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// helper
|
||||||
|
const line = (a: {x:number;y:number}, b:{x:number;y:number}) => `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
|
||||||
|
|
||||||
|
// "Gates" sit near the center to symbolize explicit permission before data leaves your pod
|
||||||
|
const gates = peers.map((p, i) => {
|
||||||
|
// point 30% of the way from center to peer
|
||||||
|
const gx = center.x + (p.x - center.x) * 0.28;
|
||||||
|
const gy = center.y + (p.y - center.y) * 0.28;
|
||||||
|
return { x: gx, y: gy };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paths for outbound shares (center -> gate -> peer)
|
||||||
|
const sharePaths = peers.map((p, i) =>
|
||||||
|
`M ${center.x} ${center.y} L ${gates[i].x} ${gates[i].y} L ${p.x} ${p.y}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Paths for local replication (center <-> owned pods)
|
||||||
|
const localPaths = owned.map((o) => line(center, o));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx("relative overflow-hidden", className)} aria-hidden="true" role="img" style={{ background: bg }}>
|
||||||
|
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
|
||||||
|
{/* GRID + GLOW */}
|
||||||
|
<defs>
|
||||||
|
<pattern id="dc-grid" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 28 0 L 0 0 0 28" fill="none" stroke={gridStroke} strokeWidth="1" opacity="0.35" />
|
||||||
|
</pattern>
|
||||||
|
<filter id="dc-glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="b"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<radialGradient id="pod-cyan" cx="50%" cy="50%" r="60%">
|
||||||
|
<stop offset="0%" stopColor={accent} stopOpacity="0.9"/>
|
||||||
|
<stop offset="100%" stopColor={accent} stopOpacity="0.06"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width={W} height={H} fill="url(#dc-grid)" />
|
||||||
|
|
||||||
|
{/* ====== BASELINES ====== */}
|
||||||
|
|
||||||
|
{/* Local control links (owned replicas) */}
|
||||||
|
{localPaths.map((d, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={`local-${i}`}
|
||||||
|
d={d}
|
||||||
|
stroke="#1e1e1e"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.3 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 0.6 }}
|
||||||
|
transition={{ duration: 0.7, delay: 0.05 * i, ease: [0.22,1,0.36,1] }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Outbound paths (to peers) */}
|
||||||
|
{sharePaths.map((d, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={`share-base-${i}`}
|
||||||
|
d={d}
|
||||||
|
stroke="#2a2a2a"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.25 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 0.5 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.1 * i, ease: [0.22,1,0.36,1] }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Cyan dash overlays to show active/allowed routes */}
|
||||||
|
{localPaths.map((d, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={`local-accent-${i}`}
|
||||||
|
d={d}
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="10 8"
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.9 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 0.9 }}
|
||||||
|
transition={{ duration: 0.7, delay: 0.15 * i }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{sharePaths.map((d, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={`share-accent-${i}`}
|
||||||
|
d={d}
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="10 8"
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.85 }}
|
||||||
|
animate={{ pathLength: 1, opacity: [0.85, 0.6, 0.85] }}
|
||||||
|
transition={{ duration: 0.9, delay: 0.2 * i, repeat: Infinity, repeatType: "reverse" }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ====== MOVING DATA (Packets) ====== */}
|
||||||
|
{!prefers && (
|
||||||
|
<>
|
||||||
|
{/* Local replication packets (bi-directional) */}
|
||||||
|
{localPaths.map((d, i) => (
|
||||||
|
<motion.circle
|
||||||
|
key={`localpkt-${i}`}
|
||||||
|
r={5}
|
||||||
|
fill={accent}
|
||||||
|
style={{ offsetPath: `path('${d}')` }}
|
||||||
|
initial={{ offsetDistance: "0%", opacity: 0.2 }}
|
||||||
|
animate={{ offsetDistance: ["0%", "100%"], opacity: [0.2, 1, 0.2] }}
|
||||||
|
transition={{ duration: 2.2, delay: i * 0.2, repeat: Infinity, ease: "linear" }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Outbound share packets: center -> gate -> peer */}
|
||||||
|
{sharePaths.map((d, i) => (
|
||||||
|
<motion.circle
|
||||||
|
key={`sharepkt-${i}`}
|
||||||
|
r={5}
|
||||||
|
fill={accent}
|
||||||
|
style={{ offsetPath: `path('${d}')` }}
|
||||||
|
initial={{ offsetDistance: "0%", opacity: 0 }}
|
||||||
|
animate={{ offsetDistance: ["0%", "100%"], opacity: [0, 1, 0] }}
|
||||||
|
transition={{ duration: 2.8, delay: 0.4 * i, repeat: Infinity, ease: "linear" }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ====== GATES (permission points) ====== */}
|
||||||
|
{gates.map((g, i) => (
|
||||||
|
<g key={`gate-${i}`}>
|
||||||
|
<circle cx={g.x} cy={g.y} r={12} fill="#0f0f0f" stroke="#232323" strokeWidth={2} />
|
||||||
|
{!prefers && (
|
||||||
|
<motion.circle
|
||||||
|
cx={g.x} cy={g.y} r={8} fill="none" stroke={accent} strokeWidth={2}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: [0, 1, 0] }}
|
||||||
|
transition={{ duration: 1.6, delay: 0.4 * i, repeat: Infinity, ease: [0.22,1,0.36,1] }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* tiny "lock" glyph */}
|
||||||
|
<path d={`M ${g.x-3} ${g.y+2} h 6 v 4 h -6 Z`} fill="#1d1d1d" stroke="#2b2b2b" strokeWidth={1}/>
|
||||||
|
<path d={`M ${g.x-2} ${g.y+2} v -3 a 3 3 0 0 1 6 0 v 3`} fill="none" stroke="#2b2b2b" strokeWidth={1}/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ====== PODS ====== */}
|
||||||
|
|
||||||
|
{/* Center (Your Pod) */}
|
||||||
|
<g>
|
||||||
|
<ellipse cx={center.x} cy={center.y} rx={52} ry={34} fill="url(#pod-cyan)" stroke="#1f1f1f" strokeWidth={2} filter="url(#dc-glow)" />
|
||||||
|
<ellipse cx={center.x} cy={center.y} rx={26} ry={18} fill="#0f0f0f" stroke="#232323" strokeWidth={2} />
|
||||||
|
{!prefers && (
|
||||||
|
<motion.ellipse
|
||||||
|
cx={center.x} cy={center.y} rx={38} ry={26}
|
||||||
|
fill="none" stroke={accent} strokeWidth={2} opacity={0.5}
|
||||||
|
animate={{ scale: [1, 1.06, 1], opacity: [0.35, 0.7, 0.35] }}
|
||||||
|
transition={{ duration: 2.2, repeat: Infinity, ease: [0.22,1,0.36,1] }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Owned pods (left-bottom cluster) */}
|
||||||
|
{owned.map((p, i) => (
|
||||||
|
<g key={`owned-${i}`}>
|
||||||
|
<ellipse cx={p.x} cy={p.y} rx={34} ry={22} fill="#0f0f0f" stroke="#232323" strokeWidth={2} />
|
||||||
|
<ellipse cx={p.x} cy={p.y} rx={16} ry={11} fill="#121212" stroke="#272727" strokeWidth={1.5} />
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* External peers (top-right cluster) */}
|
||||||
|
{peers.map((p, i) => (
|
||||||
|
<g key={`peer-${i}`}>
|
||||||
|
<ellipse cx={p.x} cy={p.y} rx={32} ry={21} fill="#0f0f0f" stroke="#232323" strokeWidth={2} />
|
||||||
|
{/* subtle cyan rim = marked as “shared” endpoints */}
|
||||||
|
<motion.ellipse
|
||||||
|
cx={p.x} cy={p.y} rx={27} ry={18}
|
||||||
|
fill="none" stroke={accent} strokeWidth={1.8} opacity={0.35}
|
||||||
|
animate={!prefers ? { opacity: [0.2, 0.5, 0.2] } : { opacity: 0.35 }}
|
||||||
|
transition={{ duration: 2.4, delay: i * 0.2, repeat: prefers ? 0 : Infinity }}
|
||||||
|
filter="url(#dc-glow)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
"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;
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<motion.circle
|
|
||||||
cx={x}
|
|
||||||
cy={y}
|
|
||||||
r={r + 8}
|
|
||||||
stroke="#1F2937"
|
|
||||||
strokeWidth={2}
|
|
||||||
fill="none"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 0.8 }}
|
|
||||||
transition={{ delay, duration: 0.6 }}
|
|
||||||
/>
|
|
||||||
<motion.circle
|
|
||||||
cx={x}
|
|
||||||
cy={y}
|
|
||||||
r={r}
|
|
||||||
fill={accent}
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
scale: pulse && !prefers ? [1, 1.1, 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],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Packet = ({
|
|
||||||
path,
|
|
||||||
delay = 0,
|
|
||||||
accent = "#00b8db",
|
|
||||||
duration = 2.4,
|
|
||||||
}: {
|
|
||||||
path: string;
|
|
||||||
delay?: number;
|
|
||||||
accent?: string;
|
|
||||||
duration?: number;
|
|
||||||
}) => {
|
|
||||||
const prefers = useReducedMotion();
|
|
||||||
return (
|
|
||||||
<motion.circle
|
|
||||||
r={4}
|
|
||||||
fill={accent}
|
|
||||||
initial={{ offsetDistance: "0%", opacity: 0 }}
|
|
||||||
animate={{
|
|
||||||
offsetDistance: ["0%", "100%"],
|
|
||||||
opacity: [0, 1, 0],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
delay,
|
|
||||||
duration,
|
|
||||||
repeat: !prefers ? Infinity : 0,
|
|
||||||
repeatType: "loop",
|
|
||||||
ease: "linear",
|
|
||||||
}}
|
|
||||||
style={{ offsetPath: `path('${path}')` }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function NoCentral({
|
|
||||||
className,
|
|
||||||
accent = "#00b8db",
|
|
||||||
bg = "#0a0a0a",
|
|
||||||
}: Props) {
|
|
||||||
const center = { x: 380, y: 210 };
|
|
||||||
const nodes = [
|
|
||||||
{ x: 160, y: 100 },
|
|
||||||
{ x: 270, y: 70 },
|
|
||||||
{ x: 500, y: 90 },
|
|
||||||
{ x: 620, y: 150 },
|
|
||||||
{ x: 220, y: 300 },
|
|
||||||
{ x: 360, y: 340 },
|
|
||||||
{ x: 530, y: 290 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
[nodes[0], nodes[1]],
|
|
||||||
[nodes[1], nodes[2]],
|
|
||||||
[nodes[2], nodes[3]],
|
|
||||||
[nodes[0], nodes[4]],
|
|
||||||
[nodes[4], nodes[5]],
|
|
||||||
[nodes[5], nodes[6]],
|
|
||||||
[nodes[1], nodes[5]],
|
|
||||||
[nodes[3], nodes[6]],
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx("relative overflow-hidden", className)}
|
|
||||||
aria-hidden="true"
|
|
||||||
role="img"
|
|
||||||
aria-label="Distributed network 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>
|
|
||||||
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
|
|
||||||
<stop offset="0%" stopColor={accent} stopOpacity="0.12" />
|
|
||||||
<stop offset="100%" stopColor={accent} stopOpacity="0" />
|
|
||||||
</radialGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width={W} height={H} fill="url(#grid-dark)" />
|
|
||||||
|
|
||||||
{/* Cyan glow field */}
|
|
||||||
<circle cx={center.x} cy={center.y} r={200} fill="url(#glow)" opacity={0.4} />
|
|
||||||
|
|
||||||
{/* Static connection lines */}
|
|
||||||
{links.map(([a, b], i) => (
|
|
||||||
<motion.path
|
|
||||||
key={`line-${i}`}
|
|
||||||
d={`M ${a.x} ${a.y} L ${b.x} ${b.y}`}
|
|
||||||
stroke="#1F2937"
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeLinecap="round"
|
|
||||||
fill="none"
|
|
||||||
initial={{ pathLength: 0, opacity: 0 }}
|
|
||||||
animate={{ pathLength: 1, opacity: 0.4 }}
|
|
||||||
transition={{
|
|
||||||
delay: 0.05 * i,
|
|
||||||
duration: 0.8,
|
|
||||||
ease: [0.22, 1, 0.36, 1],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Cyan animated signal lines */}
|
|
||||||
{links.slice(0, 5).map(([a, b], i) => (
|
|
||||||
<motion.path
|
|
||||||
key={`signal-${i}`}
|
|
||||||
d={`M ${a.x} ${a.y} L ${b.x} ${b.y}`}
|
|
||||||
stroke={accent}
|
|
||||||
strokeWidth={2.5}
|
|
||||||
strokeDasharray="10 10"
|
|
||||||
strokeLinecap="round"
|
|
||||||
fill="none"
|
|
||||||
animate={{ opacity: [0.3, 0.8, 0.3] }}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: i * 0.2,
|
|
||||||
ease: "easeInOut",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Circulating packets on random links */}
|
|
||||||
{links.map(([a, b], i) => (
|
|
||||||
<Packet
|
|
||||||
key={`pkt-${i}`}
|
|
||||||
path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`}
|
|
||||||
delay={i * 0.4}
|
|
||||||
accent={accent}
|
|
||||||
duration={3}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Pulsing nodes */}
|
|
||||||
{nodes.map((n, i) => (
|
|
||||||
<Node
|
|
||||||
key={`node-${i}`}
|
|
||||||
x={n.x}
|
|
||||||
y={n.y}
|
|
||||||
r={10}
|
|
||||||
accent={accent}
|
|
||||||
pulse={i % 2 === 0}
|
|
||||||
delay={i * 0.1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Faded red “no central” mark at middle */}
|
|
||||||
<motion.circle
|
|
||||||
cx={center.x}
|
|
||||||
cy={center.y}
|
|
||||||
r={18}
|
|
||||||
fill="none"
|
|
||||||
stroke="#FF4D4D"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeDasharray="6 4"
|
|
||||||
initial={{ scale: 0, opacity: 0 }}
|
|
||||||
animate={{ scale: 1, opacity: 0.7 }}
|
|
||||||
transition={{ delay: 1, duration: 0.8 }}
|
|
||||||
/>
|
|
||||||
<motion.path
|
|
||||||
d={`M ${center.x - 10} ${center.y - 10} L ${center.x + 10} ${center.y + 10} M ${center.x - 10} ${center.y + 10} L ${center.x + 10} ${center.y - 10}`}
|
|
||||||
stroke="#FF4D4D"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeLinecap="round"
|
|
||||||
initial={{ opacity: 0, scale: 0 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ delay: 1.2, duration: 0.6 }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
"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 component */
|
|
||||||
const Node = ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
r = 14,
|
|
||||||
accent = "#00b8db",
|
|
||||||
pulse = false,
|
|
||||||
faded = false,
|
|
||||||
}: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
r?: number;
|
|
||||||
accent?: string;
|
|
||||||
pulse?: boolean;
|
|
||||||
faded?: boolean;
|
|
||||||
}) => {
|
|
||||||
const prefers = useReducedMotion();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<motion.circle
|
|
||||||
cx={x}
|
|
||||||
cy={y}
|
|
||||||
r={r + 10}
|
|
||||||
stroke="#1F2937"
|
|
||||||
strokeWidth={2}
|
|
||||||
fill="none"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: faded ? 0.3 : 0.9 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
/>
|
|
||||||
<motion.circle
|
|
||||||
cx={x}
|
|
||||||
cy={y}
|
|
||||||
r={r}
|
|
||||||
fill={accent}
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{
|
|
||||||
opacity: faded ? 0.3 : 1,
|
|
||||||
scale: pulse && !prefers ? [1, 1.12, 1] : 1,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: pulse && !prefers ? 1.8 : 0.6,
|
|
||||||
repeat: pulse && !prefers ? Infinity : 0,
|
|
||||||
repeatType: "mirror",
|
|
||||||
ease: [0.22, 1, 0.36, 1],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Moving packet along a path */
|
|
||||||
const Packet = ({
|
|
||||||
path,
|
|
||||||
delay = 0,
|
|
||||||
accent = "#00b8db",
|
|
||||||
}: {
|
|
||||||
path: string;
|
|
||||||
delay?: number;
|
|
||||||
accent?: string;
|
|
||||||
}) => {
|
|
||||||
const prefers = useReducedMotion();
|
|
||||||
return (
|
|
||||||
<motion.circle
|
|
||||||
r={5}
|
|
||||||
fill={accent}
|
|
||||||
initial={{ offsetDistance: "0%", opacity: 0 }}
|
|
||||||
animate={{
|
|
||||||
offsetDistance: ["0%", "100%"],
|
|
||||||
opacity: [0, 1, 0],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
delay,
|
|
||||||
duration: 1.6,
|
|
||||||
repeat: !prefers ? Infinity : 0,
|
|
||||||
repeatType: "loop",
|
|
||||||
ease: "linear",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
offsetPath: `path('${path}')`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function NoControl({
|
|
||||||
className,
|
|
||||||
accent = "#00b8db",
|
|
||||||
bg = "#0a0a0a",
|
|
||||||
}: Props) {
|
|
||||||
const center = { x: 380, y: 210 };
|
|
||||||
const outer = [
|
|
||||||
{ x: 160, y: 120 },
|
|
||||||
{ x: 600, y: 120 },
|
|
||||||
{ x: 160, y: 300 },
|
|
||||||
{ x: 600, y: 300 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const link = (a: any, b: any) => `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx("relative overflow-hidden", className)}
|
|
||||||
aria-hidden="true"
|
|
||||||
role="img"
|
|
||||||
aria-label="No single point of control 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>
|
|
||||||
</defs>
|
|
||||||
<rect width={W} height={H} fill="url(#grid-dark)" />
|
|
||||||
|
|
||||||
{/* Connections between outer nodes (decentralized ring) */}
|
|
||||||
<motion.path
|
|
||||||
d={`M ${outer[0].x} ${outer[0].y}
|
|
||||||
L ${outer[1].x} ${outer[1].y}
|
|
||||||
L ${outer[3].x} ${outer[3].y}
|
|
||||||
L ${outer[2].x} ${outer[2].y} Z`}
|
|
||||||
stroke="#1F2937"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeLinecap="round"
|
|
||||||
fill="none"
|
|
||||||
initial={{ pathLength: 0, opacity: 0.3 }}
|
|
||||||
animate={{ pathLength: 1, opacity: 0.6 }}
|
|
||||||
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Cyan pulsing signals around the ring */}
|
|
||||||
{outer.map((o, i) => {
|
|
||||||
const next = outer[(i + 1) % outer.length];
|
|
||||||
const p = link(o, next);
|
|
||||||
return <Packet key={`p-${i}`} path={p} delay={i * 0.4} accent={accent} />;
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Faded center node (represents the “former” single point) */}
|
|
||||||
<Node x={center.x} y={center.y} r={18} accent={accent} faded pulse={false} />
|
|
||||||
|
|
||||||
{/* Outer sovereign nodes */}
|
|
||||||
{outer.map((n, i) => (
|
|
||||||
<Node key={i} x={n.x} y={n.y} r={14} accent={accent} pulse />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Cross mark over center node (control denied) */}
|
|
||||||
<motion.path
|
|
||||||
d={`M ${center.x - 12} ${center.y - 12} L ${center.x + 12} ${center.y + 12} M ${center.x - 12} ${center.y + 12} L ${center.x + 12} ${center.y - 12}`}
|
|
||||||
stroke="#FF4D4D"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeLinecap="round"
|
|
||||||
initial={{ opacity: 0, scale: 0 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ delay: 0.8, duration: 0.6 }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { motion, useReducedMotion } from "framer-motion";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
className?: string;
|
|
||||||
accent?: string; // cyan
|
|
||||||
bg?: string; // solid dark background
|
|
||||||
gridStroke?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const W = 720; // 4:3
|
|
||||||
const H = 540; // 4:3
|
|
||||||
|
|
||||||
export default function NoSinglePoint({
|
|
||||||
className,
|
|
||||||
accent = "#00b8db",
|
|
||||||
bg = "#0b0b0b",
|
|
||||||
gridStroke = "#2b2a2a",
|
|
||||||
}: Props) {
|
|
||||||
const prefers = useReducedMotion();
|
|
||||||
|
|
||||||
// Nodes (left source, right dest, top hub, bottom hub, plus two relays)
|
|
||||||
const nodes = {
|
|
||||||
left: { x: 120, y: H / 2 },
|
|
||||||
right: { x: W - 120, y: H / 2 },
|
|
||||||
top: { x: W / 2, y: 160 },
|
|
||||||
bot: { x: W / 2, y: H - 160 },
|
|
||||||
tl: { x: 240, y: 200 },
|
|
||||||
br: { x: W - 240, y: H - 200 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Redundant paths from left → right
|
|
||||||
const upperPath = `M ${nodes.left.x} ${nodes.left.y}
|
|
||||||
L ${nodes.tl.x} ${nodes.tl.y}
|
|
||||||
L ${nodes.top.x} ${nodes.top.y}
|
|
||||||
L ${nodes.right.x} ${nodes.right.y}`;
|
|
||||||
|
|
||||||
const lowerPath = `M ${nodes.left.x} ${nodes.left.y}
|
|
||||||
L ${nodes.bot.x} ${nodes.bot.y}
|
|
||||||
L ${nodes.br.x} ${nodes.br.y}
|
|
||||||
L ${nodes.right.x} ${nodes.right.y}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx("relative overflow-hidden", className)}
|
|
||||||
aria-hidden="true"
|
|
||||||
role="img"
|
|
||||||
style={{ background: bg }}
|
|
||||||
>
|
|
||||||
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
|
|
||||||
{/* Subtle dark grid */}
|
|
||||||
<defs>
|
|
||||||
<pattern id="grid-dark-4x3" width="28" height="28" patternUnits="userSpaceOnUse">
|
|
||||||
<path d="M 28 0 L 0 0 0 28" fill="none" stroke={gridStroke} strokeWidth="1" opacity="0.35" />
|
|
||||||
</pattern>
|
|
||||||
<filter id="glow">
|
|
||||||
<feGaussianBlur stdDeviation="3" result="b" />
|
|
||||||
<feMerge>
|
|
||||||
<feMergeNode in="b" />
|
|
||||||
<feMergeNode in="SourceGraphic" />
|
|
||||||
</feMerge>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<rect width={W} height={H} fill="url(#grid-dark-4x3)" />
|
|
||||||
|
|
||||||
{/* Base links (all potential connectivity) */}
|
|
||||||
{[
|
|
||||||
`M ${nodes.left.x} ${nodes.left.y} L ${nodes.tl.x} ${nodes.tl.y}`,
|
|
||||||
`M ${nodes.tl.x} ${nodes.tl.y} L ${nodes.top.x} ${nodes.top.y}`,
|
|
||||||
`M ${nodes.top.x} ${nodes.top.y} L ${nodes.right.x} ${nodes.right.y}`,
|
|
||||||
`M ${nodes.left.x} ${nodes.left.y} L ${nodes.bot.x} ${nodes.bot.y}`,
|
|
||||||
`M ${nodes.bot.x} ${nodes.bot.y} L ${nodes.br.x} ${nodes.br.y}`,
|
|
||||||
`M ${nodes.br.x} ${nodes.br.y} L ${nodes.right.x} ${nodes.right.y}`,
|
|
||||||
// cross edges (mesh redundancy)
|
|
||||||
`M ${nodes.tl.x} ${nodes.tl.y} L ${nodes.bot.x} ${nodes.bot.y}`,
|
|
||||||
`M ${nodes.top.x} ${nodes.top.y} L ${nodes.br.x} ${nodes.br.y}`,
|
|
||||||
].map((d, i) => (
|
|
||||||
<motion.path
|
|
||||||
key={`base-${i}`}
|
|
||||||
d={d}
|
|
||||||
stroke="#1e1e1e"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeLinecap="round"
|
|
||||||
fill="none"
|
|
||||||
initial={{ pathLength: 0, opacity: 0.2 }}
|
|
||||||
animate={{ pathLength: 1, opacity: 0.5 }}
|
|
||||||
transition={{ duration: 0.8, delay: i * 0.05, ease: [0.22, 1, 0.36, 1] }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Highlight the two redundant main routes */}
|
|
||||||
<motion.path
|
|
||||||
d={upperPath}
|
|
||||||
fill="none"
|
|
||||||
stroke="#3a3a3a"
|
|
||||||
strokeWidth={4}
|
|
||||||
strokeLinecap="round"
|
|
||||||
initial={{ pathLength: 0, opacity: 0.6 }}
|
|
||||||
animate={{ pathLength: 1, opacity: 0.6 }}
|
|
||||||
transition={{ duration: 0.9 }}
|
|
||||||
/>
|
|
||||||
<motion.path
|
|
||||||
d={lowerPath}
|
|
||||||
fill="none"
|
|
||||||
stroke="#3a3a3a"
|
|
||||||
strokeWidth={4}
|
|
||||||
strokeLinecap="round"
|
|
||||||
initial={{ pathLength: 0, opacity: 0.6 }}
|
|
||||||
animate={{ pathLength: 1, opacity: 0.6 }}
|
|
||||||
transition={{ duration: 0.9, delay: 0.1 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Cyan accent dash showing “preferred/active” path(s) */}
|
|
||||||
<motion.path
|
|
||||||
d={upperPath}
|
|
||||||
fill="none"
|
|
||||||
stroke={accent}
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeDasharray="10 8"
|
|
||||||
initial={{ pathLength: 0, opacity: 0.8 }}
|
|
||||||
animate={{ pathLength: 1, opacity: [0.8, 0.2, 0.8] }} // will fade as "blocked"
|
|
||||||
transition={{ duration: 1.1, repeat: Infinity, repeatType: "reverse" }}
|
|
||||||
filter="url(#glow)"
|
|
||||||
/>
|
|
||||||
<motion.path
|
|
||||||
d={lowerPath}
|
|
||||||
fill="none"
|
|
||||||
stroke={accent}
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeDasharray="10 8"
|
|
||||||
initial={{ pathLength: 0, opacity: 1 }}
|
|
||||||
animate={{ pathLength: 1, opacity: 1 }}
|
|
||||||
transition={{ duration: 1 }}
|
|
||||||
filter="url(#glow)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Moving packets: one tries upper (gets dimmed at top hub), one uses lower and continues */}
|
|
||||||
{!prefers && (
|
|
||||||
<>
|
|
||||||
{/* Upper path packet (dims near top hub to suggest block/censor but NOT stopping overall flow) */}
|
|
||||||
<motion.circle
|
|
||||||
r={5}
|
|
||||||
fill={accent}
|
|
||||||
style={{ offsetPath: `path('${upperPath}')` }}
|
|
||||||
initial={{ offsetDistance: "0%", opacity: 0.9 }}
|
|
||||||
animate={{
|
|
||||||
offsetDistance: ["0%", "100%"],
|
|
||||||
opacity: [0.9, 0.4, 0.9], // subtle dimming cycle
|
|
||||||
}}
|
|
||||||
transition={{ duration: 3.0, repeat: Infinity, ease: "linear" }}
|
|
||||||
filter="url(#glow)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Lower path packet (stable flow) */}
|
|
||||||
<motion.circle
|
|
||||||
r={6}
|
|
||||||
fill={accent}
|
|
||||||
style={{ offsetPath: `path('${lowerPath}')` }}
|
|
||||||
initial={{ offsetDistance: "0%", opacity: 1 }}
|
|
||||||
animate={{ offsetDistance: ["0%", "100%"] }}
|
|
||||||
transition={{ duration: 2.4, repeat: Infinity, ease: "linear" }}
|
|
||||||
filter="url(#glow)"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Nodes */}
|
|
||||||
{Object.values(nodes).map((n, i) => (
|
|
||||||
<g key={`node-${i}`}>
|
|
||||||
<circle cx={n.x} cy={n.y} r={20} fill="#0f0f0f" stroke="#1f1f1f" strokeWidth={2} />
|
|
||||||
<motion.circle
|
|
||||||
cx={n.x}
|
|
||||||
cy={n.y}
|
|
||||||
r={8}
|
|
||||||
fill={i === 2 ? "#0f0f0f" : accent} // top hub inner is dark (to hint “blocked” moment)
|
|
||||||
stroke="#222"
|
|
||||||
strokeWidth={2}
|
|
||||||
animate={
|
|
||||||
!prefers
|
|
||||||
? i === 2 // top node (potential choke/attack point) pulses differently
|
|
||||||
? { opacity: [0.5, 0.25, 0.5], scale: [1, 0.95, 1] }
|
|
||||||
: { opacity: [0.9, 1, 0.9], scale: [1, 1.06, 1] }
|
|
||||||
: { opacity: 1 }
|
|
||||||
}
|
|
||||||
transition={{
|
|
||||||
duration: 2.2,
|
|
||||||
repeat: prefers ? 0 : Infinity,
|
|
||||||
ease: [0.22, 1, 0.36, 1],
|
|
||||||
delay: i * 0.05,
|
|
||||||
}}
|
|
||||||
filter="url(#glow)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* A subtle “block” overlay on the top hub (appears/disappears) */}
|
|
||||||
{!prefers && (
|
|
||||||
<motion.g
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: [0, 0.7, 0] }}
|
|
||||||
transition={{ duration: 3.2, repeat: Infinity, ease: "easeInOut", delay: 0.8 }}
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
cx={nodes.top.x}
|
|
||||||
cy={nodes.top.y}
|
|
||||||
r={18}
|
|
||||||
fill="none"
|
|
||||||
stroke="#8b8b8b"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d={`M ${nodes.top.x - 10} ${nodes.top.y - 10} L ${nodes.top.x + 10} ${nodes.top.y + 10}
|
|
||||||
M ${nodes.top.x + 10} ${nodes.top.y - 10} L ${nodes.top.x - 10} ${nodes.top.y + 10}`}
|
|
||||||
stroke="#8b8b8b"
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</motion.g>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
255
src/pages/pods/animations/Resilience.tsx
Normal file
255
src/pages/pods/animations/Resilience.tsx
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
accent?: string; // cyan
|
||||||
|
danger?: string; // failing node color
|
||||||
|
bg?: string; // dark background
|
||||||
|
gridStroke?: string; // grid
|
||||||
|
};
|
||||||
|
|
||||||
|
const W = 720;
|
||||||
|
const H = 540;
|
||||||
|
|
||||||
|
export default function Resilience({
|
||||||
|
className,
|
||||||
|
accent = "#00b8db",
|
||||||
|
danger = "#ff4d4d",
|
||||||
|
bg = "#0b0b0b",
|
||||||
|
gridStroke = "#2b2a2a",
|
||||||
|
}: Props) {
|
||||||
|
const prefers = useReducedMotion();
|
||||||
|
|
||||||
|
// Primary healthy nodes
|
||||||
|
const nodes = [
|
||||||
|
{ x: 220, y: 180 },
|
||||||
|
{ x: 500, y: 180 },
|
||||||
|
{ x: 580, y: 330 },
|
||||||
|
{ x: 360, y: 420 },
|
||||||
|
{ x: 160, y: 330 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// One failing node
|
||||||
|
const failing = { x: 360, y: 100 };
|
||||||
|
|
||||||
|
// Full mesh except failing node
|
||||||
|
const links: [number, number][] = [];
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
for (let j = i + 1; j < nodes.length; j++) {
|
||||||
|
links.push([i, j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths for fallback reroute (detour link when failing node disappears)
|
||||||
|
const reroute = `M ${nodes[0].x} ${nodes[0].y}
|
||||||
|
L ${nodes[3].x} ${nodes[3].y}
|
||||||
|
L ${nodes[2].x} ${nodes[2].y}`;
|
||||||
|
|
||||||
|
const line = (i: number, j: number) =>
|
||||||
|
`M ${nodes[i].x} ${nodes[i].y} L ${nodes[j].x} ${nodes[j].y}`;
|
||||||
|
|
||||||
|
const failRay = `M ${failing.x} ${failing.y} L ${nodes[1].x} ${nodes[1].y}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx("relative overflow-hidden", className)}>
|
||||||
|
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full" style={{ background: bg }}>
|
||||||
|
{/* ===== GRID + FILTERS ===== */}
|
||||||
|
<defs>
|
||||||
|
<pattern id="res-grid" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||||
|
<path
|
||||||
|
d="M 28 0 L 0 0 0 28"
|
||||||
|
stroke={gridStroke}
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.35"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<filter id="res-glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="b" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<radialGradient id="res-pod" cx="50%" cy="50%" r="60%">
|
||||||
|
<stop offset="0%" stopColor={accent} stopOpacity="0.9" />
|
||||||
|
<stop offset="100%" stopColor={accent} stopOpacity="0.06" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width={W} height={H} fill="url(#res-grid)" />
|
||||||
|
|
||||||
|
{/* ===== BASE LINKS (Healthy Mesh) ===== */}
|
||||||
|
{links.map(([i, j], idx) => (
|
||||||
|
<motion.path
|
||||||
|
key={`mesh-${idx}`}
|
||||||
|
d={line(i, j)}
|
||||||
|
fill="none"
|
||||||
|
stroke="#1f1f1f"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="round"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.25 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 0.55 }}
|
||||||
|
transition={{ duration: 0.6, delay: idx * 0.04 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ===== FAILING NODE LINK (Fading Out) ===== */}
|
||||||
|
<motion.path
|
||||||
|
d={failRay}
|
||||||
|
fill="none"
|
||||||
|
stroke={danger}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="10 8"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.8 }}
|
||||||
|
animate={{ pathLength: 1, opacity: [0.8, 0.15, 0.8] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, repeatType: "reverse" }}
|
||||||
|
filter="url(#res-glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ===== ACTIVE REROUTE (Compensating Path) ===== */}
|
||||||
|
<motion.path
|
||||||
|
d={reroute}
|
||||||
|
fill="none"
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="12 10"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
opacity: [0, 1, 1, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 3,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "loop",
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
filter="url(#res-glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ===== MOVING PACKETS ON HEALTHY LINKS ===== */}
|
||||||
|
{!prefers &&
|
||||||
|
links.map(([i, j], idx) => (
|
||||||
|
<motion.circle
|
||||||
|
key={`pkt-${idx}`}
|
||||||
|
r={5}
|
||||||
|
fill={accent}
|
||||||
|
style={{ offsetPath: `path('${line(i, j)}')` }}
|
||||||
|
initial={{ offsetDistance: "0%", opacity: 0 }}
|
||||||
|
animate={{ offsetDistance: ["0%", "100%"], opacity: [0.15, 1, 0.15] }}
|
||||||
|
transition={{
|
||||||
|
duration: 2.4,
|
||||||
|
delay: idx * 0.15,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
filter="url(#res-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ===== PACKETS ON REROUTE WHEN FAILURE OCCURS ===== */}
|
||||||
|
{!prefers && (
|
||||||
|
<motion.circle
|
||||||
|
r={6}
|
||||||
|
fill={accent}
|
||||||
|
style={{ offsetPath: `path('${reroute}')` }}
|
||||||
|
initial={{ offsetDistance: "0%", opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
offsetDistance: ["0%", "100%"],
|
||||||
|
opacity: [0.2, 1, 0.2],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2.8,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
filter="url(#res-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== HEALTHY NODES ===== */}
|
||||||
|
{nodes.map((n, i) => (
|
||||||
|
<g key={`node-${i}`}>
|
||||||
|
{/* Aura */}
|
||||||
|
{!prefers && (
|
||||||
|
<motion.ellipse
|
||||||
|
cx={n.x}
|
||||||
|
cy={n.y}
|
||||||
|
rx={40}
|
||||||
|
ry={26}
|
||||||
|
fill="none"
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
opacity={0.18}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.07, 1],
|
||||||
|
opacity: [0.1, 0.25, 0.1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2.2,
|
||||||
|
delay: i * 0.1,
|
||||||
|
repeat: Infinity,
|
||||||
|
}}
|
||||||
|
filter="url(#res-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pod body */}
|
||||||
|
<ellipse
|
||||||
|
cx={n.x}
|
||||||
|
cy={n.y}
|
||||||
|
rx={34}
|
||||||
|
ry={22}
|
||||||
|
fill="url(#res-pod)"
|
||||||
|
stroke="#1f1f1f"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Core */}
|
||||||
|
<ellipse
|
||||||
|
cx={n.x}
|
||||||
|
cy={n.y}
|
||||||
|
rx={16}
|
||||||
|
ry={11}
|
||||||
|
fill="#0f0f0f"
|
||||||
|
stroke="#272727"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ===== FAILING NODE (Flickering / Disconnecting) ===== */}
|
||||||
|
<g>
|
||||||
|
<ellipse
|
||||||
|
cx={failing.x}
|
||||||
|
cy={failing.y}
|
||||||
|
rx={30}
|
||||||
|
ry={20}
|
||||||
|
fill="#1a1a1a"
|
||||||
|
stroke="#3a3a3a"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!prefers && (
|
||||||
|
<motion.ellipse
|
||||||
|
cx={failing.x}
|
||||||
|
cy={failing.y}
|
||||||
|
rx={18}
|
||||||
|
ry={12}
|
||||||
|
fill={danger}
|
||||||
|
opacity={0.5}
|
||||||
|
animate={{ opacity: [0.5, 0.1, 0.5] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
231
src/pages/pods/animations/Security.tsx
Normal file
231
src/pages/pods/animations/Security.tsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
accent?: string; // cyan
|
||||||
|
danger?: string; // hostile color
|
||||||
|
bg?: string; // dark background
|
||||||
|
gridStroke?: string; // subtle grid
|
||||||
|
};
|
||||||
|
|
||||||
|
const W = 720;
|
||||||
|
const H = 540;
|
||||||
|
|
||||||
|
export default function Security({
|
||||||
|
className,
|
||||||
|
accent = "#00b8db",
|
||||||
|
danger = "#ff4d4d",
|
||||||
|
bg = "#0b0b0b",
|
||||||
|
gridStroke = "#2b2a2a",
|
||||||
|
}: Props) {
|
||||||
|
const prefers = useReducedMotion();
|
||||||
|
|
||||||
|
// Central protected pod
|
||||||
|
const center = { x: W / 2, y: H / 2 };
|
||||||
|
|
||||||
|
// External hostile/corporate nodes
|
||||||
|
const hostile = [
|
||||||
|
{ x: 160, y: 120 },
|
||||||
|
{ x: 560, y: 120 },
|
||||||
|
{ x: 120, y: 300 },
|
||||||
|
{ x: 600, y: 380 },
|
||||||
|
{ x: 350, y: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper function for attack rays
|
||||||
|
const attackRay = (h: { x: number; y: number }) =>
|
||||||
|
`M ${h.x} ${h.y} L ${center.x} ${center.y}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("relative overflow-hidden", className)}
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
style={{ background: bg }}
|
||||||
|
>
|
||||||
|
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
|
||||||
|
{/* ====== GRID + FILTERS ====== */}
|
||||||
|
<defs>
|
||||||
|
<pattern id="sec-grid" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||||
|
<path
|
||||||
|
d="M 28 0 L 0 0 0 28"
|
||||||
|
stroke={gridStroke}
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.35"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<filter id="sec-glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="b" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<radialGradient id="sec-core" cx="50%" cy="50%" r="60%">
|
||||||
|
<stop offset="0%" stopColor={accent} stopOpacity="0.9" />
|
||||||
|
<stop offset="100%" stopColor={accent} stopOpacity="0.06" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width={W} height={H} fill="url(#sec-grid)" />
|
||||||
|
|
||||||
|
{/* ====== ATTACK LINES (blocked) ====== */}
|
||||||
|
{hostile.map((h, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={`atk-${i}`}
|
||||||
|
d={attackRay(h)}
|
||||||
|
stroke={danger}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="10 8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0.7 }}
|
||||||
|
animate={{
|
||||||
|
pathLength: 1,
|
||||||
|
opacity: [0.7, 0.2, 0.7], // attempt but weakened
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 1.5,
|
||||||
|
delay: i * 0.25,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "reverse",
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
filter="url(#sec-glow)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ====== BLOCKING SHIELD ====== */}
|
||||||
|
{/* Outer shield */}
|
||||||
|
<motion.circle
|
||||||
|
cx={center.x}
|
||||||
|
cy={center.y}
|
||||||
|
r={90}
|
||||||
|
fill="none"
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
opacity={0.25}
|
||||||
|
animate={!prefers ? { scale: [1, 1.05, 1], opacity: [0.2, 0.4, 0.2] } : {}}
|
||||||
|
transition={{ duration: 2.6, repeat: Infinity, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
filter="url(#sec-glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Impact pulses when attacks hit shield */}
|
||||||
|
{!prefers &&
|
||||||
|
hostile.map((h, i) => {
|
||||||
|
const midX = (h.x + center.x) / 2;
|
||||||
|
const midY = (h.y + center.y) / 2;
|
||||||
|
return (
|
||||||
|
<motion.circle
|
||||||
|
key={`impact-${i}`}
|
||||||
|
cx={midX}
|
||||||
|
cy={midY}
|
||||||
|
r={10}
|
||||||
|
fill="none"
|
||||||
|
stroke={danger}
|
||||||
|
strokeWidth={2}
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
scale: [0.8, 1.4, 1.8],
|
||||||
|
opacity: [0, 0.4, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
delay: i * 0.25 + 0.6,
|
||||||
|
duration: 1.2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
filter="url(#sec-glow)"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* ====== EXTERNAL HOSTILE NODES (flickering / unstable) ====== */}
|
||||||
|
{hostile.map((h, i) => (
|
||||||
|
<g key={`hnode-${i}`}>
|
||||||
|
<circle
|
||||||
|
cx={h.x}
|
||||||
|
cy={h.y}
|
||||||
|
r={18}
|
||||||
|
fill="#1a1a1a"
|
||||||
|
stroke="#3a3a3a"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!prefers && (
|
||||||
|
<motion.circle
|
||||||
|
cx={h.x}
|
||||||
|
cy={h.y}
|
||||||
|
r={10}
|
||||||
|
fill={danger}
|
||||||
|
opacity={0.3}
|
||||||
|
animate={{ opacity: [0.3, 0.1, 0.3] }}
|
||||||
|
transition={{
|
||||||
|
duration: 1.8,
|
||||||
|
delay: i * 0.3,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ====== CENTRAL SECURE POD (always stable) ====== */}
|
||||||
|
<g>
|
||||||
|
{/* Glowing pod aura */}
|
||||||
|
{!prefers && (
|
||||||
|
<motion.circle
|
||||||
|
cx={center.x}
|
||||||
|
cy={center.y}
|
||||||
|
r={60}
|
||||||
|
fill="none"
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
opacity={0.2}
|
||||||
|
animate={{ scale: [1, 1.08, 1], opacity: [0.15, 0.35, 0.15] }}
|
||||||
|
transition={{ duration: 2.4, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
filter="url(#sec-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pod body */}
|
||||||
|
<ellipse
|
||||||
|
cx={center.x}
|
||||||
|
cy={center.y}
|
||||||
|
rx={48}
|
||||||
|
ry={30}
|
||||||
|
fill="url(#sec-core)"
|
||||||
|
stroke="#1f1f1f"
|
||||||
|
strokeWidth={2}
|
||||||
|
filter="url(#sec-glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Inner dark core */}
|
||||||
|
<ellipse cx={center.x} cy={center.y} rx={22} ry={14} fill="#0f0f0f" stroke="#292929" strokeWidth={2} />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Inner stable heartbeat */}
|
||||||
|
{!prefers && (
|
||||||
|
<motion.circle
|
||||||
|
cx={center.x}
|
||||||
|
cy={center.y}
|
||||||
|
r={20}
|
||||||
|
fill="none"
|
||||||
|
stroke={accent}
|
||||||
|
strokeWidth={2}
|
||||||
|
animate={{ scale: [1, 1.15, 1], opacity: [0.35, 1, 0.35] }}
|
||||||
|
transition={{ duration: 2.2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
filter="url(#sec-glow)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user