362 lines
10 KiB
JavaScript
362 lines
10 KiB
JavaScript
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());
|
|
|
|
|
|
|