// Mapa: propiedad de CONFIG -> nombre de variable CSS const CSS_VARS = { greenNodesColor: "--ide-primary", greenStrokeColor: "--ide-primary-light", linkLineColor: "--canvas-link-line-color", linkGreenColor: "--canvas-link-green-color", linkActiveColor: "--canvas-link-active-color", purpleCoreLineColor: "--canvas-purple-core-line-color" }; // Mapa: id de nodo violeta -> variable CSS const PURPLE_NODE_VARS = { p_top: "--canvas-purple-p-top-color", p_bottom: "--canvas-purple-p-bottom-color", p_left: "--canvas-purple-p-left-color", p_right: "--canvas-purple-p-right-color" }; // Variable CSS común para todos los amarillos const YELLOW_NODE_VAR = "--canvas-yellow-node-color"; /* ========================================================================== UTILIDAD: LEER VARIABLES CSS ========================================================================== */ function readCSSVariable(variableName, fallback = "") { const value = getComputedStyle(document.documentElement) .getPropertyValue(variableName) .trim(); return value || fallback; } /* ========================================================================== CONFIGURACIÓN GENERAL ========================================================================== */ const CONFIG = { // 1. Color global configurable para TODOS los nodos verdes greenNodesColor: readCSSVariable(CSS_VARS.greenNodesColor, "#9b1717"), greenStrokeColor: readCSSVariable(CSS_VARS.greenStrokeColor, "#630b5f"), // 2. Estilos de líneas punteadas y dinámicas linkLineColor: readCSSVariable(CSS_VARS.linkLineColor, "rgba(95, 110, 240, 0.40)"), linkGreenColor: readCSSVariable(CSS_VARS.linkGreenColor, "rgba(235, 60, 60, 0.42)"), linkActiveColor: readCSSVariable(CSS_VARS.linkActiveColor, "rgba(25, 35, 205, 0.95)"), purpleCoreLineColor: readCSSVariable(CSS_VARS.purpleCoreLineColor, "rgba(35, 35, 35, 0.70)"), // 3. Nodos VIOLETAS centrales (posiciones, color individual, leyenda) purpleNodes: [ { id: "p_top", x: 615, y: 185, radius: 42, label: "Núcleo N", color: readCSSVariable(PURPLE_NODE_VARS.p_top, "#8a00c2"), desc: "Servidor central de control y despacho de tareas." }, { id: "p_bottom", x: 615, y: 335, radius: 42, label: "Núcleo S", color: readCSSVariable(PURPLE_NODE_VARS.p_bottom, "#9900d8"), desc: "Base de datos y redundancia estructural sur." }, { id: "p_left", x: 485, y: 260, radius: 42, label: "Núcleo O", color: readCSSVariable(PURPLE_NODE_VARS.p_left, "#7e00b3"), desc: "Pasarela occidental hacia submódulos de entrada." }, { id: "p_right", x: 745, y: 260, radius: 42, label: "Núcleo E", color: readCSSVariable(PURPLE_NODE_VARS.p_right, "#9400cf"), desc: "Concentrador oriental hacia subredes de distribución." } ], // 4. Nodos AMARILLOS: Cada uno conectado a 2 VIOLETAS yellowNodes: [ // --- Lado Izquierdo --- { id: "yl_1", x: 420, y: 130, r: 26, label: "IZQ-1", color: readCSSVariable(YELLOW_NODE_VAR, "#ffff00"), desc: "Módulo intermedio A-1", purpleTargets: ["p_top", "p_left"], side: "left" }, { id: "yl_2", x: 350, y: 165, r: 24, label: "IZQ-2", color: readCSSVariable(YELLOW_NODE_VAR, "#fff01f"), desc: "Módulo intermedio A-2", purpleTargets: ["p_top", "p_left"], side: "left" }, { id: "yl_3", x: 355, y: 245, r: 25, label: "IZQ-3", color: readCSSVariable(YELLOW_NODE_VAR, "#ffee00"), desc: "Balanceador izquierdo", purpleTargets: ["p_left", "p_top"], side: "left" }, { id: "yl_4", x: 335, y: 335, r: 26, label: "IZQ-4", color: readCSSVariable(YELLOW_NODE_VAR, "#ffff00"), desc: "Filtro de tráfico A-4", purpleTargets: ["p_left", "p_bottom"], side: "left" }, { id: "yl_5", x: 395, y: 415, r: 28, label: "IZQ-5", color: readCSSVariable(YELLOW_NODE_VAR, "#fff33b"), desc: "Procesador de paquetes", purpleTargets: ["p_bottom", "p_left"], side: "left" }, { id: "yl_6", x: 255, y: 350, r: 25, label: "IZQ-6", color: readCSSVariable(YELLOW_NODE_VAR, "#ffee00"), desc: "Enrutador secundario", purpleTargets: ["p_left", "p_bottom"], side: "left" }, // --- Lado Derecho --- { id: "yr_1", x: 800, y: 135, r: 25, label: "DER-1", color: readCSSVariable(YELLOW_NODE_VAR, "#ffff00"), desc: "Concentrador de salida 1", purpleTargets: ["p_top", "p_right"], side: "right" }, { id: "yr_2", x: 875, y: 155, r: 25, label: "DER-2", color: readCSSVariable(YELLOW_NODE_VAR, "#ffee00"), desc: "Concentrador de salida 2", purpleTargets: ["p_top", "p_right"], side: "right" }, { id: "yr_3", x: 950, y: 195, r: 25, label: "DER-3", color: readCSSVariable(YELLOW_NODE_VAR, "#ffff00"), desc: "Distribuidor rápido 3", purpleTargets: ["p_right", "p_top"], side: "right" }, { id: "yr_4", x: 875, y: 235, r: 25, label: "DER-4", color: readCSSVariable(YELLOW_NODE_VAR, "#fff33b"), desc: "Replicador este 4", purpleTargets: ["p_right", "p_top"], side: "right" }, { id: "yr_5", x: 890, y: 300, r: 25, label: "DER-5", color: readCSSVariable(YELLOW_NODE_VAR, "#ffee00"), desc: "Gestor de colas 5", purpleTargets: ["p_right", "p_bottom"], side: "right" }, { id: "yr_6", x: 965, y: 345, r: 25, label: "DER-6", color: readCSSVariable(YELLOW_NODE_VAR, "#ffff00"), desc: "Canal asíncrono 6", purpleTargets: ["p_right", "p_bottom"], side: "right" }, { id: "yr_7", x: 890, y: 375, r: 25, label: "DER-7", color: readCSSVariable(YELLOW_NODE_VAR, "#fff01f"), desc: "Salida perimetral 7", purpleTargets: ["p_bottom", "p_right"], side: "right" }, { id: "yr_8", x: 830, y: 420, r: 26, label: "DER-8", color: readCSSVariable(YELLOW_NODE_VAR, "#ffee00"), desc: "Canal de descarga 8", purpleTargets: ["p_bottom", "p_right"], side: "right" } ] }; /* ========================================================================== MOTOR INTERACTIVO Y RENDERIZADO ========================================================================== */ const canvas = document.getElementById("networkCanvas"); const ctx = canvas.getContext("2d"); const tooltip = document.getElementById("tooltip"); const tooltipTitle = document.getElementById("tooltip-title"); const tooltipDesc = document.getElementById("tooltip-desc"); let nodes = []; let links = []; let hoveredNode = null; let dashOffset = 0; // Construcción de la estructura function initNetwork() { nodes = []; links = []; const linkSet = new Set(); // Previene enlaces duplicados entre mismos nodos function addLink(fromId, toId, type, curved = false, curvature = 0) { const keyA = `${fromId}->${toId}`; const keyB = `${toId}->${fromId}`; if (linkSet.has(keyA) || linkSet.has(keyB)) return; linkSet.add(keyA); links.push({ from: fromId, to: toId, type, curved, curvature }); } // 1. Agregar Nodos Violetas CONFIG.purpleNodes.forEach(p => { nodes.push({ ...p, type: "purple", baseX: p.x, baseY: p.y, speedOffset: Math.random() * 10 }); }); // Conectar los violetas entre sí (malla central) const purples = nodes.filter(n => n.type === "purple"); for (let i = 0; i < purples.length; i++) { for (let j = i + 1; j < purples.length; j++) { addLink(purples[i].id, purples[j].id, "purple-purple", false); } } // 2. Agregar Nodos Amarillos y crear DOBLE conexión curva hacia los violetas CONFIG.yellowNodes.forEach((y) => { nodes.push({ id: y.id, x: y.x, y: y.y, baseX: y.x, baseY: y.y, radius: y.r, label: y.label, color: y.color, desc: y.desc, side: y.side, type: "yellow", speedOffset: Math.random() * 10 }); if (Array.isArray(y.purpleTargets)) { y.purpleTargets.forEach((targetPurpleId, index) => { addLink( targetPurpleId, y.id, "purple-yellow", true, index === 0 ? 0.16 : -0.16 ); }); } }); // 3. Crear Nodos Verdes Terciarios const greenPositions = [ // Lado Izquierdo { id: "gl_1", x: 190, y: 95, side: "left" }, { id: "gl_2", x: 260, y: 120, side: "left" }, { id: "gl_3", x: 140, y: 155, side: "left" }, { id: "gl_4", x: 75, y: 150, side: "left" }, { id: "gl_5", x: 130, y: 225, side: "left" }, { id: "gl_6", x: 65, y: 230, side: "left" }, { id: "gl_7", x: 195, y: 235, side: "left" }, { id: "gl_8", x: 60, y: 290, side: "left" }, { id: "gl_9", x: 165, y: 305, side: "left" }, { id: "gl_10", x: 75, y: 375, side: "left" }, { id: "gl_11", x: 140, y: 380, side: "left" }, { id: "gl_12", x: 145, y: 440, side: "left" }, { id: "gl_13", x: 235, y: 425, side: "left" }, // Lado Derecho { id: "gr_1", x: 1040, y: 90, side: "right" }, { id: "gr_2", x: 960, y: 110, side: "right" }, { id: "gr_3", x: 1025, y: 150, side: "right" }, { id: "gr_4", x: 1095, y: 155, side: "right" }, { id: "gr_5", x: 1160, y: 150, side: "right" }, { id: "gr_6", x: 1030, y: 220, side: "right" }, { id: "gr_7", x: 1105, y: 215, side: "right" }, { id: "gr_8", x: 1170, y: 225, side: "right" }, { id: "gr_9", x: 1060, y: 285, side: "right" }, { id: "gr_10", x: 1120, y: 305, side: "right" }, { id: "gr_11", x: 1175, y: 290, side: "right" }, { id: "gr_12", x: 1025, y: 335, side: "right" }, { id: "gr_13", x: 1095, y: 350, side: "right" }, { id: "gr_14", x: 1035, y: 400, side: "right" }, { id: "gr_15", x: 1085, y: 435, side: "right" }, { id: "gr_16", x: 1165, y: 385, side: "right" }, { id: "gr_17", x: 970, y: 425, side: "right" } ]; greenPositions.forEach(g => { nodes.push({ id: g.id, x: g.x, y: g.y, baseX: g.x, baseY: g.y, radius: 14, type: "green", side: g.side, speedOffset: Math.random() * 10 }); }); // 4. CONEXIÓN AMARILLOS <-> VERDES (cobertura total en ambas direcciones) const yellows = nodes.filter(n => n.type === "yellow"); const greens = nodes.filter(n => n.type === "green"); // PASO A: cada amarillo con al menos 1 verde yellows.forEach((yNode, idx) => { const candidateGreens = greens.filter(g => g.side === yNode.side); candidateGreens.sort((a, b) => { const distA = Math.hypot(a.baseX - yNode.baseX, a.baseY - yNode.baseY); const distB = Math.hypot(b.baseX - yNode.baseX, b.baseY - yNode.baseY); return distA - distB; }); if (candidateGreens.length > 0) { addLink( yNode.id, candidateGreens[0].id, "yellow-green", true, idx % 2 === 0 ? 0.20 : -0.20 ); } }); // PASO B: cada verde con al menos 1 amarillo greens.forEach((gNode, idx) => { const alreadyConnected = links.some( l => l.type === "yellow-green" && (l.from === gNode.id || l.to === gNode.id) ); const candidateYellows = yellows.filter(y => y.side === gNode.side); candidateYellows.sort((a, b) => { const distA = Math.hypot(a.baseX - gNode.baseX, a.baseY - gNode.baseY); const distB = Math.hypot(b.baseX - gNode.baseX, b.baseY - gNode.baseY); return distA - distB; }); if (!alreadyConnected && candidateYellows.length > 0) { addLink( candidateYellows[0].id, gNode.id, "yellow-green", true, idx % 2 === 0 ? -0.22 : 0.22 ); } else if (candidateYellows.length > 1 && idx % 3 === 0) { addLink( candidateYellows[1].id, gNode.id, "yellow-green", true, idx % 2 === 0 ? 0.26 : -0.26 ); } }); } // Bucle de actualización (flotación en zona segura) function update(time) { const maxOffset = 4.5; nodes.forEach(node => { if (hoveredNode && hoveredNode.id === node.id) { node.x = node.baseX; node.y = node.baseY; return; } const t = (time * 0.0016) + node.speedOffset; node.x = node.baseX + Math.sin(t) * maxOffset; node.y = node.baseY + Math.cos(t * 0.85) * (maxOffset * 0.75); }); dashOffset -= 0.35; } // Renderizado de líneas curvadas function drawConnection(source, target, link, isHighlight) { ctx.save(); ctx.beginPath(); ctx.moveTo(source.x, source.y); if (link.curved) { const midX = (source.x + target.x) / 2; const midY = (source.y + target.y) / 2; const dx = target.x - source.x; const dy = target.y - source.y; const dist = Math.hypot(dx, dy); const nx = -dy / dist; const ny = dx / dist; const arcHeight = dist * link.curvature; const cpX = midX + nx * arcHeight; const cpY = midY + ny * arcHeight; ctx.quadraticCurveTo(cpX, cpY, target.x, target.y); } else { ctx.lineTo(target.x, target.y); } if (link.type === "purple-purple") { ctx.setLineDash([4, 4]); ctx.lineDashOffset = dashOffset * 0.5; ctx.strokeStyle = isHighlight ? "#000000" : CONFIG.purpleCoreLineColor; ctx.lineWidth = isHighlight ? 2.6 : 1.8; } else if (link.type === "yellow-green") { ctx.setLineDash([3, 4]); ctx.lineDashOffset = dashOffset; ctx.strokeStyle = isHighlight ? CONFIG.linkActiveColor : CONFIG.linkGreenColor; ctx.lineWidth = isHighlight ? 2.0 : 0.95; } else { ctx.setLineDash([3, 4]); ctx.lineDashOffset = dashOffset; ctx.strokeStyle = isHighlight ? CONFIG.linkActiveColor : CONFIG.linkLineColor; ctx.lineWidth = isHighlight ? 2.0 : 1.05; } ctx.stroke(); ctx.restore(); } // Renderizado del Canvas function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); const nodeMap = new Map(nodes.map(n => [n.id, n])); // 1. Dibujar líneas links.forEach(link => { const s = nodeMap.get(link.from); const t = nodeMap.get(link.to); if (!s || !t) return; const isHighlight = hoveredNode && (hoveredNode.id === s.id || hoveredNode.id === t.id); drawConnection(s, t, link, isHighlight); }); // 2. Dibujar nodos nodes.forEach(node => { ctx.save(); ctx.beginPath(); ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2); if (node.type === "purple") { ctx.fillStyle = node.color; ctx.strokeStyle = "#210036"; ctx.lineWidth = 1.8; } else if (node.type === "yellow") { ctx.fillStyle = node.color; ctx.strokeStyle = "#383500"; ctx.lineWidth = 1.3; } else { ctx.fillStyle = CONFIG.greenNodesColor; ctx.strokeStyle = CONFIG.greenStrokeColor; ctx.lineWidth = 1.1; } if (hoveredNode && hoveredNode.id === node.id) { ctx.shadowColor = "rgba(0, 0, 0, 0.35)"; ctx.shadowBlur = 14; } ctx.fill(); ctx.stroke(); ctx.restore(); // 3. Texto en nodos violetas y amarillos if (node.label) { ctx.save(); ctx.textAlign = "center"; ctx.textBaseline = "middle"; if (node.type === "purple") { ctx.fillStyle = "#ffffff"; ctx.font = "bold 12.5px system-ui, sans-serif"; } else if (node.type === "yellow") { ctx.fillStyle = "#1e1e1e"; ctx.font = "600 10.5px system-ui, sans-serif"; } ctx.fillText(node.label, node.x, node.y); ctx.restore(); } }); } // Loop 60 FPS function animate(time) { update(time); draw(); requestAnimationFrame(animate); } // Coordenadas con soporte responsivo function getCanvasCoordinates(e) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY, cssX: e.clientX - rect.left, cssY: e.clientY - rect.top }; } // Eventos de Mouse canvas.addEventListener("mousemove", (e) => { const { x, y, cssX, cssY } = getCanvasCoordinates(e); const targets = nodes.filter(n => n.type === "purple" || n.type === "yellow"); let found = null; for (let node of targets) { if (Math.hypot(node.x - x, node.y - y) <= node.radius) { found = node; break; } } hoveredNode = found; if (hoveredNode) { canvas.style.cursor = "pointer"; tooltipTitle.textContent = hoveredNode.label; tooltipDesc.textContent = hoveredNode.desc; tooltip.style.left = `${cssX}px`; tooltip.style.top = `${cssY - (hoveredNode.radius * (canvas.getBoundingClientRect().height / canvas.height))}px`; tooltip.classList.remove("hidden"); } else { canvas.style.cursor = "default"; tooltip.classList.add("hidden"); } }); canvas.addEventListener("mouseleave", () => { hoveredNode = null; tooltip.classList.add("hidden"); }); /* ========================================================================== SOPORTE DE TEMA CLARO / OSCURO -------------------------------------------------------------------------- - Re-lee las variables CSS y actualiza CONFIG + nodos vivos. - Observa cambios de `data-theme` o `class` en y . ========================================================================== */ function refreshColorsFromCSS() { // 1. Colores globales CONFIG.greenNodesColor = readCSSVariable(CSS_VARS.greenNodesColor, CONFIG.greenNodesColor); CONFIG.greenStrokeColor = readCSSVariable(CSS_VARS.greenStrokeColor, CONFIG.greenStrokeColor); CONFIG.linkLineColor = readCSSVariable(CSS_VARS.linkLineColor, CONFIG.linkLineColor); CONFIG.linkGreenColor = readCSSVariable(CSS_VARS.linkGreenColor, CONFIG.linkGreenColor); CONFIG.linkActiveColor = readCSSVariable(CSS_VARS.linkActiveColor, CONFIG.linkActiveColor); CONFIG.purpleCoreLineColor = readCSSVariable(CSS_VARS.purpleCoreLineColor, CONFIG.purpleCoreLineColor); // 2. Colores de violetas en CONFIG CONFIG.purpleNodes.forEach(p => { const cssVar = PURPLE_NODE_VARS[p.id]; if (cssVar) p.color = readCSSVariable(cssVar, p.color); }); // 3. Colores de amarillos en CONFIG CONFIG.yellowNodes.forEach(y => { y.color = readCSSVariable(YELLOW_NODE_VAR, y.color); }); // 4. Propagar a los nodos ya instanciados nodes.forEach(node => { if (node.type === "purple" && PURPLE_NODE_VARS[node.id]) { node.color = readCSSVariable(PURPLE_NODE_VARS[node.id], node.color); } else if (node.type === "yellow") { node.color = readCSSVariable(YELLOW_NODE_VAR, node.color); } // Los verdes leen CONFIG.greenNodesColor directo en draw(), no requieren update propio }); } function watchThemeChanges() { const observer = new MutationObserver(() => { refreshColorsFromCSS(); }); // Observa (habitual en data-theme) y (por si acaso) observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] }); if (document.body) { observer.observe(document.body, { attributes: true, attributeFilter: ["data-theme", "class"] }); } } // Inicialización initNetwork(); watchThemeChanges(); requestAnimationFrame(animate);