forked from emre/www_projectmycelium_com
- Replaced em dashes with commas for better readability in descriptions - Standardized punctuation in aria-labels (changed "—" to ",") - Removed unnecessary em dashes in favor of commas or removed punctuation - Fixed inconsistent spacing around punctuation marks - Improved sentence flow in multiple component descriptions
287 lines
6.7 KiB
TypeScript
287 lines
6.7 KiB
TypeScript
"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 = 12,
|
|
accent = "#00b8db",
|
|
pulse = false,
|
|
delay = 0,
|
|
type = "dot",
|
|
}: {
|
|
x: number;
|
|
y: number;
|
|
r?: number;
|
|
accent?: string;
|
|
pulse?: boolean;
|
|
delay?: number;
|
|
type?: "dot" | "text" | "image" | "audio";
|
|
}) => {
|
|
const prefers = useReducedMotion();
|
|
|
|
return (
|
|
<>
|
|
{/* faint ring */}
|
|
<motion.circle
|
|
cx={x}
|
|
cy={y}
|
|
r={r + 10}
|
|
stroke="#1F2937"
|
|
strokeWidth={2}
|
|
fill="none"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 0.7 }}
|
|
transition={{ delay, duration: 0.6 }}
|
|
/>
|
|
|
|
{/* inner icon shape to represent modality */}
|
|
{type === "text" && (
|
|
<motion.rect
|
|
x={x - r}
|
|
y={y - r / 2}
|
|
width={r * 2}
|
|
height={r}
|
|
rx={3}
|
|
fill={accent}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay }}
|
|
/>
|
|
)}
|
|
{type === "image" && (
|
|
<motion.path
|
|
d={`M ${x - r} ${y + r/2} L ${x - r} ${y - r/2} L ${x + r} ${y - r/2} L ${x + r} ${y + r/2} Z`}
|
|
stroke={accent}
|
|
strokeWidth={2}
|
|
fill="none"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay }}
|
|
/>
|
|
)}
|
|
{type === "audio" && (
|
|
<motion.path
|
|
d={`
|
|
M ${x - r/2} ${y - r/2}
|
|
L ${x - r/2} ${y + r/2}
|
|
M ${x} ${y - r/3}
|
|
L ${x} ${y + r/3}
|
|
M ${x + r/2} ${y - r/4}
|
|
L ${x + r/2} ${y + r/4}
|
|
`}
|
|
stroke={accent}
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay }}
|
|
/>
|
|
)}
|
|
|
|
{/* standard pulsing circle fallback */}
|
|
{type === "dot" && (
|
|
<motion.circle
|
|
cx={x}
|
|
cy={y}
|
|
r={r}
|
|
fill={accent}
|
|
initial={{ opacity: 0, scale: 0.85 }}
|
|
animate={{
|
|
opacity: 1,
|
|
scale: pulse && !prefers ? [1, 1.12, 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],
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
// inward pulse particle
|
|
const Packet = ({
|
|
path,
|
|
delay = 0,
|
|
accent = "#00b8db",
|
|
duration = 1.8,
|
|
}: {
|
|
path: string;
|
|
delay?: number;
|
|
accent?: string;
|
|
duration?: number;
|
|
}) => {
|
|
const prefers = useReducedMotion();
|
|
return (
|
|
<motion.circle
|
|
r={4}
|
|
fill={accent}
|
|
initial={{ offsetDistance: "100%", opacity: 0 }}
|
|
animate={{
|
|
offsetDistance: ["100%", "0%"],
|
|
opacity: [0, 1, 0],
|
|
}}
|
|
transition={{
|
|
delay,
|
|
duration,
|
|
repeat: !prefers ? Infinity : 0,
|
|
repeatType: "loop",
|
|
ease: "linear",
|
|
}}
|
|
style={{ offsetPath: `path('${path}')` }}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default function Herodb({
|
|
className,
|
|
accent = "#00b8db",
|
|
bg = "#0a0a0a",
|
|
}: Props) {
|
|
const center = { x: 380, y: 210 };
|
|
|
|
const shardNodes = [
|
|
{ x: 160, y: 100, type: "text" },
|
|
{ x: 580, y: 120, type: "image" },
|
|
{ x: 620, y: 280, type: "audio" },
|
|
{ x: 420, y: 330, type: "text" },
|
|
{ x: 240, y: 320, type: "image" },
|
|
{ x: 150, y: 220, type: "dot" },
|
|
];
|
|
|
|
const paths = shardNodes.map(
|
|
(n) =>
|
|
`M ${n.x} ${n.y} Q ${(n.x + center.x) / 2} ${(n.y + center.y) / 2 - 40} ${center.x} ${center.y}`
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={clsx("relative overflow-hidden", className)}
|
|
aria-hidden="true"
|
|
role="img"
|
|
aria-label="HeroDB, active AI memory retrieval"
|
|
style={{ background: bg }}
|
|
>
|
|
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full" preserveAspectRatio="xMidYMid slice">
|
|
{/* 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.18" />
|
|
<stop offset="100%" stopColor={accent} stopOpacity="0" />
|
|
</radialGradient>
|
|
</defs>
|
|
|
|
<rect width={W} height={H} fill="url(#grid-dark)" />
|
|
|
|
{/* global halo */}
|
|
<circle
|
|
cx={center.x}
|
|
cy={center.y}
|
|
r={200}
|
|
fill="url(#glow)"
|
|
opacity={0.45}
|
|
/>
|
|
|
|
{/* core retrieval sphere */}
|
|
<motion.circle
|
|
cx={center.x}
|
|
cy={center.y}
|
|
r={22}
|
|
fill={accent}
|
|
initial={{ opacity: 0, scale: 0.85 }}
|
|
animate={{
|
|
opacity: 1,
|
|
scale: [1, 1.05, 1],
|
|
}}
|
|
transition={{
|
|
duration: 1.6,
|
|
repeat: Infinity,
|
|
repeatType: "mirror",
|
|
ease: "easeInOut",
|
|
}}
|
|
/>
|
|
|
|
{/* core aura ring */}
|
|
<motion.circle
|
|
cx={center.x}
|
|
cy={center.y}
|
|
r={40}
|
|
stroke={accent}
|
|
strokeWidth={2}
|
|
fill="none"
|
|
animate={{
|
|
opacity: [0.2, 0.7, 0.2],
|
|
}}
|
|
transition={{
|
|
duration: 2.2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
}}
|
|
/>
|
|
|
|
{/* inward paths */}
|
|
{paths.map((p, i) => (
|
|
<motion.path
|
|
key={`path-${i}`}
|
|
d={p}
|
|
stroke="#1F2937"
|
|
strokeWidth={2}
|
|
fill="none"
|
|
initial={{ pathLength: 0, opacity: 0 }}
|
|
animate={{ pathLength: 1, opacity: 0.4 }}
|
|
transition={{
|
|
delay: 0.05 * i,
|
|
duration: 0.7,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* packets flowing inward */}
|
|
{paths.map((p, i) => (
|
|
<Packet
|
|
key={`pkt-${i}`}
|
|
path={p}
|
|
delay={i * 0.3}
|
|
accent={accent}
|
|
duration={1.8}
|
|
/>
|
|
))}
|
|
|
|
{/* modality nodes */}
|
|
{shardNodes.map((n, i) => (
|
|
<Node
|
|
key={`node-${i}`}
|
|
x={n.x}
|
|
y={n.y}
|
|
type={n.type as any}
|
|
r={12}
|
|
accent={accent}
|
|
pulse={i % 2 === 0}
|
|
delay={i * 0.1}
|
|
/>
|
|
))}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|