fix some sections
This commit is contained in:
+302
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Plexus 3D Hero - Red de Nodos Conectados para Hero
|
||||
* Integración con Three.js y soporte dinámico para Modo Claro / Oscuro vía CSS Variables.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
// Asegurarse de que el DOM esté listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPlexus);
|
||||
} else {
|
||||
initPlexus();
|
||||
}
|
||||
|
||||
function initPlexus() {
|
||||
// 1. Validar dependencia de Three.js
|
||||
if (typeof THREE === 'undefined') {
|
||||
console.warn('[Plexus 3D] Three.js no está cargado. Asegúrate de incluir Three.js antes de este script.');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('hero-3d-container');
|
||||
if (!container) {
|
||||
// Si el contenedor no existe en esta página, no ejecutar nada
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CONFIGURACIÓN GENERAL
|
||||
// =========================================================================
|
||||
const CONFIG = {
|
||||
nodeCount: 100, // Cantidad de nodos (puntos)
|
||||
radius: 120, // Radio general de la esfera/nube 3D
|
||||
maxDistance: 60, // Distancia máxima para conectar dos nodos con línea
|
||||
nodeSize: 6, // Tamaño del punto en píxeles
|
||||
driftSpeed: 0.0005, // Velocidad de flotación orgánica propia
|
||||
driftAmplitude: 12, // Rango máximo de oscilación (mantiene la forma)
|
||||
mouseFactor: 0.35, // Grado de rotación en respuesta al cursor
|
||||
lerpSpeed: 0.05, // Suavidad de la inercia del movimiento (0.01 - 0.1)
|
||||
|
||||
// Variables CSS que el script vigilará en tu :root / .dark
|
||||
cssVars: {
|
||||
nodeColor: '--ide-primary',
|
||||
lineColor: '--ide-primary'
|
||||
},
|
||||
|
||||
// Colores de respaldo en caso de que las variables no existan en el CSS
|
||||
fallbacks: {
|
||||
nodeColor: '#0a2540',
|
||||
lineColor: '#008080'
|
||||
},
|
||||
lineOpacity: 0.35 // Opacidad de las líneas
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// LECTURA REACTIVA DE VARIABLES CSS
|
||||
// =========================================================================
|
||||
function getCSSVariable(varName, fallback) {
|
||||
const val = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(varName)
|
||||
.trim();
|
||||
return val || fallback;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CONFIGURACIÓN THREE.JS (Escena, Cámara y Renderer)
|
||||
// =========================================================================
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const width = container.clientWidth || 400;
|
||||
const height = container.clientHeight || 450;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(50, width / height, 1, 1000);
|
||||
camera.position.z = 320;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true, // Fondo 100% transparente para integrarse con el Hero
|
||||
antialias: true, // Suavizado de bordes
|
||||
powerPreference: 'high-performance'
|
||||
});
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// Grupo principal para aplicar rotación y control de inercia
|
||||
const cloudGroup = new THREE.Group();
|
||||
scene.add(cloudGroup);
|
||||
|
||||
// =========================================================================
|
||||
// GENERACIÓN DE NODOS (Distribución Esférica Uniforme)
|
||||
// =========================================================================
|
||||
const nodes = [];
|
||||
const nodePositions = new Float32Array(CONFIG.nodeCount * 3);
|
||||
|
||||
for (let i = 0; i < CONFIG.nodeCount; i++) {
|
||||
// Distribución equidistante en esfera (Espiral de Fibonacci 3D)
|
||||
const phi = Math.acos(-1 + (2 * i) / CONFIG.nodeCount);
|
||||
const theta = Math.sqrt(CONFIG.nodeCount * Math.PI) * phi;
|
||||
|
||||
// Variación radial controlada para darle volumen tridimensional
|
||||
const r = CONFIG.radius * (0.65 + 0.35 * Math.random());
|
||||
const baseX = r * Math.cos(theta) * Math.sin(phi);
|
||||
const baseY = r * Math.sin(theta) * Math.sin(phi);
|
||||
const baseZ = r * Math.cos(phi);
|
||||
|
||||
nodes.push({
|
||||
base: new THREE.Vector3(baseX, baseY, baseZ),
|
||||
current: new THREE.Vector3(baseX, baseY, baseZ),
|
||||
speedOffset: Math.random() * Math.PI * 2,
|
||||
axisFactors: [
|
||||
0.8 + Math.random() * 0.4,
|
||||
0.8 + Math.random() * 0.4,
|
||||
0.8 + Math.random() * 0.4
|
||||
]
|
||||
});
|
||||
|
||||
nodePositions[i * 3] = baseX;
|
||||
nodePositions[i * 3 + 1] = baseY;
|
||||
nodePositions[i * 3 + 2] = baseZ;
|
||||
}
|
||||
|
||||
// Geometría y textura circular suave para los nodos
|
||||
const pointsGeometry = new THREE.BufferGeometry();
|
||||
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(nodePositions, 3));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 28, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
const pointTexture = new THREE.CanvasTexture(canvas);
|
||||
|
||||
const pointsMaterial = new THREE.PointsMaterial({
|
||||
size: CONFIG.nodeSize,
|
||||
map: pointTexture,
|
||||
transparent: true,
|
||||
alphaTest: 0.05
|
||||
});
|
||||
|
||||
const pointCloud = new THREE.Points(pointsGeometry, pointsMaterial);
|
||||
cloudGroup.add(pointCloud);
|
||||
|
||||
// =========================================================================
|
||||
// LÍNEAS DE CONEXIÓN DINÁMICAS
|
||||
// =========================================================================
|
||||
const maxPossibleLines = (CONFIG.nodeCount * (CONFIG.nodeCount - 1)) / 2;
|
||||
const linePositions = new Float32Array(maxPossibleLines * 6);
|
||||
|
||||
const linesGeometry = new THREE.BufferGeometry();
|
||||
linesGeometry.setAttribute(
|
||||
'position',
|
||||
new THREE.BufferAttribute(linePositions, 3).setUsage(THREE.DynamicDrawUsage)
|
||||
);
|
||||
|
||||
const linesMaterial = new THREE.LineBasicMaterial({
|
||||
transparent: true,
|
||||
opacity: CONFIG.lineOpacity
|
||||
});
|
||||
|
||||
const lines = new THREE.LineSegments(linesGeometry, linesMaterial);
|
||||
cloudGroup.add(lines);
|
||||
|
||||
// =========================================================================
|
||||
// GESTIÓN DE COLORES Y OBSERVADORES DE TEMA (DARK / LIGHT MODE)
|
||||
// =========================================================================
|
||||
function updateThemeColors() {
|
||||
const nodeColorStr = getCSSVariable(CONFIG.cssVars.nodeColor, CONFIG.fallbacks.nodeColor);
|
||||
const lineColorStr = getCSSVariable(CONFIG.cssVars.lineColor, CONFIG.fallbacks.lineColor);
|
||||
|
||||
pointsMaterial.color.set(nodeColorStr);
|
||||
linesMaterial.color.set(lineColorStr);
|
||||
}
|
||||
|
||||
// Aplicar colores al arrancar
|
||||
updateThemeColors();
|
||||
|
||||
// 1. Observar cambios en atributos de <html> y <body> (classes, data-theme, etc.)
|
||||
const observer = new MutationObserver(() => {
|
||||
updateThemeColors();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'data-mode', 'style']
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'data-mode', 'style']
|
||||
});
|
||||
|
||||
// 2. Escuchar cambios de preferencia de color del sistema operativo
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeColors);
|
||||
}
|
||||
|
||||
// Exponer función de refresco manual por si tu toggle dispara eventos personalizados
|
||||
window.refreshPlexusTheme = updateThemeColors;
|
||||
|
||||
// =========================================================================
|
||||
// INTERACCIÓN CON EL MOUSE (PARALAJE E INCLINACIÓN 3D)
|
||||
// =========================================================================
|
||||
let targetRotationX = 0;
|
||||
let targetRotationY = 0;
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
|
||||
// Cálculo de posición normalizada respecto al centro de la ventana o hero
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
const normX = (e.clientX - centerX) / (window.innerWidth / 2);
|
||||
const normY = (e.clientY - centerY) / (window.innerHeight / 2);
|
||||
|
||||
targetRotationY = normX * CONFIG.mouseFactor;
|
||||
targetRotationX = -normY * CONFIG.mouseFactor;
|
||||
}, { passive: true });
|
||||
|
||||
// =========================================================================
|
||||
// BUCLE DE ANIMACIÓN
|
||||
// =========================================================================
|
||||
let time = 0;
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
time += CONFIG.driftSpeed * 1000;
|
||||
|
||||
// 1. Oscilación armónica de los nodos (flotación sin desarme)
|
||||
const currentPosArray = pointsGeometry.attributes.position.array;
|
||||
for (let i = 0; i < CONFIG.nodeCount; i++) {
|
||||
const node = nodes[i];
|
||||
const offset = time * 0.0015 + node.speedOffset;
|
||||
|
||||
node.current.x = node.base.x + Math.sin(offset * node.axisFactors[0]) * CONFIG.driftAmplitude;
|
||||
node.current.y = node.base.y + Math.cos(offset * node.axisFactors[1]) * CONFIG.driftAmplitude;
|
||||
node.current.z = node.base.z + Math.sin(offset * node.axisFactors[2]) * CONFIG.driftAmplitude;
|
||||
|
||||
currentPosArray[i * 3] = node.current.x;
|
||||
currentPosArray[i * 3 + 1] = node.current.y;
|
||||
currentPosArray[i * 3 + 2] = node.current.z;
|
||||
}
|
||||
pointsGeometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// 2. Conectar nodos próximos
|
||||
let lineIndex = 0;
|
||||
const lineArray = linesGeometry.attributes.position.array;
|
||||
|
||||
for (let i = 0; i < CONFIG.nodeCount; i++) {
|
||||
for (let j = i + 1; j < CONFIG.nodeCount; j++) {
|
||||
const dist = nodes[i].current.distanceTo(nodes[j].current);
|
||||
|
||||
if (dist < CONFIG.maxDistance) {
|
||||
lineArray[lineIndex++] = nodes[i].current.x;
|
||||
lineArray[lineIndex++] = nodes[i].current.y;
|
||||
lineArray[lineIndex++] = nodes[i].current.z;
|
||||
|
||||
lineArray[lineIndex++] = nodes[j].current.x;
|
||||
lineArray[lineIndex++] = nodes[j].current.y;
|
||||
lineArray[lineIndex++] = nodes[j].current.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
linesGeometry.setDrawRange(0, lineIndex / 3);
|
||||
linesGeometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// 3. Rotación base continua + interpolación suave hacia el mouse (Lerp)
|
||||
cloudGroup.rotation.y += 0.0012;
|
||||
cloudGroup.rotation.y += (targetRotationY - cloudGroup.rotation.y) * CONFIG.lerpSpeed;
|
||||
cloudGroup.rotation.x += (targetRotationX - cloudGroup.rotation.x) * CONFIG.lerpSpeed;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
// =========================================================================
|
||||
// RESPONSIVE RESIZING (Evita deformaciones y recortes)
|
||||
// =========================================================================
|
||||
function handleResize() {
|
||||
if (!container) return;
|
||||
const newWidth = container.clientWidth;
|
||||
const newHeight = container.clientHeight;
|
||||
|
||||
if (newWidth === 0 || newHeight === 0) return;
|
||||
|
||||
camera.aspect = newWidth / newHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(newWidth, newHeight);
|
||||
}
|
||||
|
||||
// ResizeObserver para cambios precisos en el contenedor del Hero
|
||||
if (window.ResizeObserver) {
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(container);
|
||||
} else {
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user