forked from sashaastiadi/www_mycelium_net
151 lines
5.6 KiB
TypeScript
151 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 (gray/black + accent only) */
|
|
const ACCENT = '#00b8db';
|
|
const STROKE = '#111827'; // black-ish
|
|
const GRAY = '#9CA3AF';
|
|
const GRAY_LT = '#E5E7EB';
|
|
|
|
function Envelope({
|
|
x, y, w = 88, h = 56, fill = GRAY_LT, accent = false, delay = 0, duration = 1.6,
|
|
path = 'none', // 'left1' | 'left2' | 'rightTop' | 'rightBottom' | 'none'
|
|
reverse = false,
|
|
}: {
|
|
x: number; y: number; w?: number; h?: number; fill?: string; accent?: boolean;
|
|
delay?: number; duration?: number; path?: 'left1'|'left2'|'rightTop'|'rightBottom'|'none'; reverse?: boolean;
|
|
}) {
|
|
const prefersReduced = useReducedMotion();
|
|
|
|
// simple keyframe paths (straight line segments)
|
|
const paths: Record<string, { x: number[]; y: number[] }> = {
|
|
left1: { x: [x, 380], y: [y, 220] },
|
|
left2: { x: [x, 380], y: [y, 220] },
|
|
rightTop: { x: [380, 720], y: [220, 150] },
|
|
rightBottom: { x: [380, 720], y: [220, 290] },
|
|
none: { x: [x], y: [y] },
|
|
};
|
|
|
|
const k = paths[path];
|
|
|
|
return (
|
|
<motion.g
|
|
initial={{ opacity: 0, scale: 0.98 }}
|
|
animate={{
|
|
opacity: 1,
|
|
scale: 1,
|
|
x: prefersReduced ? 0 : (reverse ? [...k.x].reverse() : k.x),
|
|
y: prefersReduced ? 0 : (reverse ? [...k.y].reverse() : k.y),
|
|
}}
|
|
transition={{
|
|
delay,
|
|
duration: prefersReduced ? 0.01 : duration,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
repeat: prefersReduced ? 0 : Infinity,
|
|
repeatDelay: 0.6,
|
|
}}
|
|
>
|
|
{/* envelope body */}
|
|
<rect x={-w / 2} y={-h / 2} width={w} height={h} rx={8} fill={fill} stroke={STROKE} strokeWidth={3} />
|
|
{/* flap */}
|
|
<path
|
|
d={`M ${-w/2+4} ${-h/2+6} L 0 ${-h/2+26} L ${w/2-4} ${-h/2+6}`}
|
|
fill="none"
|
|
stroke={accent ? ACCENT : STROKE}
|
|
strokeWidth={4}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</motion.g>
|
|
);
|
|
}
|
|
|
|
export default function MessageBus({ className, bg = '#ffffff' }: Props) {
|
|
const W = 900;
|
|
const H = 460;
|
|
|
|
return (
|
|
<div className={className} aria-hidden="true" role="img" style={{ background: bg }}>
|
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
|
|
|
|
{/* subtle grid */}
|
|
<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)" />
|
|
|
|
{/* producers (left) */}
|
|
{[{cx:140,cy:120},{cx:140,cy:340}].map((n,i)=>(
|
|
<g key={i}>
|
|
<circle cx={n.cx} cy={n.cy} r={44} fill="#fff" stroke={STROKE} strokeWidth={4}/>
|
|
{/* arrows toward queue */}
|
|
<motion.path
|
|
d={`M ${n.cx+48} ${n.cy} L 320 ${n.cy>200?260:180}`}
|
|
fill="none" stroke={STROKE} strokeWidth={4} strokeLinecap="round"
|
|
initial={{ pathLength: 0, opacity: 0.3 }}
|
|
animate={{ pathLength: 1, opacity: 1 }}
|
|
transition={{ duration: 0.9, delay: 0.1 + i*0.1, ease: [0.22,1,0.36,1] }}
|
|
/>
|
|
</g>
|
|
))}
|
|
|
|
{/* consumers (right) */}
|
|
{[{cx:760,cy:120},{cx:760,cy:340}].map((n,i)=>(
|
|
<g key={i}>
|
|
<circle cx={n.cx} cy={n.cy} r={44} fill="#fff" stroke={STROKE} strokeWidth={4}/>
|
|
{/* arrows out from queue */}
|
|
<motion.path
|
|
d={`M 560 ${i===0?180:260} L ${n.cx-48} ${n.cy}`}
|
|
fill="none" stroke={STROKE} strokeWidth={4} strokeLinecap="round"
|
|
initial={{ pathLength: 0, opacity: 0.3 }}
|
|
animate={{ pathLength: 1, opacity: 1 }}
|
|
transition={{ duration: 0.9, delay: 0.2 + i*0.1, ease: [0.22,1,0.36,1] }}
|
|
/>
|
|
</g>
|
|
))}
|
|
|
|
{/* central queue container */}
|
|
<rect x={330} y={150} width={240} height={140} rx={24} fill="#fff" stroke={STROKE} strokeWidth={4} />
|
|
{/* inner slots (visual only) */}
|
|
{[0,1,2].map(i=>(
|
|
<rect key={i} x={350 + i*76} y={170} width={64} height={100} rx={12} fill="none" stroke={GRAY} strokeWidth={3}/>
|
|
))}
|
|
|
|
{/* inbound envelopes (from left nodes to queue) */}
|
|
<Envelope x={200} y={120} accent fill="#fff" path="left1" delay={0.0} duration={2.0}/>
|
|
<Envelope x={200} y={340} fill={GRAY_LT} path="left2" delay={0.4} duration={2.2}/>
|
|
<Envelope x={200} y={340} accent fill="#fff" path="left2" delay={0.9} duration={2.0}/>
|
|
|
|
{/* queue “processing” envelopes (pulse inside slots) */}
|
|
{[0,1,2].map((i)=>(
|
|
<motion.g key={i} transform={`translate(${382 + i*76} 220)`}>
|
|
<motion.rect
|
|
x={-28} y={-18} width={56} height={36} rx={8}
|
|
fill={i===2 ? ACCENT : GRAY_LT}
|
|
stroke={STROKE} strokeWidth={3}
|
|
initial={{ opacity: 0.6 }}
|
|
animate={{ opacity: [0.6, 1, 0.6] }}
|
|
transition={{ duration: 1.8, repeat: Infinity, delay: i*0.2 }}
|
|
/>
|
|
<path d="M -24 -12 L 0 0 L 24 -12" fill="none" stroke={i===2 ? STROKE : STROKE} strokeWidth={4} strokeLinecap="round" />
|
|
</motion.g>
|
|
))}
|
|
|
|
{/* outbound envelopes (from queue to right nodes) */}
|
|
<Envelope x={560} y={180} accent fill="#fff" path="rightTop" delay={0.6} duration={2.1}/>
|
|
<Envelope x={560} y={260} fill={GRAY_LT} path="rightBottom" delay={1.0} duration={2.3}/>
|
|
<Envelope x={560} y={260} accent fill="#fff" path="rightBottom" delay={1.5} duration={2.0}/>
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|