const container = document.getElementById('patch-container'); const svgOverlay = document.getElementById('cable-overlay'); const portsState = {}; const LED_COLORS = [ // Gruvbox dark bright colors '#fb4934', // red '#b8bb26', // green '#fabd2f', // yellow '#83a598', // blue '#d3869b', // purple '#8ec07c', // aqua '#fe8019', // orange ]; const CABLE_COLORS = [ // Gruvbox light dark colors '#9d0006', // red '#79740e', // green '#b57614', // yellow '#076678', // blue '#8f3f71', // purple '#427b58', // aqua '#af3a03', // orange ]; let cables = []; let dragSource = null; let tempCablePath = null; const NUM_SEGMENTS = 64; const GRAVITY = 0.6; const DAMPING = 0.4; const CONSTRAINT_ITERATIONS = 15; const SWAY_AMPLITUDE = 0.05; const SWAY_FREQUENCY = 0.002; function getPortCenter(portEl) { const rect = portEl.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; } /********************************************** * LED logic **********************************************/ function initPortState(portId) { if (!portsState[portId]) { portsState[portId] = { connectionCount: 0, ledColor: null, ledIsLit: false, nextBlinkTime: 0 }; } } function addConnection(portId) { initPortState(portId); const st = portsState[portId]; st.connectionCount++; if (st.connectionCount === 1) { // choose a color if not set if (!st.ledColor) { st.ledColor = LED_COLORS[Math.floor(Math.random() * LED_COLORS.length)]; } // schedule next blink st.nextBlinkTime = performance.now(); } } function removeConnection(portId) { initPortState(portId); const st = portsState[portId]; if (st.connectionCount > 0) { st.connectionCount--; } if (st.connectionCount <= 0) { st.connectionCount = 0; st.ledIsLit = false; updatePortLED(portId); } } /** Check if we should toggle LED, then update DOM. */ function updateLEDs(now) { for (const portId in portsState) { const st = portsState[portId]; if (st.connectionCount > 0) { if (now >= st.nextBlinkTime) { st.ledIsLit = !st.ledIsLit; const interval = 50 + Math.random() * 150; // 200..800 ms st.nextBlinkTime = now + interval; } } updatePortLED(portId); } } function updatePortLED(portId) { const st = portsState[portId]; const portEl = document.getElementById(portId); if (!portEl) return; const ledEl = portEl.querySelector('.led'); if (!ledEl) return; if (st.connectionCount > 0 && st.ledColor) { ledEl.style.background = st.ledIsLit ? st.ledColor : '#222'; } else { ledEl.style.background = '#222'; } } /********************************************** * Cable creation & removal **********************************************/ function randomColor() { return CABLE_COLORS[Math.floor(Math.random() * CABLE_COLORS.length)]; } function createCablePath(color) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute('stroke', color); path.setAttribute('fill', 'none'); path.setAttribute('stroke-width', '3'); path.classList.add('cable-path'); // We want to click cables to remove them path.addEventListener('click', onCableClick); svgOverlay.appendChild(path); return path; } function createRope(sourceEl, targetEl, numSegments) { const s = getPortCenter(sourceEl); const t = getPortCenter(targetEl); const rope = []; for (let i = 0; i <= numSegments; i++) { const alpha = i / numSegments; rope.push({ x: s.x + (t.x - s.x) * alpha, y: s.y + (t.y - s.y) * alpha, vx: 0, vy: 0 }); } return rope; } function connect(sourceId, targetId, color) { if (!sourceId || !targetId || sourceId === targetId) return; const sourceEl = document.getElementById(sourceId); const targetEl = document.getElementById(targetId); if (!sourceEl || !targetEl) return; color = color || randomColor(); const rope = createRope(sourceEl, targetEl, NUM_SEGMENTS); const dx = rope[rope.length - 1].x - rope[0].x; const dy = rope[rope.length - 1].y - rope[0].y; const totalDist = Math.sqrt(dx * dx + dy * dy); const restLength = totalDist / NUM_SEGMENTS; const pathEl = createCablePath(color); const swayOffset = Math.random() * 1000; cables.push({ sourceId, targetId, pathEl, ropeSegments: rope, restLength, color, swayOffset }); addConnection(sourceId); addConnection(targetId); } function removeCable(cable) { svgOverlay.removeChild(cable.pathEl); cables = cables.filter(c => c !== cable); removeConnection(cable.sourceId); removeConnection(cable.targetId); } function onCableClick(e) { const pathEl = e.currentTarget; const cable = cables.find(c => c.pathEl === pathEl); if (cable) removeCable(cable); } /********************************************** * Dragging logic (user can draw cables) **********************************************/ function handlePortMouseDown(e) { e.preventDefault(); dragSource = e.currentTarget; const color = randomColor(); tempCablePath = createCablePath(color); tempCablePath.style.pointerEvents = 'none'; } function handleMouseMove(e) { if (!dragSource || !tempCablePath) return; const s = getPortCenter(dragSource); const mx = e.clientX, my = e.clientY; const midX = (s.x + mx) / 2, midY = (s.y + my) / 2; const d = ` M ${s.x},${s.y} C ${midX},${s.y} ${midX},${my} ${mx},${my} `; tempCablePath.setAttribute('d', d); } function handleMouseUp(e) { const dropTargetEl = document.elementFromPoint(e.clientX, e.clientY); const portEl = dropTargetEl && dropTargetEl.closest('.port'); if (portEl) { const sourceId = dragSource.id; const targetId = portEl.id; if (sourceId !== targetId) { const color = tempCablePath.getAttribute('stroke'); svgOverlay.removeChild(tempCablePath); tempCablePath = null; connect(sourceId, targetId, color); } else { svgOverlay.removeChild(tempCablePath); tempCablePath = null; } } else { svgOverlay.removeChild(tempCablePath); tempCablePath = null; } dragSource = null; } /*********************************************** * NAS drive-lights random blinking ***********************************************/ let driveLights = []; function initDriveLights() { driveLights = document.querySelectorAll('.drive-light'); driveLights.forEach(light => { light.dataset.nextBlinkTime = '0'; }); } function updateDriveLights(timestamp) { driveLights.forEach(light => { let nextBlink = parseFloat(light.dataset.nextBlinkTime) || 0; if (timestamp >= nextBlink) { light.classList.toggle('lit'); const interval = 300 + Math.random() * 500; // 300..800 ms light.dataset.nextBlinkTime = (timestamp + interval).toString(); } }); } /********************************************** * Main animation loop **********************************************/ function animate(timestamp) { updateLEDs(timestamp); updateDriveLights(timestamp); const dt = 1.0; cables.forEach(cable => { const rope = cable.ropeSegments; const n = rope.length; if (n < 2) return; const sourceEl = document.getElementById(cable.sourceId); const targetEl = document.getElementById(cable.targetId); if (!sourceEl || !targetEl) return; const s = getPortCenter(sourceEl); const t = getPortCenter(targetEl); // anchors rope[0].x = s.x; rope[0].y = s.y; rope[0].vx = 0; rope[0].vy = 0; rope[n - 1].x = t.x; rope[n - 1].y = t.y; rope[n - 1].vx = 0; rope[n - 1].vy = 0; // 1) gravity + wind for interior segments for (let i = 1; i < n - 1; i++) { rope[i].vy += GRAVITY; const wave = Math.sin((timestamp + cable.swayOffset + i * 50) * SWAY_FREQUENCY); rope[i].vx += wave * SWAY_AMPLITUDE; rope[i].vx *= DAMPING; rope[i].vy *= DAMPING; rope[i].x += rope[i].vx * dt; rope[i].y += rope[i].vy * dt; } // 2) constraints for (let iter = 0; iter < CONSTRAINT_ITERATIONS; iter++) { for (let i = 0; i < n - 1; i++) { let p1 = rope[i], p2 = rope[i + 1]; let dx = p2.x - p1.x, dy = p2.y - p1.y; let dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.001) dist = 0.001; let diff = (dist - cable.restLength) / dist; if (i !== 0) { p1.x += 0.5 * dx * diff; p1.y += 0.5 * dy * diff; } if (i + 1 !== n - 1) { p2.x -= 0.5 * dx * diff; p2.y -= 0.5 * dy * diff; } } } // 3) recalc velocities for (let i = 1; i < n - 1; i++) { rope[i].vx = rope[i].x - (rope[i].x - rope[i].vx * dt); rope[i].vy = rope[i].y - (rope[i].y - rope[i].vy * dt); } // 4) update path let pathD = `M ${rope[0].x},${rope[0].y}`; for (let i = 1; i < n; i++) { pathD += ` L ${rope[i].x},${rope[i].y}`; } cable.pathEl.setAttribute('d', pathD); }); requestAnimationFrame(animate); } /********************************************** * Startup **********************************************/ initDriveLights(); document.querySelectorAll('.port').forEach(portEl => { portEl.addEventListener('mousedown', handlePortMouseDown); }); container.addEventListener('mousemove', handleMouseMove); container.addEventListener('mouseup', handleMouseUp); requestAnimationFrame(animate); connect('r1s0_portA', 'r1s2_portA',randomColor()); connect('r1s2_portB', 'r2s1_portA',randomColor()); connect('r1s2_portC', 'r1s3_portA',randomColor()); connect('r2s1_portB', 'r2s2_portS',randomColor()); connect('r2s2_portA', 'r2s3_portA',randomColor()); connect('r2s2_portB', 'r3s1_portA',randomColor()); connect('r2s2_portC', 'r3s2_portA',randomColor()); connect('r2s2_portD', 'r3s3_portA',randomColor()); connect('r2s1_portD', 'r2s1_portG',randomColor()); connect('r2s1_portK', 'r3s2_portB',randomColor());