En esta entrada veremos cómo crear un efecto de onda con JavaScript. Para esto usaremos el elemento Canvas de HTML5 junto con la función Window.requestAnimationFrame(), que ya exploramos a fondo en la entrega anterior: El secreto de las animaciones en JavaScript (requestAnimationFrame()). A modo de repaso rápido: requestAnimationFrame() permite animar figuras dibujadas en un elemento <canvas> de forma sincronizada con el ciclo de refresco del navegador. En otras palabras:
Con requestAnimationFrame() obtienes transiciones suaves gracias a una API nativa optimizada para trabajar en conjunto con el Canvas. A diferencia de setInterval() o setTimeout(), esta función se integra directamente con el motor de renderizado del navegador.
Para más información sobre esta función, consulta el enlace anterior.
Qué es un efecto de onda en JavaScript y por qué usar Canvas
Un efecto de onda (wave animation) es una animación que simula el comportamiento de una ola u oscilación. Puede usarse para una gran variedad de propósitos:
- Visualizaciones de datos en tiempo real
- Fondos animados en páginas de presentación
- Efectos visuales futuristas o artísticos
- Representaciones matemáticas y científicas
Aunque existen efectos de "onda" hechos exclusivamente con CSS (como los botones ripple), el elemento <canvas> te otorga control total sobre cada píxel y cada fotograma. Cuando trabajas con Canvas:
- Dibujas directamente sobre un lienzo virtual.
- Controlas coordenadas, formas, trazos y colores con precisión.
- Puedes aplicar matemáticas reales —trigonometría incluida— al movimiento.
Por eso Canvas es la herramienta ideal para implementar ondas sinusoidales y animaciones complejas en el navegador.
Por qué requestAnimationFrame es clave para animaciones fluidas
Uno de los errores más comunes al empezar con animaciones en JavaScript es recurrir a setInterval() o setTimeout(). Yo mismo lo hice al principio… hasta que empecé a notar tirones, consumo excesivo de CPU y animaciones nada naturales.
requestAnimationFrame() resuelve todos esos problemas porque:
- Se sincroniza con la tasa de refresco del monitor (generalmente 60 fps).
- Pausa automáticamente cuando la pestaña no está activa, ahorrando recursos.
- Produce transiciones notablemente más suaves y naturales.
En animaciones de onda, donde cada fotograma importa, la diferencia respecto a setInterval() es enorme y muy perceptible.
Construyendo la onda con JavaScript y Canvas
El JavaScript es, en realidad, bastante corto y sencillo; aunque puede resultar algo confuso a primera vista por las operaciones matemáticas de sumas, divisiones y funciones trigonométricas. Voy a explicarlo paso a paso de la manera más clara posible.
Antes de ver el código, conviene recordar las funciones seno y coseno que seguramente viste en el colegio o en la universidad. Estas funciones describen curvas que "vienen y van" de forma periódica, exactamente como una ola:

Imagen obtenida de: Wikipedia – Coseno.
Las funciones seno y coseno aplicadas a animaciones web
Cómo se comporta una onda sinusoidal
La función coseno o seno nos viene de maravilla para este experimento, precisamente por su comportamiento oscilatorio. Si no recuerdas bien cómo funcionan, en Internet encontrarás abundante material al respecto; lo esencial aquí es que generan valores que suben y bajan de manera continua y predecible.
Aplicadas en JavaScript, las usamos así:
Math.cos(n)— aplica la función coseno al valorn.Math.sin(n)— aplica la función seno al valorn.
Las funciones trigonométricas
Math.sin()yMath.cos()generan curvas periódicas que suben y bajan suavemente entre-1y1. Justo lo que necesitamos para simular una ola.
La fórmula general de una onda sinusoidal es:
y = amplitud * Math.sin(frecuencia * x + fase);Donde cada parámetro cumple un rol específico:
amplitud— controla la altura máxima de la onda (qué tan "alta" es la cresta).frecuencia— define cuántas oscilaciones completas hay a lo largo del ancho.fase— al incrementarla en cada frame, genera el movimiento de desplazamiento horizontal.
A continuación te presento el código JavaScript completo de la animación:
var c = document.getElementById('canv');
var $ = c.getContext('2d');
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;
var draw = function(t) {
$.lineWidth = 1;
$.fillStyle = 'rgb(0, 0, 0)';
$.fillRect(0, 0, w, h);
for (var i = -60; i < 60; i += 1) {
$.strokeStyle = 'rgb(255, 255, 255)';
$.beginPath();
$.moveTo(0, h / 2);
for (var j = 0; j < w; j += 10) {
$.lineTo(10 * Math.cos(i) +
j + (0.008 * j * j),
Math.floor(h / 2 + j / 2 *
Math.cos(j / 50 - t / 50 - i / 118) +
(i * 0.9) * Math.cos(j / 25 - (i + t) / 65)));
};
$.stroke();
}
}
var t = 0;
window.addEventListener('resize', function() {
c.width = w = window.innerWidth;
c.height = h = window.innerHeight;
}, false);
var run = function() {
window.requestAnimationFrame(run);
t += 5;
draw(t);
};
run();Como podrás imaginar, el HTML consiste en una simple etiqueta <canvas> con un id definido.
Algunas consideraciones sobre el código anterior
Primero inicializamos algunas variables globales para obtener acceso al elemento <canvas>, su contexto 2D y las dimensiones de la ventana:
var c = document.getElementById('canv');
var $ = c.getContext('2d');
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;Dentro de la función draw()
Definimos los estilos del trazo y limpiamos el canvas en cada frame antes de volver a dibujar. Este paso es crítico: si no limpias el canvas con fillRect() antes de redibujar, los trazos anteriores se acumulan y el resultado es un desastre visual.
$.lineWidth = 1;
$.fillStyle = 'rgb(0, 0, 0)';
$.fillRect(0, 0, w, h);
$.strokeStyle = 'rgb(255, 255, 255)';Este primer for permite dibujar un conjunto de líneas en paralelo. Al variar la cota inferior (-60) y la cota superior (60) podemos crear ondas más o menos anchas. Además, este for se encarga de inicializar los componentes necesarios para pintar cada línea (beginPath(), moveTo() y stroke()):
for (var i = -60; i < 60; i += 1) {
$.beginPath();
$.moveTo(0, h / 2);
/* For anidado */
$.stroke();
}En otras palabras, si este for externo no existiera, nuestra onda se vería como un látigo solitario moviéndose de un lado a otro:

