forked from sashaastiadi/www_mycelium_net
234 lines
6.1 KiB
TypeScript
234 lines
6.1 KiB
TypeScript
// pathfinding.tsx
|
|
// Animated SVG illustrating "Automatic pathfinding"
|
|
// - Central hub + surrounding nodes
|
|
// - Arrows fade/slide in
|
|
// - Shortest path highlights on loop
|
|
// - Respects prefers-reduced-motion
|
|
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
import clsx from 'clsx';
|
|
|
|
|
|
type Props = {
|
|
className?: string; // e.g. "w-full h-64"
|
|
accent?: string; // main accent color
|
|
stroke?: string; // neutral stroke color
|
|
bg?: string; // background color
|
|
};
|
|
|
|
const Node = ({
|
|
cx,
|
|
cy,
|
|
r = 16,
|
|
fill = "#00b8db",
|
|
ring = "#E5E7EB",
|
|
pulse = false,
|
|
rMotion = 2,
|
|
}: {
|
|
cx: number;
|
|
cy: number;
|
|
r?: number;
|
|
fill?: string;
|
|
ring?: string;
|
|
pulse?: boolean;
|
|
rMotion?: number;
|
|
}) => {
|
|
const prefersReduced = useReducedMotion();
|
|
|
|
return (
|
|
<>
|
|
{/* outer ring */}
|
|
<motion.circle
|
|
cx={cx}
|
|
cy={cy}
|
|
r={r + 14}
|
|
fill="none"
|
|
stroke={ring}
|
|
strokeWidth={2}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.6 }}
|
|
/>
|
|
{/* core node */}
|
|
<motion.circle
|
|
cx={cx}
|
|
cy={cy}
|
|
r={r}
|
|
fill={fill}
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{
|
|
opacity: 1,
|
|
scale: pulse && !prefersReduced ? [1, 1 + rMotion / 16, 1] : 1,
|
|
}}
|
|
transition={{
|
|
duration: pulse && !prefersReduced ? 1.8 : 0.6,
|
|
repeat: pulse && !prefersReduced ? Infinity : 0,
|
|
repeatType: "loop",
|
|
ease: [0.22, 1, 0.36, 1],
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Arrow = ({
|
|
d,
|
|
color = "#111827",
|
|
delay = 0,
|
|
}: {
|
|
d: string;
|
|
color?: string;
|
|
delay?: number;
|
|
}) => (
|
|
<motion.path
|
|
d={d}
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth={3}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
initial={{ pathLength: 0, opacity: 0 }}
|
|
animate={{ pathLength: 1, opacity: 1 }}
|
|
transition={{
|
|
delay,
|
|
duration: 0.8,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const DashedPath = ({
|
|
d,
|
|
color = "#9CA3AF",
|
|
dash = 6,
|
|
delay = 0,
|
|
loop = false,
|
|
}: {
|
|
d: string;
|
|
color?: string;
|
|
dash?: number;
|
|
delay?: number;
|
|
loop?: boolean;
|
|
}) => {
|
|
const prefersReduced = useReducedMotion();
|
|
|
|
return (
|
|
<motion.path
|
|
d={d}
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth={3}
|
|
strokeDasharray={dash}
|
|
strokeLinecap="round"
|
|
initial={{ pathLength: 0, opacity: 0.4 }}
|
|
animate={{
|
|
pathLength: 1,
|
|
opacity: 1,
|
|
}}
|
|
transition={{
|
|
delay,
|
|
duration: 0.9,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
repeat: !prefersReduced && loop ? Infinity : 0,
|
|
repeatDelay: 1.2,
|
|
repeatType: "reverse",
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default function Pathfinding({
|
|
className,
|
|
accent = "#00b8db", // indigo-800 vibe
|
|
stroke = "#111827", // gray-900
|
|
bg = "#FFFFFF",
|
|
}: Props) {
|
|
// Canvas
|
|
const W = 760;
|
|
const H = 420;
|
|
|
|
// Layout (simple radial)
|
|
const center = { x: 380, y: 210 };
|
|
const nodes = [
|
|
{ x: 130, y: 210 }, // left
|
|
{ x: 670, y: 210 }, // right
|
|
{ x: 380, y: 70 }, // top
|
|
{ x: 280, y: 340 }, // bottom-left
|
|
{ x: 500, y: 340 }, // bottom-right
|
|
];
|
|
|
|
// Helper to make arrow path with a small head
|
|
const arrowTo = (from: { x: number; y: number }, to: { x: number; y: number }) => {
|
|
const dx = to.x - from.x;
|
|
const dy = to.y - from.y;
|
|
const len = Math.hypot(dx, dy);
|
|
const ux = dx / len;
|
|
const uy = dy / len;
|
|
const end = { x: to.x - ux * 18, y: to.y - uy * 18 }; // inset a bit
|
|
const headL = {
|
|
x: end.x - uy * 8 - ux * 6,
|
|
y: end.y + ux * 8 - uy * 6,
|
|
};
|
|
const headR = {
|
|
x: end.x + uy * 8 - ux * 6,
|
|
y: end.y - ux * 8 - uy * 6,
|
|
};
|
|
return `M ${from.x} ${from.y} L ${end.x} ${end.y} M ${headL.x} ${headL.y} L ${end.x} ${end.y} L ${headR.x} ${headR.y}`;
|
|
};
|
|
|
|
// "Shortest" highlighted route: left -> center -> bottom-right
|
|
const highlightA = `M ${nodes[0].x} ${nodes[0].y} L ${center.x} ${center.y}`;
|
|
const highlightB = `M ${center.x} ${center.y} L ${nodes[4].x} ${nodes[4].y}`;
|
|
|
|
// Faint alternative routes
|
|
const alt1 = `M ${nodes[2].x} ${nodes[2].y} L ${center.x} ${center.y}`;
|
|
const alt2 = `M ${nodes[3].x} ${nodes[3].y} L ${center.x} ${center.y}`;
|
|
const alt3 = `M ${center.x} ${center.y} L ${nodes[1].x} ${nodes[1].y}`;
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"relative overflow-hidden",
|
|
className
|
|
)}
|
|
aria-hidden="true"
|
|
role="img"
|
|
aria-label="Automatic pathfinding between nodes"
|
|
style={{ background: bg }}
|
|
>
|
|
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
|
|
{/* background 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="#F3F4F6" strokeWidth="1" />
|
|
</pattern>
|
|
</defs>
|
|
<rect width={W} height={H} fill="url(#grid)" />
|
|
|
|
{/* faint alternative connections */}
|
|
<DashedPath d={alt1} color="#E5E7EB" dash={5} delay={0.1} />
|
|
<DashedPath d={alt2} color="#E5E7EB" dash={5} delay={0.2} />
|
|
<DashedPath d={alt3} color="#E5E7EB" dash={5} delay={0.3} />
|
|
|
|
{/* highlighted “shortest path” (animates / pulses) */}
|
|
<DashedPath d={highlightA} color={accent} dash={8} delay={0.2} loop />
|
|
<DashedPath d={highlightB} color={accent} dash={8} delay={0.4} loop />
|
|
|
|
{/* directional arrows toward the center (auto routing) */}
|
|
<Arrow d={arrowTo(nodes[0], center)} color={stroke} delay={0.1} />
|
|
<Arrow d={arrowTo(nodes[2], center)} color={stroke} delay={0.2} />
|
|
<Arrow d={arrowTo(nodes[3], center)} color={stroke} delay={0.25} />
|
|
<Arrow d={arrowTo(nodes[1], center)} color={stroke} delay={0.3} />
|
|
|
|
{/* nodes */}
|
|
<Node cx={center.x} cy={center.y} r={18} fill={accent} ring="#E5E7EB" pulse />
|
|
{nodes.map((n, i) => (
|
|
<Node key={i} cx={n.x} cy={n.y} r={14} fill="#FFFFFF" ring="#E5E7EB" />
|
|
))}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|