Efecto de onda con JavaScript y Canvas: wave animation con requestAnimationFrame

- Andrés Cruz - EN In english

Efecto de onda con JavaScript y Canvas: wave animation con requestAnimationFrame

Ver ejemplo en vivo

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:

Gráfica de la función coseno

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 valor n.
  • Math.sin(n) — aplica la función seno al valor n.

Las funciones trigonométricas Math.sin() y Math.cos() generan curvas periódicas que suben y bajan suavemente entre -1 y 1. 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:

Onda látigo sin el for externo

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 (variable i) crea múltiples líneas paralelas con desfases distintos.
  • El segundo for (variable j) 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.
  • Ajustes que marcan la diferencia visual
    • Pequeños cambios producen efectos enormes. Vale la pena experimentar con:
      • Incrementar la fase más lentamente (reducir el += 5 de t).
      • Variar ligeramente la amplitud multiplicando i por un decimal distinto.
      • Introducir offsets mínimos en los argumentos de Math.cos() para diversificar el patrón.
  • 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() o clearRect()).
  • 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 de setInterval().
  • 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 de i o t como 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() o Math.cos() indistintamente?
    • Sí. La diferencia principal es el desfase inicial: el seno empieza en 0 y el coseno en 1. En la práctica, para efectos visuales, el resultado es casi idéntico.
  • ¿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.
  • ¿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.

Ver ejemplo en vivo

Crea un efecto de onda animado con HTML5 Canvas y JavaScript. Tutorial que explica cómo usar Math.sin, Math.cos y requestAnimationFrame para construir una wave animation fluida, orgánica y sin librerías externas.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.