140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
|
|
interface Props {
|
|
technical: string[];
|
|
soft: string[];
|
|
}
|
|
|
|
export default function PatchBay({ technical, soft }: Props) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current;
|
|
const svg = svgRef.current;
|
|
if (!container || !svg) return;
|
|
|
|
function drawCables() {
|
|
if (!svg || !container) return;
|
|
const rect = container.getBoundingClientRect();
|
|
const width = rect.width;
|
|
const height = rect.height;
|
|
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
svg.setAttribute('width', String(width));
|
|
svg.setAttribute('height', String(height));
|
|
|
|
// Find left + right jack positions
|
|
const leftJacks = container.querySelectorAll<HTMLElement>('[data-jack="left"]');
|
|
const rightJacks = container.querySelectorAll<HTMLElement>('[data-jack="right"]');
|
|
const cr = container.getBoundingClientRect();
|
|
|
|
const leftPoints = Array.from(leftJacks).map((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
return { x: r.right - cr.left, y: r.top + r.height / 2 - cr.top };
|
|
});
|
|
const rightPoints = Array.from(rightJacks).map((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
return { x: r.left - cr.left, y: r.top + r.height / 2 - cr.top };
|
|
});
|
|
|
|
// Build cable paths pairing left to right
|
|
const colors = ['var(--color-cyan)', 'var(--color-magenta)', 'var(--color-acid)'];
|
|
svg.innerHTML = '';
|
|
leftPoints.forEach((p1, i) => {
|
|
const pairIdx = i % rightPoints.length;
|
|
const p2 = rightPoints[pairIdx];
|
|
if (!p2) return;
|
|
const dx = p2.x - p1.x;
|
|
const sag = 20 + (i % 3) * 8;
|
|
const midY = (p1.y + p2.y) / 2 + sag;
|
|
const c1x = p1.x + dx * 0.4;
|
|
const c2x = p1.x + dx * 0.6;
|
|
const d = `M ${p1.x} ${p1.y} C ${c1x} ${midY}, ${c2x} ${midY}, ${p2.x} ${p2.y}`;
|
|
|
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
path.setAttribute('d', d);
|
|
path.setAttribute('fill', 'none');
|
|
path.setAttribute('stroke', colors[i % colors.length]);
|
|
path.setAttribute('stroke-width', '1.6');
|
|
path.setAttribute('stroke-opacity', '0.55');
|
|
path.style.filter = `drop-shadow(0 0 4px ${colors[i % colors.length]})`;
|
|
svg.appendChild(path);
|
|
|
|
// Plug tips
|
|
[p1, p2].forEach((p) => {
|
|
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
circle.setAttribute('cx', String(p.x));
|
|
circle.setAttribute('cy', String(p.y));
|
|
circle.setAttribute('r', '3');
|
|
circle.setAttribute('fill', colors[i % colors.length]);
|
|
circle.style.filter = `drop-shadow(0 0 6px ${colors[i % colors.length]})`;
|
|
svg.appendChild(circle);
|
|
});
|
|
});
|
|
}
|
|
|
|
drawCables();
|
|
const ro = new ResizeObserver(drawCables);
|
|
ro.observe(container);
|
|
window.addEventListener('resize', drawCables);
|
|
return () => {
|
|
ro.disconnect();
|
|
window.removeEventListener('resize', drawCables);
|
|
};
|
|
}, [technical.length, soft.length]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="relative grid grid-cols-1 sm:grid-cols-2 md:grid-cols-[1fr_auto_1fr] gap-6 sm:gap-10 md:gap-20 bg-[color:var(--color-surface)] border border-[color:var(--color-border)] rounded-sm p-5 sm:p-6 md:p-10"
|
|
>
|
|
{/* Left panel : technical */}
|
|
<div>
|
|
<h3 className="stencil text-sm uppercase tracking-[0.25em] text-[color:var(--color-cyan)] glow-text-cyan mb-4">
|
|
Techniques
|
|
</h3>
|
|
<ul className="space-y-2.5">
|
|
{technical.map((s, i) => (
|
|
<li key={s} className="flex items-center gap-3 font-mono text-xs uppercase tracking-wider">
|
|
<span className="text-[color:var(--color-text-muted)] flex-1">{s}</span>
|
|
<span
|
|
data-jack="left"
|
|
className="w-3 h-3 rounded-full border-2 border-[color:var(--color-cyan)] bg-[color:var(--color-bg)]"
|
|
style={{ boxShadow: `inset 0 0 0 1px #000` }}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Middle : SVG cables */}
|
|
<div className="hidden md:block relative min-h-full">
|
|
<svg
|
|
ref={svgRef}
|
|
className="absolute inset-0 pointer-events-none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
/>
|
|
</div>
|
|
|
|
{/* Right panel : soft */}
|
|
<div>
|
|
<h3 className="stencil text-sm uppercase tracking-[0.25em] text-[color:var(--color-magenta)] glow-text-magenta mb-4">
|
|
Transverses
|
|
</h3>
|
|
<ul className="space-y-2.5">
|
|
{soft.map((s, i) => (
|
|
<li key={s} className="flex items-center gap-3 font-mono text-xs uppercase tracking-wider">
|
|
<span
|
|
data-jack="right"
|
|
className="w-3 h-3 rounded-full border-2 border-[color:var(--color-magenta)] bg-[color:var(--color-bg)]"
|
|
style={{ boxShadow: `inset 0 0 0 1px #000` }}
|
|
/>
|
|
<span className="text-[color:var(--color-text-muted)] flex-1">{s}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|