Files
IDE-Demo-B/js/netschema.js
T
2026-09-10 16:25:29 -03:00

543 lines
19 KiB
JavaScript

// 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",
linkActiveColor: "--ide-primary-light",
purpleCoreLineColor: "--ide-primary-line",
shadowCanva: "--shadow-canva",
textMain: "--text-main",
textInverse: "--text-inverse"
};
// Mapa: id de nodo violeta -> variable CSS
const PURPLE_NODE_VARS = {
p_top: "--ide-excellence",
p_bottom: "--ide-digital",
p_left: "--ide-academy",
p_right: "--ide-projects"
};
// 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),
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. 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: "Digital",
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: "Academy",
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: "Projects",
color: readCSSVariable(PURPLE_NODE_VARS.p_right),
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: "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: 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 ? 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 {
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 = 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();
// 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 = "#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;
}
}
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 <html> y <body>.
========================================================================== */
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 <html> (habitual en data-theme) y <body> (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);