domingo, 3 de diciembre de 2023

Animaciones con HTML Canvas y Javascript

Con el elemento canvas de HTML podemos realizar dibujos y hacer que se muevan modificando su posición con el transcurso del tiempo. Como ejemplo para este artículo he creado una animación consistente en bolas dentro de un cuadrado con cuatro rectángulos. Las bolas se mueven y chocan entre si, contra los rectángulos y contra los bordes del cuadrado.

Tu navegador no admite el elemento <canvas>.

En primer lugar se crea un elemento canvas en el cuerpo del documento HTML. Es necesario indicar el ancho y el alto. Se puede incluir un mensaje para navegadores que no admitan este elemento. A continuación es necesario un script que dibuje en el canvas. El script accede al elemento canvas una vez que se produce el evento DOMContentLoaded, que indica que se ha cargado el documento HTML. Del elemento canvas se obtiene una clase CanvasRenderingContext2D que nos va a permitir dibujar en el canvas.

<canvas id="balls" width="550" height="550">
    Tu navegador no admite el elemento <canvas>.   
</canvas>
<script>
    var context;
    var balls = new Array();
    var ballsNumber = 15;
				
    document.addEventListener("DOMContentLoaded", function(){
        var canvas = document.getElementById("balls");
        context = canvas.getContext("2d");

        for (x = 0; x < ballsNumber; x++) {
            balls.push(new Ball());
        }

        animation();
    });

Seguidamente el script inicializa de forma aleatoria la información de 15 bolas. Cada bola está representada por una clase Ball con 5 parámetros:

  • Posición en el eje horizontal x.
  • Posición en el eje vertical y.
  • Movimiento en el eje x. -1 = izquierda, 1 = derecha.
  • Movimiento en el eje y. -1 = arriba, 1 = abajo.
  • Color.
class Ball {
    constructor() {
        this.x = Math.floor((Math.random() * 200) + 50);
        this.y = Math.floor((Math.random() * 200) + 150);
        this.xm = Math.floor(Math.random() * 2) == 0 ? -1 : 1;
        this.ym = Math.floor(Math.random() * 2) == 0 ? -1 : 1;
        this.color = "#" + Math.floor(Math.random() *
            16777216).toString(16);
    }
}

Una vez inicializadas las bolas se ejecuta la función animation(). Esta función lo primero que hace es ejecutar otras cuatro funciones:

  • drawScene(): Dibujado de la escena en la que se van mover las bolas compuesta por un cuadrado que forma los bordes de la escena y cuatro rectángulos.
  • drawBalls(): Dibujado de las bolas según los parámetros de los objetos de la clase Ball.
  • checkCollisions(): Chequeo de colisiones de las bolas con el cuadrado, los rectángulos y otras bolas.
  • moveBalls(): Aumento o disminución de las coordenadas de las bolas de acuerdo a su dirección de movimiento.

Por último ejecuta la función requestAnimationFrame() para indicar al navegador que se quiere redibujar la escena con las nuevas coordenadas de las bolas. A esta función se le pasa como parámetro la función animation() creando un bucle infinito en el que continuamente se vuelve a dibujar la escena y las bolas, se chequean las colisiones y se modifican las coordenadas de las bolas.

function animation() {		

    drawScene();
						
    drawBalls();
		
    checkCollisions();
			
    moveBalls();

    requestAnimationFrame(animation);
}

En las funciones drawScene() y drawBalls() se utiliza la clase CanvasRenderingContext2D para dejar la escena en blanco y a continuación dibujar el cuadrado, los rectángulos y las bolas. El cuadrado se dibuja mediante cuatro líneas y las bolas con arcos de 2π radianes / 360 grados.

function drawScene() {
	
    //Clear Scene
    context.clearRect(0, 0, 550, 550);
		
    //Draw Square
    context.beginPath(); 
    context.moveTo(0,0);
    context.lineWidth = 10;
    context.lineTo(550,0);
    context.lineTo(550,550);
    context.lineTo(0,550);
    context.lineTo(0,0);
    context.fillStyle = "white";
    context.fill();
    context.strokeStyle = "green";
    context.stroke();

    //Draw Rectangles
    context.fillStyle = "green";

    context.fillRect(50,100,100,25);
    context.fillRect(300,150,100,25);

    context.fillRect(100,400,100,25);
    context.fillRect(350,355,100,25);
}

