forked from emre/www_projectmycelium_com
- Added cyan radial glow SVG to CallToAction components across all product pages for visual consistency - Created NoSinglePoint animation demonstrating redundant network paths and resilience against single point failures - Updated HomeArchitecture layout to better center and display animations with improved flex positioning
226 lines
7.7 KiB
TypeScript
226 lines
7.7 KiB
TypeScript
"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>
|
|
);
|
|
}
|