Por qué una sola onda se ve artificial
Aquí viene algo que aprendí a base de probar:
cuando usas una sola función seno, la animación se vuelve demasiado perfecta… y precisamente por eso, poco creíble.
Al combinar varias funciones coseno con ligeros desfases (offsets) y ajustes de amplitud distintos, la onda empieza a verse más orgánica e irregular. Ese fue el punto de inflexión en el que mi animación dejó de parecer un ejercicio de matemáticas y empezó a verse viva.
El siguiente fragmento es el for anidado que pinta w puntos por iteración, donde w es el ancho de la pantalla en píxeles:
for (var j = 0; j < w; j += 10) {
$.lineTo(10 * Math.cos(i) +
j + (0.008 * j * j),
Math.floor(h / 2 + j / 2 *
Math.cos(j / 50 - t / 50 - i / 118) +
(i * 0.9) * Math.cos(j / 25 - (i + t) / 65)));
};Como puedes ver, empleamos múltiples llamadas a Math.cos() con argumentos distintos para crear un efecto de onda variado y no monótono. Es la superposición de estas curvas lo que da esa sensación de movimiento natural.
Las divisiones y multiplicaciones por números pequeños —como / 50, / 118 o * 0.9— sirven para mantener los valores dentro de un rango razonable y evitar que la onda se dispare o se deforme a medida que las variables i y j aumentan.
Para que la animación evolucione en el tiempo, cada vez que requestAnimationFrame() invoca recursivamente la función run(), se incrementa el valor de la variable t en 5 unidades. Esta t es la que actúa como fase y produce el movimiento de desplazamiento de la onda.
Finalmente, creamos la función run(), que es la que arranca el bucle de animación al cargarse la página:
var run = function() {
window.requestAnimationFrame(run);
t += 5;
draw(t);
};Dibujando la onda paso a paso: desglose completo de draw()
function draw(t) {
ctx.lineWidth = 1;
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, w, h);
for (var i = -60; i < 60; i++) {
ctx.strokeStyle = 'rgb(255,255,255)';
ctx.beginPath();
ctx.moveTo(0, h / 2);
for (var j = 0; j < w; j += 10) {
ctx.lineTo(
10 * Math.cos(i) + j + (0.008 * j * j),
Math.floor(
h / 2 +
j / 2 * Math.cos(j / 50 - t / 50 - i / 118) +
(i * 0.9) * Math.cos(j / 25 - (i + t) / 65)
)
);
}
ctx.stroke();
}
}En resumen, la función draw() hace tres cosas en cada frame:
- El primer
for(variablei) crea múltiples líneas paralelas con desfases distintos. - El segundo
for(variablej) dibuja cada línea punto a punto a lo largo del ancho del canvas. - La combinación de varios
Math.cos()con argumentos diferentes rompe la simetría perfecta y da organicidad.
Cuando eliminé ese primer for durante mis pruebas, la onda parecía un látigo. Al añadirlo, el efecto ganó profundidad y volumen al instante.
Superposición de ondas para un resultado más orgánico
Esta técnica de superposición genera tres cualidades visuales que una sola onda seno nunca puede ofrecer:
- Variación visual: ninguna zona del canvas se ve igual a otra.
- Sensación de volumen: la superposición de curvas genera la ilusión de profundidad.
- Movimiento no repetitivo: la animación nunca se ve mecánica ni estancada.
Es exactamente lo que los ejemplos simples de una sola onda seno no pueden ofrecer.
Controlando amplitud, frecuencia y movimiento de la onda
- Evitar deformaciones extremas
- Un error típico es no acotar los valores de las variables de los bucles. Si no controlas las divisiones y multiplicaciones en los argumentos de
Math.cos(), la onda empieza a crecer sin control y el resultado es visualmente caótico. - Por experiencia: usar valores de divisor grandes y multiplicadores pequeños mantiene la animación estable y predecible.
- Un error típico es no acotar los valores de las variables de los bucles. Si no controlas las divisiones y multiplicaciones en los argumentos de
- Ajustes que marcan la diferencia visual
- Pequeños cambios producen efectos enormes. Vale la pena experimentar con:
- Incrementar la
fasemás lentamente (reducir el+= 5det). - Variar ligeramente la amplitud multiplicando
ipor un decimal distinto. - Introducir offsets mínimos en los argumentos de
Math.cos()para diversificar el patrón.
- Incrementar la
- Pequeños cambios producen efectos enormes. Vale la pena experimentar con:
- En animaciones de este tipo, menos es más: los ajustes sutiles son los que producen resultados elegantes.
Optimización y rendimiento en animaciones con Canvas
Errores comunes que degradan el rendimiento de una canvas wave animation:
- Redibujar sin limpiar el canvas antes (con
fillRect()oclearRect()). - Realizar demasiados cálculos matemáticos por frame sin cachear resultados intermedios.
- No adaptar el canvas al cambio de tamaño de la ventana (
resize).
Yo siempre escucho el evento resize para que la animación ocupe siempre el 100% de la pantalla:
window.addEventListener('resize', function () {
c.width = w = window.innerWidth;
c.height = h = window.innerHeight;
});Consejos prácticos para animaciones estables y eficientes
- Usa siempre
requestAnimationFrame()en lugar desetInterval(). - Evita bucles innecesarios o anidamientos excesivos dentro de
draw(). - Controla bien las constantes matemáticas: pequeños valores en divisores y multiplicadores hacen la diferencia.
- Prueba en pantallas grandes y pequeñas para asegurarte de que el canvas escala correctamente.
Variaciones del efecto de onda y posibles mejoras
Una vez dominas la base, las posibilidades se multiplican. Puedes explorar:
- Añadir color dinámico usando
hsl()con el valor deiotcomo matiz. - Introducir ruido aleatorio con
Math.random()para un efecto más caótico y expresivo. - Crear ondas reactivas al movimiento del mouse capturando las coordenadas con
mousemove. - Sincronizar varias capas de ondas con diferentes velocidades para conseguir profundidad de campo.
Aquí es donde Canvas realmente brilla frente a CSS: la creatividad es el único límite.
Preguntas frecuentes sobre la animación de ondas en JavaScript
- ¿Es mejor Canvas que CSS para este efecto?
- Sí, cuando necesitas ondas reales con control matemático sobre cada punto. CSS es suficiente para efectos simples como botones ripple, pero no puede igualar la flexibilidad del Canvas.
- ¿Puedo usar
Math.sin()oMath.cos()indistintamente?- Sí. La diferencia principal es el desfase inicial: el seno empieza en
0y el coseno en1. En la práctica, para efectos visuales, el resultado es casi idéntico.
- Sí. La diferencia principal es el desfase inicial: el seno empieza en
- ¿Consume muchos recursos esta animación?
- Bien optimizada, no.
requestAnimationFrame()pausa la animación cuando la pestaña no está visible, lo que reduce el consumo drásticamente.
- Bien optimizada, no.
- ¿Funciona esta wave animation en móviles?
- Sí, siempre que ajustes la resolución del canvas y el número de iteraciones de los bucles para adaptarlos a la capacidad de procesamiento del dispositivo.
Conclusión
Crear un efecto de onda con JavaScript va mucho más allá de un simple truco visual. Cuando combinas el <canvas> de HTML5, requestAnimationFrame() y trigonometría, puedes construir animaciones fluidas, orgánicas y totalmente personalizables sin depender de ninguna librería externa.
En mi experiencia, el salto de una onda simple a superponer varias funciones Math.cos() con desfases distintos fue lo que realmente transformó el resultado final. A partir de ahí, las posibilidades son enormes: color dinámico, reactividad al mouse, capas de profundidad… la canvas wave animation es una base increíblemente versátil.