dwssite/static/script.js
2025-01-26 16:58:18 -05:00

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());