/** * 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); } })();