rf-v4.10-ecosistema50%
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/* ==========================================================================
|
||||
ECOSISTEMA IDE — Cambio in-place entre tarjetas y paneles de unidad
|
||||
========================================================================== */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const section = document.getElementById('ecosistema-ide');
|
||||
if (!section) return;
|
||||
|
||||
const stateEcosystem = section.querySelector('[data-eco-state="ecosystem"]');
|
||||
const stateUnit = section.querySelector('[data-eco-state="unit"]');
|
||||
const panels = Array.from(section.querySelectorAll('[data-eco-panel]'));
|
||||
const triggers = Array.from(section.querySelectorAll('[data-eco-open]'));
|
||||
const backBtn = section.querySelector('[data-eco-close]');
|
||||
const pills = Array.from(section.querySelectorAll('.eco-pill'));
|
||||
|
||||
let lastFocusedTrigger = null;
|
||||
|
||||
/* --- Mostrar panel de una unidad --- */
|
||||
function openUnit(id) {
|
||||
const panel = panels.find(p => p.dataset.ecoPanel === id);
|
||||
if (!panel) return;
|
||||
|
||||
// Guardamos el trigger que originó el cambio (para devolver foco)
|
||||
const origin = triggers.find(t => t.dataset.ecoOpen === id);
|
||||
if (origin) lastFocusedTrigger = origin;
|
||||
|
||||
// Ocultar ecosistema, mostrar estado unidad
|
||||
stateEcosystem.hidden = true;
|
||||
stateUnit.hidden = false;
|
||||
|
||||
// Alternar paneles
|
||||
panels.forEach(p => {
|
||||
const isActive = p.dataset.ecoPanel === id;
|
||||
p.hidden = !isActive;
|
||||
if (isActive) {
|
||||
// Re-trigger de la animación de entrada
|
||||
p.style.animation = 'none';
|
||||
// Forzar reflow
|
||||
void p.offsetWidth;
|
||||
p.style.animation = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Aria expandido en tarjetas
|
||||
triggers.forEach(t => {
|
||||
const isThis = t.dataset.ecoOpen === id;
|
||||
t.setAttribute('aria-expanded', isThis ? 'true' : 'false');
|
||||
});
|
||||
|
||||
// Pills activas
|
||||
pills.forEach(pill => {
|
||||
pill.classList.toggle('is-active', pill.dataset.ecoOpen === id);
|
||||
});
|
||||
|
||||
// Foco accesible al título del panel
|
||||
const title = panel.querySelector('.eco-panel__title');
|
||||
if (title) {
|
||||
title.setAttribute('tabindex', '-1');
|
||||
// Pequeño timeout para asegurar visibilidad tras el cambio de estado
|
||||
requestAnimationFrame(() => title.focus({ preventScroll: false }));
|
||||
}
|
||||
|
||||
// Scroll suave al inicio de la sección (solo si quedó fuera de vista)
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (rect.top < -80) {
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Volver al estado ecosistema --- */
|
||||
function closeUnit() {
|
||||
stateUnit.hidden = true;
|
||||
stateEcosystem.hidden = false;
|
||||
|
||||
triggers.forEach(t => t.setAttribute('aria-expanded', 'false'));
|
||||
pills.forEach(p => p.classList.remove('is-active'));
|
||||
|
||||
// Devolver foco a la tarjeta origen
|
||||
if (lastFocusedTrigger && typeof lastFocusedTrigger.focus === 'function') {
|
||||
lastFocusedTrigger.focus({ preventScroll: false });
|
||||
} else {
|
||||
// Fallback: primer botón del grid
|
||||
const first = triggers[0];
|
||||
if (first) first.focus({ preventScroll: false });
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Listeners --- */
|
||||
triggers.forEach(trigger => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
const id = trigger.dataset.ecoOpen;
|
||||
if (!id) return;
|
||||
// Si es un <a> real (academy), dejamos que navegue
|
||||
if (trigger.tagName === 'A' && trigger.getAttribute('href')) return;
|
||||
e.preventDefault();
|
||||
openUnit(id);
|
||||
});
|
||||
|
||||
// Soporte teclado para tarjetas no-button (por si acaso)
|
||||
trigger.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const id = trigger.dataset.ecoOpen;
|
||||
if (!id) return;
|
||||
if (trigger.tagName === 'A') return;
|
||||
e.preventDefault();
|
||||
openUnit(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (backBtn) backBtn.addEventListener('click', closeUnit);
|
||||
|
||||
// Escape cierra el panel expandido
|
||||
section.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !stateUnit.hidden) {
|
||||
closeUnit();
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Deep link opcional por hash (#eco-projects) sin recargar --- */
|
||||
function readHash() {
|
||||
const hash = window.location.hash.replace('#eco-', '');
|
||||
if (['excellence', 'projects', 'digital'].includes(hash)) {
|
||||
openUnit(hash);
|
||||
}
|
||||
}
|
||||
window.addEventListener('hashchange', readHash);
|
||||
readHash();
|
||||
})();
|
||||
+172
-543
@@ -1,543 +1,172 @@
|
||||
// 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);
|
||||
const nodosData = [
|
||||
// Reemplaza el src con la ruta real de tus logos
|
||||
{ id: 'tl', x0: 220, y0: 140, r: 52, color: '#16a085', html: '<img src="assets/logos/excellence_b.png" alt="EXCELLENCE">' },
|
||||
{ id: 'bl', x0: 200, y0: 420, r: 52, color: '#f39200', html: '<img src="assets/logos/projects_b.png" alt="PROJECTS">' },
|
||||
{ id: 'tr', x0: 780, y0: 110, r: 52, color: '#c41230', html: '<img src="assets/logos/academy_b.png" alt="ACADEMY">' },
|
||||
{ id: 'br', x0: 750, y0: 400, r: 52, color: '#0061a0', html: '<img src="assets/logos/digital_b.png" alt="DIGITAL">' },
|
||||
{ id: 'c', x0: 500, y0: 280, r: 70, color: '#ffffff', html: '<img src="assets/logos/ide.png" alt="EL TODO">' }
|
||||
];
|
||||
|
||||
const conexiones = [
|
||||
{ id: 'arcL', from: 'tl', to: 'bl', type: 'vertical', curveOut: -180 },
|
||||
{ id: 'c1', from: 'tl', to: 'c', type: 'horizontal', curveOut: 0 },
|
||||
{ id: 'c2', from: 'bl', to: 'c', type: 'horizontal', curveOut: 0 },
|
||||
{ id: 'arcR', from: 'tr', to: 'br', type: 'vertical', curveOut: 180 },
|
||||
{ id: 'c3', from: 'tr', to: 'c', type: 'horizontal', curveOut: 0 },
|
||||
{ id: 'c4', from: 'br', to: 'c', type: 'horizontal', curveOut: 0 }
|
||||
];
|
||||
|
||||
const contenedorNodos = document.getElementById('grupo-nodos');
|
||||
const grupoLineas = document.getElementById('grupo-lineas');
|
||||
const svgDefs = document.getElementById('svg-defs');
|
||||
const nodosMap = {};
|
||||
|
||||
// 1. Renderizado de Nodos HTML (AHORA FIJOS)
|
||||
nodosData.forEach(nodo => {
|
||||
nodosMap[nodo.id] = nodo;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'nodo-circulo';
|
||||
el.id = `node-${nodo.id}`;
|
||||
el.style.backgroundColor = nodo.color; // Lo comento para que el fondo sea blanco y se vea bien tu logo, descomenta si prefieres el fondo de color
|
||||
//el.style.border = `4px solid ${nodo.color}`; // Agrego un borde con el color corporativo
|
||||
el.style.width = `${(nodo.r * 2 / 1000) * 100}%`;
|
||||
el.style.height = `${(nodo.r * 2 / 500) * 100}%`;
|
||||
|
||||
// Asignamos la posición una única vez
|
||||
nodo.x = nodo.x0;
|
||||
nodo.y = nodo.y0;
|
||||
el.style.left = `${(nodo.x / 1000) * 100}%`;
|
||||
el.style.top = `${(nodo.y / 500) * 100}%`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'nodo-contenido';
|
||||
content.innerHTML = nodo.html;
|
||||
|
||||
el.appendChild(content);
|
||||
contenedorNodos.appendChild(el);
|
||||
nodo.el = el;
|
||||
});
|
||||
|
||||
const offsetsLineas = [-12, -6, 0, 6, 12];
|
||||
|
||||
// 2. Pre-calcular rutas y gradientes (UNA SOLA VEZ)
|
||||
conexiones.forEach(conn => {
|
||||
const n1 = nodosMap[conn.from];
|
||||
const n2 = nodosMap[conn.to];
|
||||
const isHoriz = conn.type === 'horizontal';
|
||||
|
||||
conn.rutasCache = {}; // Diccionario para guardar las rutas
|
||||
|
||||
// Crear y fijar Gradiente
|
||||
const gradId = `grad-${conn.from}-${conn.to}`;
|
||||
const grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
|
||||
grad.setAttribute('id', gradId);
|
||||
grad.setAttribute('gradientUnits', 'userSpaceOnUse');
|
||||
grad.setAttribute('x1', n1.x);
|
||||
grad.setAttribute('y1', n1.y);
|
||||
grad.setAttribute('x2', n2.x);
|
||||
grad.setAttribute('y2', n2.y);
|
||||
|
||||
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
||||
stop1.setAttribute('offset', '0%');
|
||||
stop1.setAttribute('stop-color', n1.color);
|
||||
|
||||
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
||||
stop2.setAttribute('offset', '100%');
|
||||
const colorTarget = n2.id === 'c' ? '#0e626e' : n2.color;
|
||||
stop2.setAttribute('stop-color', colorTarget);
|
||||
|
||||
grad.appendChild(stop1);
|
||||
grad.appendChild(stop2);
|
||||
svgDefs.appendChild(grad);
|
||||
|
||||
// Función matemática
|
||||
const calcularRuta = (offsetX, offsetY) => {
|
||||
let cp1x, cp1y, cp2x, cp2y;
|
||||
if (isHoriz) {
|
||||
const distanciaX = n2.x - n1.x;
|
||||
cp1x = n1.x + distanciaX * 0.5;
|
||||
cp1y = n1.y;
|
||||
cp2x = n2.x - distanciaX * 0.5;
|
||||
cp2y = n2.y;
|
||||
} else {
|
||||
cp1x = n1.x + conn.curveOut;
|
||||
cp1y = n1.y;
|
||||
cp2x = n2.x + conn.curveOut;
|
||||
cp2y = n2.y;
|
||||
}
|
||||
return `M ${n1.x + offsetX} ${n1.y + offsetY} C ${cp1x + offsetX} ${cp1y + offsetY}, ${cp2x + offsetX} ${cp2y + offsetY}, ${n2.x + offsetX} ${n2.y + offsetY}`;
|
||||
};
|
||||
|
||||
// Dibujar y Cachear las 5 líneas base
|
||||
conn.pathsBase = [];
|
||||
offsetsLineas.forEach(offset => {
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.classList.add('linea-base');
|
||||
path.setAttribute('stroke', `url(#${gradId})`);
|
||||
|
||||
const rutaCalculada = calcularRuta(isHoriz ? 0 : offset, isHoriz ? offset : 0);
|
||||
path.setAttribute('d', rutaCalculada);
|
||||
conn.rutasCache[offset] = rutaCalculada; // Guardado en caché
|
||||
|
||||
grupoLineas.appendChild(path);
|
||||
conn.pathsBase.push({ el: path, offset: offset });
|
||||
});
|
||||
|
||||
// Crear pulsos
|
||||
conn.pulsos = [];
|
||||
const cantPulsos = 10;
|
||||
|
||||
for (let i = 0; i < cantPulsos; i++) {
|
||||
const pathPulso = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
pathPulso.classList.add('linea-pulso');
|
||||
|
||||
const carrilInicial = offsetsLineas[Math.floor(Math.random() * offsetsLineas.length)];
|
||||
pathPulso.setAttribute('d', conn.rutasCache[carrilInicial]); // Le asignamos su ruta desde la caché
|
||||
|
||||
grupoLineas.appendChild(pathPulso);
|
||||
|
||||
conn.pulsos.push({
|
||||
el: pathPulso,
|
||||
currentOffset: carrilInicial,
|
||||
needsPathUpdate: false, // Bandera de optimización
|
||||
dash: (1200 / cantPulsos) * i + Math.random() * 50,
|
||||
speed: 2.0 + Math.random() * 1.5
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Lógica de salto entre carriles
|
||||
setInterval(() => {
|
||||
conexiones.forEach(conn => {
|
||||
conn.pulsos.forEach(p => {
|
||||
if (Math.random() > 0.8) {
|
||||
p.currentOffset = offsetsLineas[Math.floor(Math.random() * offsetsLineas.length)];
|
||||
p.needsPathUpdate = true; // Avisamos que en el próximo frame debe cambiar de ruta
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 1200);
|
||||
|
||||
// 4. Motor de Animación Ultraligero
|
||||
function animarFrame() {
|
||||
conexiones.forEach(conn => {
|
||||
conn.pulsos.forEach(p => {
|
||||
p.dash -= p.speed;
|
||||
|
||||
// OPTIMIZACIÓN EXTREMA: Solo interactuamos con el DOM si el pulso saltó de carril
|
||||
if (p.needsPathUpdate) {
|
||||
p.el.setAttribute('d', conn.rutasCache[p.currentOffset]);
|
||||
p.needsPathUpdate = false;
|
||||
}
|
||||
|
||||
// Lo único que se actualiza contínuamente es el desplazamiento
|
||||
p.el.style.strokeDashoffset = p.dash;
|
||||
});
|
||||
});
|
||||
|
||||
requestAnimationFrame(animarFrame);
|
||||
}
|
||||
|
||||
animarFrame();
|
||||
@@ -1,727 +0,0 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user