Configuración Inicial del Lienzo Gráfico
Para iniciar nuestro proyecto de animación de nieve, primero debemos configurar el entorno gráfico. Esto implica obtener una referencia al elemento <canvas> de nuestro documento HTML, inicializar su contexto de renderizado 2D y asegurar que sus dimensiones se ajusten a las de la ventana del navegador. También es útil definir un coler base para el fondo.
const lienzoPrincipal = document.getElementById('lienzoNieve') as HTMLCanvasElement;
// Usamos '!' para indicar que estamos seguros de que getContext no devolverá null
const contexto2D = lienzoPrincipal.getContext('2d')!;
const alturaVentana = lienzoPrincipal.height = window.innerHeight;
const anchoVentana = lienzoPrincipal.width = window.innerWidth;
contexto2D.fillStyle = "#53B7F6"; // Un tono azul claro para el fondo
El uso de la aserción de tipo as HTMLCanvasElement es crucial en TypeScript para garantizar la manipulación correcta del elemento Canvas. Si no se utiliza un color de fondo sólido, esta línea puede ser omitida.
Carga y Representación de la Imagen de Copo
Para crear el efecto visual de la nieve, utilizaremos una imagen que represente un copo. Es fundamental cargar esta imagen de forma asíncrona y verificar que esté lista antes de intentar dibujarla en el lienzo. Una función auxiliar nos permitirá dibujar estos copos en coordenadas específicas.
let imagenDeCopoCargada: boolean = false;
const imagenCopo = new Image();
imagenCopo.onload = (): void => {
imagenDeCopoCargada = true;
};
imagenCopo.src = 'snow.jpg'; // Asegúrate de que esta ruta sea correcta
/**
* Función para dibujar un copo de nieve en el lienzo.
* @param coordX La posición X donde se dibujará el copo.
* @param coordY La posición Y donde se dibujará el copo.
*/
const dibujarImagenCopo = (coordX: number, coordY: number): void => {
// Las dimensiones (42, 30) pueden ajustarse según el tamaño deseado del copo
contexto2D.drawImage(imagenCopo, coordX, coordY, 42, 30);
};
Es vital ajustar los valores de ancho y alto (42 y 30 en el ejemplo) en la función dibujarImagenCopo para que coincidan con las proporciones de tu imagen y el tamaño visual deseado.
Estructura de Datos para los Copos de Nieve
Para manejar múltiples copos de nieve en la pantalla, definimos una interfaz que especifica las propiedades de cada copo (posición y velocidad). Los almacenaremos en un arreglo y crearemos una función para generar nuevos copos de manera controlada.
interface AtributosCopo {
posicionX: number,
posicionY: number,
velocidadHorizontal: number,
velocidadVertical: number
}
let coleccionDeCopos: AtributosCopo[] = [];
const maxCoposPorImpulso: number = 8; // Define cuántos copos nuevos aparecen por segundo
/**
* Genera una cantidad aleatoria de nuevos copos de nieve
* y los añade a la colección.
*/
const agregarNuevosCopos = (): void => {
const numNuevosCopos: number = Math.floor(maxCoposPorImpulso * Math.random());
for (let i = 0; i < numNuevosCopos; i++) {
coleccionDeCopos.push({
posicionX: Math.random() * anchoVentana,
posicionY: 0, // Los copos empiezan en la parte superior del lienzo
velocidadHorizontal: (Math.random() * 5 - 2.5), // Velocidad horizontal entre -2.5 y 2.5
velocidadVertical: (Math.random() * 8) + 2 // Velocidad vertical descendente entre 2 y 10
});
}
};
Bucle Principal de Renderizado y Lógica de Movimiento
Esta sección contiene la lógica principal para actualizar y dibujar los copos de nieve en cada fotograma. Incluye funciones para limpiar el lienzo, verificar si un copo ha salido de los límites visibles y actualizar sus posiciones.
/**
* Limpia todo el contenido del lienzo para el siguiente fotograma.
*/
const limpiarLienzo = (): void => {
contexto2D.clearRect(0, 0, anchoVentana, alturaVentana);
};
/**
* Determina si un copo ha caído fuera de la parte inferior del lienzo.
* @param copo El objeto copo a verificar.
* @returns Verdadero si el copo está fuera de los límites, falso en caso contrario.
*/
const estaFueraDePantalla = (copo: AtributosCopo): boolean => {
return copo.posicionY > alturaVentana;
};
/**
* Actualiza las posiciones de todos los copos y los redibuja en el lienzo.
*/
const actualizarYDibujarEscena = (): void => {
if (!imagenDeCopoCargada) return; // No dibujar hasta que la imagen esté cargada
limpiarLienzo(); // Borra el contenido del fotograma anterior
// Vuelve a dibujar el fondo azul. Esto es importante después de limpiar el lienzo.
contexto2D.fillStyle = "#53B7F6";
contexto2D.fillRect(0, 0, anchoVentana, alturaVentana);
// Itera la colección de copos en orden inverso para poder eliminar elementos de forma segura
for (let i = coleccionDeCopos.length - 1; i >= 0; i--) {
let copoActual = coleccionDeCopos[i];
dibujarImagenCopo(copoActual.posicionX, copoActual.posicionY);
// Actualiza la posición del copo para el siguiente fotograma
copoActual.posicionX += copoActual.velocidadHorizontal;
copoActual.posicionY += copoActual.velocidadVertical;
// Elimina el copo si ha salido de la pantalla
if (estaFueraDePantalla(copoActual)) {
coleccionDeCopos.splice(i, 1);
}
}
};
El orden en actualizarYDibujarEscena es crucial: primero se limpia el lienzo, luego se dibuja el fondo y finalmente se dibujan los copos. Iterar el arreglo de copos en reversa es una práctica común y segura cuando se eliminan eleemntos durante la iteración.
Implementación del Bucle de Animación con requestAnimationFrame
Para lograr una animación fluida y optimizada, utilizamos window.requestAnimationFrame. Este método es superior a setInterval porque sincroniza la animación con la tasa de refresco del navegaodr, lo que reduce el consumo de CPU y batería, y previene problemas de renderizado cuando la pestaña no está activa.
let tiempoUltimoDibujo: number = 0;
let tiempoUltimaGeneracionDeCopos: number = 0;
/**
* Función principal del bucle de animación. Se ejecuta en cada fotograma.
* @param marcaDeTiempo El tiempo actual proporcionado por requestAnimationFrame.
*/
const iniciarBucleAnimacion = (marcaDeTiempo: number = 0): void => {
// Controla la frecuencia de actualización del dibujo (aproximadamente cada 50ms)
if (marcaDeTiempo - tiempoUltimoDibujo > 50) {
actualizarYDibujarEscena();
tiempoUltimoDibujo = marcaDeTiempo;
}
// Controla la frecuencia de generación de nuevos copos (cada 1000ms = 1 segundo)
if (marcaDeTiempo - tiempoUltimaGeneracionDeCopos > 1000) {
agregarNuevosCopos();
tiempoUltimaGeneracionDeCopos = marcaDeTiempo;
}
try {
window.requestAnimationFrame(iniciarBucleAnimacion);
} catch (error) {
console.error("Error: Su navegador no soporta requestAnimationFrame.", error);
alert('Lo sentimos, su navegador no es compatible con requestAnimationFrame. Por favor, actualice o use otro navegador.');
}
};
// Arrancar el bucle de animación
iniciarBucleAnimacion();
La ventaja de requestAnimationFrame es que el navegador detiene la ejecución cuando la ventana no está visible, evitando la acumulación de lógica de animación innecesaria y el consumo excesivo de recursos.