fix some sections

This commit is contained in:
2026-09-09 04:00:47 -03:00
parent b3b62362e8
commit 49e90d6062
4 changed files with 571 additions and 56 deletions
+302
View File
@@ -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);
}
}
})();
+164
View File
@@ -0,0 +1,164 @@
/**
* Ripple Waves Background - Versión con Distorsión de Ruido Topográfico
* Reemplaza o actualiza tu archivo ripple-bg.js
*/
(() => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initRipple);
} else {
initRipple();
}
function initRipple() {
const canvas = document.getElementById('ripple-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const parent = canvas.parentElement;
// =========================================================================
// PARÁMETROS CONFIGURABLES
// =========================================================================
const CONFIG = {
spacing: 36, // Separación entre anillos
speed: 0.32, // Velocidad de expansión hacia afuera
lineWidth: 1.2, // Grosor de las líneas
maxAlpha: 0.42, // Opacidad máxima
centerOffsetX: 0.5, // Centro horizontal (50%)
centerOffsetY: 0.2, // Centro vertical (alineado con el h2)
// --- PARÁMETROS DE RUIDO / DISTORSIÓN ---
distortionStrength: 14, // Qué tanto se deforma el círculo (en px)
distortionFrequency: 3, // Cantidad de ondulaciones en el perímetro
evolutionSpeed: 0.00001, // Velocidad con la que muta la forma de las ondas
segmentsPerRing: 25, // Calidad/resolución del contorno (70 a 100)
cssVar: '--ripple-color', // Variable de tu CSS
fallbackColor: 'rgba(239, 68, 68, 0.4)'
};
let width = 0;
let height = 0;
let dpr = 1;
let maxRadius = 0;
let offset = 0;
let time = 0;
let currentColor = CONFIG.fallbackColor;
// --- Leer variable de color desde CSS ---
function updateThemeColor() {
const color = getComputedStyle(document.documentElement)
.getPropertyValue(CONFIG.cssVar)
.trim();
currentColor = color || CONFIG.fallbackColor;
}
updateThemeColor();
const observer = new MutationObserver(updateThemeColor);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] });
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeColor);
}
// --- Adaptación de resolución Retina / HiDPI ---
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = parent.clientWidth;
height = parent.clientHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
const cx = width * CONFIG.centerOffsetX;
const cy = height * CONFIG.centerOffsetY;
const dx = Math.max(cx, width - cx);
const dy = Math.max(cy, height - cy);
maxRadius = Math.sqrt(dx * dx + dy * dy) + CONFIG.spacing * 2;
}
resize();
if (window.ResizeObserver) {
new ResizeObserver(resize).observe(parent);
} else {
window.addEventListener('resize', resize);
}
// --- Función de Pseudo-Ruido Armónico ---
// Combina ondas senoidales a distintas frecuencias para simular turbulencia/ruido suave
function getNoiseDeformation(angle, baseRadius, t) {
const f = CONFIG.distortionFrequency;
const w1 = Math.sin(angle * f + t + baseRadius * 0.015);
const w2 = Math.cos(angle * (f + 2) - t * 0.8 + baseRadius * 0.02);
const w3 = Math.sin(angle * 2 + t * 0.4);
// Crece ligeramente a medida que el anillo viaja hacia afuera
const growthFactor = 0.5 + (baseRadius / maxRadius) * 0.5;
return (w1 * 0.55 + w2 * 0.35 + w3 * 0.1) * CONFIG.distortionStrength * growthFactor;
}
// --- Loop de Animación ---
function render() {
ctx.clearRect(0, 0, width, height);
const cx = width * CONFIG.centerOffsetX;
const cy = height * CONFIG.centerOffsetY;
offset = (offset + CONFIG.speed) % CONFIG.spacing;
time += CONFIG.evolutionSpeed * 1000;
ctx.save();
ctx.strokeStyle = currentColor;
ctx.lineWidth = CONFIG.lineWidth;
const angleStep = (Math.PI * 2) / CONFIG.segmentsPerRing;
// Dibujar cada anillo deformado
for (let r = offset; r < maxRadius; r += CONFIG.spacing) {
if (r <= 2) continue;
const progress = r / maxRadius;
// Desvanecimiento suave en centro y bordes
let alpha = 1;
if (progress < 0.12) {
alpha = progress / 0.12;
} else {
alpha = Math.cos(((progress - 0.12) / 0.88) * (Math.PI / 2));
}
ctx.globalAlpha = Math.max(0, Math.min(1, alpha * CONFIG.maxAlpha));
ctx.beginPath();
// Trazar el contorno deformado punto a punto
for (let i = 0; i <= CONFIG.segmentsPerRing; i++) {
const angle = i * angleStep;
const deformation = getNoiseDeformation(angle, r, time);
const currentRadius = Math.max(0, r + deformation);
const px = cx + Math.cos(angle) * currentRadius;
const py = cy + Math.sin(angle) * currentRadius;
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
ctx.stroke();
}
ctx.restore();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
})();