// Mapa: propiedad de CONFIG -> nombre de variable CSS const CSS_VARS = { greenNodesColor: "--ide-primary", greenStrokeColor: "--ide-primary-light", linkLineColor: "--ide-primary-line", linkGreenColor: "--ide-primary-line", linkGreenToGreenColor: "--ide-primary-light2", // Color para líneas entre nodos verdes linkActiveColor: "--ide-primary-light", purpleCoreLineColor: "--ide-primary-line", shadowCanva: "--shadow-canva", textMain: "--text-main", textInverse: "--text-inverse", centerBoxBg: "--ide-center-bg", // Fondo del recuadro central centerBoxBorder: "--ide-center-border", // Borde del recuadro central centerBoxText: "--ide-center-text" // Texto del recuadro central }; // Mapa: id de nodo violeta -> variable CSS const PURPLE_NODE_VARS = { p_top: "--ide-excellence", p_bottom: "--ide-academy", p_left: "--ide-projects", p_right: "--ide-digital" }; // Variable CSS común para todos los amarillos const YELLOW_NODE_VAR = "--ide-primary-dark"; /* ========================================================================== 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), greenStrokeColor: readCSSVariable(CSS_VARS.greenStrokeColor), // 2. Estilos de líneas punteadas y dinámicas linkLineColor: readCSSVariable(CSS_VARS.linkLineColor), linkGreenColor: readCSSVariable(CSS_VARS.linkGreenColor), linkGreenToGreenColor: readCSSVariable(CSS_VARS.linkGreenToGreenColor, readCSSVariable(CSS_VARS.linkGreenColor, "#24333E")), linkActiveColor: readCSSVariable(CSS_VARS.linkActiveColor), purpleCoreLineColor: readCSSVariable(CSS_VARS.purpleCoreLineColor), shadowCanva: readCSSVariable(CSS_VARS.shadowCanva), textMain: readCSSVariable(CSS_VARS.textMain), textInverse: readCSSVariable(CSS_VARS.textInverse), // 3. Estilos del recuadro central "EL TODO" centerBoxBg: readCSSVariable(CSS_VARS.centerBoxBg, readCSSVariable(YELLOW_NODE_VAR, "#1b2631")), centerBoxBorder: readCSSVariable(CSS_VARS.centerBoxBorder, readCSSVariable(CSS_VARS.purpleCoreLineColor, "#3a5160")), centerBoxText: readCSSVariable(CSS_VARS.centerBoxText, readCSSVariable(CSS_VARS.textInverse, "#ffffff")), // Datos del componente central centerBox: { id: "center_box", label: "EL TODO", desc: "Núcleo integrador central del sistema.", x: 615, y: 260, baseX: 615, baseY: 260, width: 104, height: 36, borderRadius: 8, speedOffset: Math.random() * 10 }, // 4. Nodos VIOLETAS centrales (posiciones, color individual, leyenda) purpleNodes: [ { id: "p_top", x: 615, y: 185, radius: 42, label: "Excellence", color: readCSSVariable(PURPLE_NODE_VARS.p_top), desc: "Servidor central de control y despacho de tareas." }, { id: "p_bottom", x: 615, y: 335, radius: 42, label: "Academy", color: readCSSVariable(PURPLE_NODE_VARS.p_bottom), desc: "Base de datos y redundancia estructural sur." }, { id: "p_left", x: 485, y: 260, radius: 42, label: "Project", color: readCSSVariable(PURPLE_NODE_VARS.p_left), desc: "Pasarela occidental hacia submódulos de entrada." }, { id: "p_right", x: 745, y: 260, radius: 42, label: "Digital", color: readCSSVariable(PURPLE_NODE_VARS.p_right), desc: "Concentrador oriental hacia subredes de distribución." } ], // 5. Nodos AMARILLOS: Cada uno conectado a 2 VIOLETAS yellowNodes: [ // --- Lado Izquierdo --- { id: "yl_1", x: 420, y: 130, r: 26, label: "5S", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Capacitación e Impmentación", purpleTargets: ["p_top", "p_left"], side: "left" }, { id: "yl_2", x: 350, y: 165, r: 24, label: "ISO 9001", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Certificación", purpleTargets: ["p_top", "p_left"], side: "left" }, { id: "yl_3", x: 355, y: 245, r: 25, label: "KAIZEN", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Modificación de Procesos", purpleTargets: ["p_left", "p_top"], side: "left" }, { id: "yl_4", x: 335, y: 335, r: 26, label: "BPM", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Automatización de Procesos de Venta", purpleTargets: ["p_left", "p_bottom"], side: "left" }, { id: "yl_5", x: 395, y: 415, r: 28, label: "KPI", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Implementación y Monitoreo", purpleTargets: ["p_bottom", "p_left"], side: "left" }, { id: "yl_6", x: 255, y: 350, r: 25, label: "PDCA", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Capacitación y Mejora de Taller", purpleTargets: ["p_left", "p_bottom"], side: "left" }, // --- Lado Derecho --- { id: "yr_1", x: 800, y: 135, r: 25, label: "BPR", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Capacitación e Impmentación", purpleTargets: ["p_top", "p_right"], side: "right" }, { id: "yr_2", x: 875, y: 155, r: 25, label: "ISO45001", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Certificación", purpleTargets: ["p_top", "p_right"], side: "right" }, { id: "yr_3", x: 950, y: 195, r: 25, label: "LEAN", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Modificación de Procesos", purpleTargets: ["p_right", "p_top"], side: "right" }, { id: "yr_4", x: 875, y: 235, r: 25, label: "ISO14001", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Automatización de Procesos de Venta", purpleTargets: ["p_right", "p_top"], side: "right" }, { id: "yr_5", x: 890, y: 300, r: 25, label: "PMO", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Mejoras en procedimientos de Atención de situaciones críticas", purpleTargets: ["p_right", "p_bottom"], side: "right" }, { id: "yr_6", x: 965, y: 345, r: 25, label: "SIGMA", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Capacitación y Mejora de Taller", purpleTargets: ["p_right", "p_bottom"], side: "right" }, { id: "yr_7", x: 890, y: 375, r: 25, label: "IA", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Automatización de Toma de Decisiones", purpleTargets: ["p_bottom", "p_right"], side: "right" }, { id: "yr_8", x: 830, y: 420, r: 26, label: "RPA", color: readCSSVariable(YELLOW_NODE_VAR, "#24333E"), desc: "Implementación y Monitoreo", 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: 1060, 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 (Múltiples conexiones por verde) const yellows = nodes.filter(n => n.type === "yellow"); const greens = nodes.filter(n => n.type === "green"); // Configura cuántos amarillos conectar a cada verde (recomendado: 2 o 3) const YELLOWS_PER_GREEN = 2; greens.forEach((gNode, gIdx) => { // 1. Filtrar amarillos que pertenezcan al mismo sector (left/right) const candidateYellows = yellows.filter(y => y.side === gNode.side); // 2. Ordenar de menor a mayor distancia al nodo verde 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; }); // 3. Tomar los N amarillos más cercanos const targets = candidateYellows.slice(0, YELLOWS_PER_GREEN); targets.forEach((yNode, targetRank) => { // Alternar arco: el primero arquea hacia un lado y el segundo hacia el opuesto // Esto previene que las líneas coincidan o se solapen const direction = (gIdx + targetRank) % 2 === 0 ? 1 : -1; const baseCurvature = 0.16 + (targetRank * 0.07); const curvature = baseCurvature * direction; addLink( yNode.id, gNode.id, "yellow-green", true, curvature ); }); }); // PASO DE SEGURIDAD: asegurar que ningún amarillo quede sin al menos un verde yellows.forEach(yNode => { const hasConnection = links.some( l => l.type === "yellow-green" && (l.from === yNode.id || l.to === yNode.id) ); if (!hasConnection) { 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, 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 ); } }); // 5. CONEXIÓN VERDES <-> VERDES (mismo sector, líneas rectas) ["left", "right"].forEach(side => { const sectorGreens = greens.filter(g => g.side === side); sectorGreens.forEach(gNode => { const neighbors = sectorGreens .filter(other => other.id !== gNode.id) .map(other => ({ id: other.id, dist: Math.hypot(other.baseX - gNode.baseX, other.baseY - gNode.baseY) })) .sort((a, b) => a.dist - b.dist); // Conecta con los 4 vecinos más cercanos del mismo sector para formar la malla limpia neighbors.slice(0, 5).forEach(neighbor => { if (neighbor.dist <= 130) { addLink(gNode.id, neighbor.id, "green-green", false); } }); }); }); } // 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); }); // Movimiento coordinado de "EL TODO" según el centroide de los nodos principales const purples = nodes.filter(n => n.type === "purple"); if (purples.length > 0 && CONFIG.centerBox) { const avgX = purples.reduce((sum, p) => sum + p.x, 0) / purples.length; const avgY = purples.reduce((sum, p) => sum + p.y, 0) / purples.length; if (hoveredNode && hoveredNode.id === CONFIG.centerBox.id) { CONFIG.centerBox.x = 615; CONFIG.centerBox.y = 260; } else { const t = (time * 0.0016) + CONFIG.centerBox.speedOffset; CONFIG.centerBox.x = avgX + Math.sin(t) * (maxOffset * 0.45); CONFIG.centerBox.y = avgY + Math.cos(t * 0.85) * (maxOffset * 0.35); } } //aceleracion de la red const isCenterActive = hoveredNode && CONFIG.centerBox && hoveredNode.id === CONFIG.centerBox.id; dashOffset -= isCenterActive ? 0.95 : 0.35; dashOffset -= 0.35; } // Renderizado de líneas curvadas y rectas 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 ? CONFIG.linkActiveColor : 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 if (link.type === "green-green") { ctx.setLineDash([3, 4]); ctx.lineDashOffset = dashOffset * 0.8; ctx.strokeStyle = isHighlight ? CONFIG.linkActiveColor : CONFIG.linkGreenToGreenColor; ctx.lineWidth = isHighlight ? 1.8 : 0.9; } 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(); } // Dibujo del recuadro central "EL TODO" function drawCenterBox() { const box = CONFIG.centerBox; if (!box) return; const w = box.width; const h = box.height; const r = box.borderRadius; const x = box.x - w / 2; const y = box.y - h / 2; const isBoxHovered = hoveredNode && hoveredNode.id === box.id; ctx.save(); ctx.beginPath(); if (ctx.roundRect) { ctx.roundRect(x, y, w, h, r); } else { // Compatibilidad en navegadores antiguos ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); } if (isBoxHovered) { ctx.shadowColor = CONFIG.shadowCanva; ctx.shadowBlur = 14; } else { ctx.shadowColor = "rgba(0, 0, 0, 0.25)"; ctx.shadowBlur = 6; } ctx.fillStyle = CONFIG.centerBoxBg; ctx.fill(); ctx.strokeStyle = isBoxHovered ? CONFIG.linkActiveColor : CONFIG.centerBoxBorder; ctx.lineWidth = isBoxHovered ? 2.0 : 1.4; ctx.stroke(); ctx.restore(); // Texto "EL TODO" ctx.save(); ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = CONFIG.centerBoxText; ctx.font = "bold 11.5px system-ui, sans-serif"; ctx.fillText(box.label, box.x, box.y); 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 const isCenterBoxHovered = hoveredNode && CONFIG.centerBox && hoveredNode.id === CONFIG.centerBox.id; links.forEach(link => { const s = nodeMap.get(link.from); const t = nodeMap.get(link.to); if (!s || !t) return; // Se ilumina si estás sobre un nodo conectado O si estás sobre "EL TODO" const isHighlight = isCenterBoxHovered || (hoveredNode && (hoveredNode.id === s.id || hoveredNode.id === t.id)); drawConnection(s, t, link, isHighlight); });; // 2. Dibujar recuadro central "EL TODO" (situado en el plano intermedio para enmascarar cruces) drawCenterBox(); // 3. 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 = node.color; ctx.lineWidth = 1.8; } else if (node.type === "yellow") { ctx.fillStyle = node.color; ctx.strokeStyle = node.color; 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 = CONFIG.shadowCanva; ctx.shadowBlur = 14; } ctx.fill(); ctx.stroke(); ctx.restore(); // 4. 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 = "#ffffff"; 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; } } // Detección sobre el recuadro central "EL TODO" if (!found && CONFIG.centerBox) { const box = CONFIG.centerBox; const halfW = box.width / 2; const halfH = box.height / 2; if (x >= box.x - halfW && x <= box.x + halfW && y >= box.y - halfH && y <= box.y + halfH) { found = { id: box.id, label: box.label, desc: box.desc, radius: halfH, x: box.x, y: box.y }; } } 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 ========================================================================== */ 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.linkGreenToGreenColor = readCSSVariable(CSS_VARS.linkGreenToGreenColor, CONFIG.linkGreenToGreenColor); CONFIG.linkActiveColor = readCSSVariable(CSS_VARS.linkActiveColor, CONFIG.linkActiveColor); CONFIG.purpleCoreLineColor = readCSSVariable(CSS_VARS.purpleCoreLineColor, CONFIG.purpleCoreLineColor); CONFIG.centerBoxBg = readCSSVariable(CSS_VARS.centerBoxBg, CONFIG.centerBoxBg); CONFIG.centerBoxBorder = readCSSVariable(CSS_VARS.centerBoxBorder, CONFIG.centerBoxBorder); CONFIG.centerBoxText = readCSSVariable(CSS_VARS.centerBoxText, CONFIG.centerBoxText); // 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); } }); } function watchThemeChanges() { const observer = new MutationObserver(() => { refreshColorsFromCSS(); }); 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);