first commit
This commit is contained in:
139
src/components/islands/PatchBay.tsx
Normal file
139
src/components/islands/PatchBay.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
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 md:grid-cols-[1fr_auto_1fr] gap-10 md:gap-20 bg-[color:var(--color-surface)] border border-[color:var(--color-border)] rounded-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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user