function drawBalls() {
	
    //Draw Balls
    for (let ball of balls) {
        context.beginPath();
        context.arc(ball.x, ball.y, 10, 0, 2 * Math.PI, false);
        context.fillStyle = ball.color;
        context.fill();
        context.lineWidth = 2;
        context.strokeStyle = "black";
        context.stroke();
    }
}

En la función checkCollisions() se comprueba si las bolas han colisionado con el cuadrado, los rectángulos u otras bolas. Para ello se comprueba si el borde de las bolas está en contacto con algún objeto. Por simplificar solo se comprueban 8 puntos: izquierda, derecha, arriba, abajo, izquierda-arriba, derecha-arriba, izquierda-abajo y derecha-abajo. Mediante la función isPixelWhite() se comprueba si el punto contiguo a la bola está en blanco. Si no lo está es que hay un objeto y ha habido colisión. En caso de colisión se calcula la nueva dirección de la bola en los dos ejes. La velocidad se limita a un máximo de un punto por eje en cada ejecución del bucle para simplificar.

function checkCollisions() {
    for (let ball of balls) {
		
        //Left
        if (!isPixelWhite(ball.x-12, ball.y)) {
            ball.xm = 1;
        }

        //Right
        if (!isPixelWhite(ball.x+11, ball.y)) {
            ball.xm = -1;
        }

        //Top
        if (!isPixelWhite(ball.x, ball.y-12)) {
            ball.ym = 1;
        }

        //Bottom
        if (!isPixelWhite(ball.x, ball.y+11)) {
            ball.ym = -1;
        }

        //Left-Top
        if (!isPixelWhite(ball.x-9, ball.y-9)) {
            if (ball.xm == -1 && ball.ym == -1) {
                ball.xm = 1;
                ball.ym = 1;
            } else {
                ball.xm += 1;
                ball.ym += 1;
            }
        }

        //Right-Top
        if (!isPixelWhite(ball.x+8, ball.y-9)) {
            if (ball.xm == 1 && ball.ym == -1) {
                ball.xm = -1;
                ball.ym = 1;
            } else {
                ball.xm -= 1;
                ball.ym += 1;
            }
        }

        //Left-Bottom
        if (!isPixelWhite(ball.x-9, ball.y+8)) {
            if (ball.xm == -1 && ball.ym == 1) {
                ball.xm = 1
                ball.ym = -1;
            } else {
                ball.xm += 1;
                ball.ym -= 1;
            }
        }

        //Right-Bottom
        if (!isPixelWhite(ball.x+8, ball.y+8)) {
            if (ball.xm == 1 && ball.ym == 1) {
                ball.xm = -1;
                ball.ym = -1;
            } else {
                ball.xm -= 1;
                ball.ym -= 1;
            }
        }
            
        //Limit speed to 1 pixel/frame
        ball.xm = Math.min(1, Math.max(-1, ball.xm));
        ball.ym = Math.min(1, Math.max(-1, ball.ym));
           
    }
}

function isPixelWhite(x, y) {
    var data = context.getImageData(x, y, 1, 1).data;
    return data[0] == 255 && data[1] == 255 && data[2] == 255;
}

Por último la función moveBalls() modifica las coordenadas de las bolas sumándoles el movimiento negativo, cero o positivo que tengan. En cada repetición del bucle las bolas se mueven cero o un punto en cada uno de los ejes.

function moveBalls() {
	
    for (let ball of balls) {
				
        ball.x += ball.xm;
        ball.y += ball.ym;	
			
    }
}

Esto es lo básico para dibujar y crear animaciones con el elemento canvas. En la clase CanvasRenderingContext2D hay más funciones que podemos utilizar para hacer diferentes dibujos y también existen bibliotecas que nos pueden facilitar la creación de animaciones o juegos de todo tipo.

2 comentarios: