forked from sashaastiadi/www_mycelium_net
176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
|
|
type Props = {
|
|
className?: string; // e.g. "w-full h-72"
|
|
bg?: string; // default white
|
|
};
|
|
|
|
/** Palette */
|
|
const ACCENT = '#00b8db';
|
|
const STROKE = '#111827'; // black-ish
|
|
const GRAY = '#9CA3AF';
|
|
const GRAY_LT = '#E5E7EB';
|
|
|
|
function Laptop({ x, y }: { x: number; y: number }) {
|
|
return (
|
|
<g transform={`translate(${x}, ${y})`}>
|
|
{/* screen */}
|
|
<rect x={-48} y={-32} width={96} height={64} rx={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
|
|
<rect x={-44} y={-28} width={88} height={40} rx={6} fill={GRAY_LT} />
|
|
{/* base */}
|
|
<rect x={-56} y={32} width={112} height={10} rx={5} fill={STROKE} />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function ServerStack({ x, y }: { x: number; y: number }) {
|
|
return (
|
|
<g transform={`translate(${x}, ${y})`}>
|
|
{[0, 1, 2].map((i) => (
|
|
<g key={i} transform={`translate(0, ${-38 + i * 28})`}>
|
|
<rect x={-56} y={-12} width={112} height={24} rx={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
|
|
<rect x={-46} y={-6} width={56} height={12} rx={6} fill={GRAY_LT} />
|
|
<circle cx={20} cy={0} r={4} fill={GRAY} />
|
|
<circle cx={30} cy={0} r={4} fill={ACCENT} />
|
|
<circle cx={40} cy={0} r={4} fill={GRAY} />
|
|
</g>
|
|
))}
|
|
{/* tiny rack base */}
|
|
<rect x={-18} y={48} width={36} height={6} rx={3} fill={GRAY} />
|
|
<rect x={-10} y={54} width={20} height={6} rx={3} fill={ACCENT} />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function Cloud({ x, y }: { x: number; y: number }) {
|
|
return (
|
|
<g transform={`translate(${x}, ${y})`} fill={STROKE}>
|
|
<circle cx={-30} cy={0} r={18} fill={STROKE} />
|
|
<circle cx={-8} cy={-10} r={22} fill={STROKE} />
|
|
<circle cx={16} cy={0} r={20} fill={STROKE} />
|
|
<rect x={-40} y={0} width={72} height={20} rx={10} fill={STROKE} />
|
|
<rect x={-46} y={18} width={88} height={6} rx={3} fill={STROKE} />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function Arrow({ d, delay = 0 }: { d: string; delay?: number }) {
|
|
return (
|
|
<motion.path
|
|
d={d}
|
|
fill="none"
|
|
stroke={STROKE}
|
|
strokeWidth={4}
|
|
strokeLinecap="round"
|
|
initial={{ pathLength: 0, opacity: 0.3 }}
|
|
animate={{ pathLength: 1, opacity: 1 }}
|
|
transition={{ duration: 0.8, delay, ease: [0.22, 1, 0.36, 1] }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/** Small packet traveling along keyframe x/y arrays */
|
|
function Packet({
|
|
xs,
|
|
ys,
|
|
delay = 0,
|
|
color = ACCENT,
|
|
duration = 2.2,
|
|
}: {
|
|
xs: number[];
|
|
ys: number[];
|
|
delay?: number;
|
|
color?: string;
|
|
duration?: number;
|
|
}) {
|
|
const prefersReduced = useReducedMotion();
|
|
return (
|
|
<motion.circle
|
|
r={6}
|
|
fill={color}
|
|
initial={{ x: xs[0], y: ys[0], opacity: 0 }}
|
|
animate={{
|
|
x: prefersReduced ? xs[0] : xs,
|
|
y: prefersReduced ? ys[0] : ys,
|
|
opacity: 1,
|
|
}}
|
|
transition={{
|
|
delay,
|
|
duration: prefersReduced ? 0.01 : duration,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
repeat: prefersReduced ? 0 : Infinity,
|
|
repeatDelay: 0.6,
|
|
}}
|
|
stroke="#fff"
|
|
strokeWidth={2}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default function ProxyForwarding({ className, bg = '#ffffff' }: Props) {
|
|
const W = 1000;
|
|
const H = 420;
|
|
|
|
// Key points
|
|
const C1 = { x: 140, y: 90 };
|
|
const C2 = { x: 140, y: 210 };
|
|
const C3 = { x: 140, y: 330 };
|
|
|
|
const PROXY = { x: 420, y: 210 };
|
|
const CLOUD = { x: 640, y: 210 };
|
|
const DEST = { x: 860, y: 210 };
|
|
|
|
return (
|
|
<div className={className} aria-hidden="true" role="img" style={{ background: bg }}>
|
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
|
|
{/* subtle grid bg */}
|
|
<defs>
|
|
<pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
|
|
<path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
|
|
</pattern>
|
|
</defs>
|
|
<rect width={W} height={H} fill="url(#grid)" />
|
|
|
|
{/* Clients */}
|
|
<Laptop x={C1.x} y={C1.y} />
|
|
<Laptop x={C2.x} y={C2.y} />
|
|
<Laptop x={C3.x} y={C3.y} />
|
|
|
|
{/* Proxy (stack) */}
|
|
<ServerStack x={PROXY.x} y={PROXY.y} />
|
|
|
|
{/* Cloud / Internet */}
|
|
<Cloud x={CLOUD.x} y={CLOUD.y} />
|
|
|
|
{/* Destination servers */}
|
|
<ServerStack x={DEST.x} y={DEST.y} />
|
|
|
|
{/* Arrows: clients -> proxy */}
|
|
<Arrow d={`M ${C1.x + 70} ${C1.y} C 260 ${C1.y}, 320 150, ${PROXY.x - 80} 170`} delay={0.05} />
|
|
<Arrow d={`M ${C2.x + 70} ${C2.y} L ${PROXY.x - 80} ${PROXY.y}`} delay={0.1} />
|
|
<Arrow d={`M ${C3.x + 70} ${C3.y} C 260 ${C3.y}, 320 270, ${PROXY.x - 80} 250`} delay={0.15} />
|
|
|
|
{/* Arrow: proxy -> cloud -> destination */}
|
|
<Arrow d={`M ${PROXY.x + 80} ${PROXY.y} L ${CLOUD.x - 60} ${CLOUD.y}`} delay={0.2} />
|
|
<Arrow d={`M ${CLOUD.x + 60} ${CLOUD.y} L ${DEST.x - 80} ${DEST.y}`} delay={0.25} />
|
|
|
|
{/* Packets flowing from clients to proxy */}
|
|
<Packet xs={[C1.x + 70, PROXY.x - 80]} ys={[C1.y, 170]} delay={0.0} />
|
|
<Packet xs={[C2.x + 70, PROXY.x - 80]} ys={[C2.y, PROXY.y]} delay={0.3} color={GRAY} />
|
|
<Packet xs={[C3.x + 70, PROXY.x - 80]} ys={[C3.y, 250]} delay={0.6} />
|
|
|
|
{/* Packets moving through proxy to cloud */}
|
|
<Packet xs={[PROXY.x + 80, CLOUD.x - 60]} ys={[PROXY.y, CLOUD.y]} delay={0.4} />
|
|
<Packet xs={[PROXY.x + 80, CLOUD.x - 60]} ys={[PROXY.y, CLOUD.y]} delay={0.9} color={GRAY} />
|
|
|
|
{/* Packets leaving cloud to destination */}
|
|
<Packet xs={[CLOUD.x + 60, DEST.x - 80]} ys={[CLOUD.y, DEST.y]} delay={0.7} />
|
|
<Packet xs={[CLOUD.x + 60, DEST.x - 80]} ys={[CLOUD.y, DEST.y]} delay={1.1} color={GRAY} />
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|