Sesion 6 Animaciones - jesusgpa/2023-2024-CSAAI GitHub Wiki

Sesión 6: Animaciones

  • Tiempo: 2h (50 + 50 min)
  • Fecha: Martes 5 de Marzo de 2024
  • Objetivos de la sesión:
    • Manejo básico del canvas
    • Animaciones en el canvas

Contenido

Introducción

Animaciones

Las animaciones se realizan dibujando elementos en cada fotograma.

Si estos fotogramas se muestran con una frecuencia suficientemente alta, se obtendrá una animación suave.

Si la frecuencia no es tan alta, la animación irá "a saltos".

Seguro que alguna vez has hecho una animación en las hojas de un cuaderno.

Basta con pintar en cada una de ellas un dibujo, y que entre dos hojas consecutivas el personaje o elemento cambie ligeramente de posición.

Luego al pasar las hojas rápidamente se nos genera la ilusión de la animación.

En este vídeo puedes ver un ejemplo.

Click to see the youtube video

Esto mismo lo podemos hacer digitalmente.

Basta con hacer una serie de dibujos, generar las imágenes correspondientes y visualizarlas una detrás de otra.

Por ejemplo, podemos tener la siguiente secuencia de imágenes:

A partir de ellas podemos tener un gif animado, o las podemos reproducir manualmente.

Fases de la animación

El proceso para hacer animaciones, y que se aprecie cómo los objetos se mueven suavemente por la pantalla consiste en estos pasos:

1 Actualizar las posiciones de los objetos

Este paso implica calcular las nuevas posiciones de todos los objetos en la escena en función del tiempo transcurrido desde la última actualización.

Esto puede implicar el uso de fórmulas de movimiento, como la cinemática básica para el desplazamiento lineal, o algoritmos más complejos para movimientos más elaborados como la interpolación.

2 Borrar la pantalla

Antes de dibujar los objetos en sus nuevas posiciones, es importante limpiar la pantalla para evitar la superposición de imágenes anteriores.

Esto implica borrar todo el contenido previo de la pantalla.

3 Pintar en la pantalla todos los objetos visibles

Una vez que la pantalla está limpia, se dibujan todos los objetos en sus nuevas posiciones calculadas.

Esto puede implicar el uso de bibliotecas gráficas o de renderización que proporcionan métodos para dibujar formas geométricas, imágenes, etc.

4 Repetir

Después de dibujar todos los objetos en sus nuevas posiciones, el ciclo se repite, comenzando nuevamente con la actualización de las posiciones de los objetos.

Este ciclo se ejecuta continuamente para lograr la animación suave.

Este proceso se debe realizar al menos 60 veces por segundo (frecuencia de 60Hz).

Es decir, que se dispone de unos 17ms para realizar los cálculos y pintar el nuevo frame.

Si nuestra escena a animar requiere de muchos cálculos (hay muchos elementos que se mueven), puede pasar que no haya tiempo suficiente en 17ms para recalcular todo y pintarlo.

Si eso ocurre la frecuencia de la animación (frames/seg) será menor de 60 y el movimiento no se verá tan fluido.

Una definición de frecuencia (Hz)

La frecuencia (Hz), es una medida de la cantidad de repeticiones de un evento periódico por unidad de tiempo.

Un Hercio (Hz) equivale a un ciclo por segundo, lo que significa que una frecuencia de 1 Hz indica que un evento se repite una vez por segundo.

Por ejemplo, si una onda se repite 100 veces en un segundo, su frecuencia sería de 100 Hz.

Ejemplo: Cálculo de los frames por segundo

Un juego programado en javascript tarda los siguientes tiempos en realizar una serie de acciones:

  • Cálculo de la física del juego: posiciones, velocidades y aceleraciones de los objetos: 10ms
  • Borrado de la pantalla: 4ms
  • Pintar cada frame: 6ms

¿Cuáles será la frecuencia de refresco? (fps)

Solución:

El tiempo total que se requiere para obtener cada frame es de: 10 + 4 + 6 = 20ms.

Por tanto, la frecuencia de refresco será de 1/20ms = 0.05Khz = 50Hz

Elemento canvas: Dibujando gráficos 2D

En HTML disponemos de un elementos para hacer dibujos en él: El canvas (Lienzo).

Dentro de un canvas podemos realizar el dibujo que queramos, incluir texto, figuras geométricas, rectas, curvas, imágenes...

Aprenderemos a usarlo mediante ejemplos

Ejemplo 1: Creando el canvas

Comenzaremos creando un canvas vacío.

Como siempre, lo definimos en el documento HTML, dándole un identificador (display en nuestro ejemplo).

En el documento HTML situamos el elemento y su identificador, pero NO establecemos su tamaño ni su estilo.

El canvas se define mediante la etiqueta <canvas>

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="canvas-01.js" defer></script>
    <link rel="stylesheet" href="canvas-01.css">
    <title>Canvas 01</title>
</head>
<body>
    <h2>Canvas</h2>
    <canvas id="canvas"></canvas>
</body>
</html>

En el fichero canvas-01.css definimos su estilo: lo bordeamos con una línea negra y establecemos el color de su fondo a azul, para distinguirlo.

También redondeamos sus esquinas.

/* Estilo del canvas
  Lo bordeamos con una línea negra y le damos fondo
  azul para distinguirlo
*/

canvas {
    background-color: lightblue;
    border-style: solid;
    border-width: 1px;
    border-color: black;
    border-radius: 5px;
  }

En el código javascript obtenemos el elemento canvas y fijamos sus dimensiones a 200x100

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

Esto es lo que nos aparece al abrirlo en el navegador:

Ejemplo 2: Dibujando el trazo de un rectángulo

Vamos a dibujar un rectángulo en el interior del canvas.

Utilizaremos el mismo HTML y CSS que en el ejemplo anterior, pero cambiamos el código javascript.

Para poder dibujar en el canvas hay que obtener un objeto HTML especial que permite hacer dibujos, que denominamos el contexto.

El contexto lo obtenemos mediante el método getContext("2d") del canvas:

const ctx = canvas.getContext("2d");

Ahora ya podemos dibujar usando los métodos del contexto.

Cada una de los objetos a dibujar los delimitamos por los métodos ctx.beginPath() y ctx.closePath().

Para definir un rectángulo invocamos al método rect(), pasándole como parámetros las coordenadas x,y de la esquina superior izquierda, su anchura y su altura.

En este ejemplo pintaremos el trazado de un rectángulo de dimensiones 100x50, y cuya esquina superior izquierda está situada en la coordenada (5,5).

El origen de coordenadas (0,0) está situado en la esquina superior izquierda del canvas.

ctx.rect(5,5, 100, 50);

Cada elemento tiene una parte interna: el relleno (fill) y un trazo (stroke).

Para mostrar el interior usamos el método ctx.fill() y para mostrar el trazo el método ctx.stroke().

El código javascript completo es el siguiente:

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 200;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Cada objeto a dibujar lo delimitaremos 
//-- por los métodos beginPath() y closePath()
ctx.beginPath();
  //-- Definir un rectángulo de dimensiones 100x50,
  //-- cuya esquina superior izquierda está en (5,5)
  ctx.rect(5,5, 100, 50);

  //-- Color de relleno del rectángulo
  ctx.fillStyle = 'blue';

  //-- Mostrar el relleno
  ctx.fill();

  //-- Mostrar el trazo del rectángulo
  ctx.stroke();
ctx.closePath();

Y al ejecutarlo en el navegador nos aparece el rectángulo dentro del canvas:

Ejemplo 3: Trazos y relleno

Es posible dibujar tanto el relleno como el trazo, y cambiar sus propiedades.

En este ejemplo modificamos el grosor del trazo para que sea mayor, utilizando la propiedad lineWidth.

//-- Cambiar el tamaño de la línea del trazo
  ctx.lineWidth = 4;

El programa completo es el siguiente.

Primero se dibuja el relleno, con el método fill() y luego el contorno, con el método stroke().

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");


ctx.beginPath();
    //-- Definir un rectángulo de dimensiones 100x50,
    //-- cuya esquina superior izquierda está en (5,5)
    ctx.rect(5,5, 100, 50);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Cambiar el tamaño de la línea del trazo
    ctx.lineWidth = 4;

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
    
ctx.closePath()

El resultado es este:

Ejemplo 4: Tirando líneas

Para dibujar líneas en el canvas se usan los métodos moveTo() y lineTo().

Con el primero se define el punto inicial de la línea.

Y con el segundo se pinta la línea hasta el punto destino.

El grosor de los trazos de las líneas se define con la propiedad lineWidth y el color con strokeStyle.

En este ejemplo se tiran 3 líneas.

La primera es una horizontal.

La siguiente es una horizontal seguida de una vertical, unidas.

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 200;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");


ctx.beginPath();
    //-- Línea horizontal
    ctx.moveTo(10, 20);
    ctx.lineTo(100, 20);

    //-- Línea horizontal y vertical, unidas
    ctx.moveTo(10, 80);
    ctx.lineTo(150,80);
    ctx.lineTo(150,20);

    ctx.strokeStyle = 'blue';
    //-- Cambiar el tamaño de la linea del trazo
    ctx.lineWidth = 4;

    //-- Dibujar el trazo
    ctx.stroke()
    
ctx.closePath()

Este es el resultado:

Ejemplo 5: Dibujando círculos

Para dibujar círculos, circunferencias y arcos usamos el método arc().

Primero hay que invocar el método beginPath(), y luego se dibujan las curvas.

En este ejemplo se dibuja un círculo con relleno amarillo y trazo azul.

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 200;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");


ctx.beginPath();
    //-- Dibujar un circulo: coordenadas x,y del centro
    //-- Radio, Angulo inicial y angulo final
    ctx.arc(100, 50, 10, 0, 2 * Math.PI);
    ctx.strokeStyle = 'blue';
    ctx.lineWidth = 3;
    ctx.fillStyle = 'yellow';

    //-- Dibujar el trazo
    ctx.stroke()

    //-- Dibujar el relleno
    ctx.fill()
    
ctx.closePath()

El resultado es:

Ejemplo 6: Poniendo textos

En el canvas podemos colocar dos tipos de texto, igual que el resto de objetos: Texto sólido (que está rellenado) y texto definido sólo por su trazo exterior (sin rellenado).

En este ejemplo se escriben en el canvas ambos texto. Usamos los métodos fillText() y strokeText().

Con la propiedad font establecemos el tamaño y el tipo de letra.

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 200;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Texto solido
ctx.font = "25px Arial";
ctx.fillStyle = 'yellow'
ctx.fillText("Texto sólido", 10, 30);

//-- Texto trazo
ctx.strokeStyle = 'blue';
ctx.font = "35px Arial";
ctx.strokeText("Texto trazo", 5, 80);

Este es el resultado:

Ejemplo 7: Poniendo imágenes

Dentro del canvas también podemos meter imágenes.

Las imágenes las colocamos dentro del fichero HTML, pero cambiamos sus propiedades de estilo modificadas para que estén deshabilitadas y NO se muestren.

Desde el programa javascript accedemos a esas imágenes y las insertamos en el canvas.

En este ejemplo insertamos una única imágen.

El fichero HTML es el siguiente:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="canvas-07.js" defer></script>
    <link rel="stylesheet" href="canvas-07.css">
    <title>Canvas 07</title>
</head>
<body>
    <h2>Canvas</h2>
    <canvas id="canvas"></canvas>
    <img src="logo-urjc.png" alt="No encontrada" id="logo-urjc">
    <p>Texto de prueba</p>
</body>
</html>

En el fichero de estilo hemos cambiado la propiedad display de la imagen a none, para que no aparezca:

/* Estilo del canvas
  Lo bordeamos con una línea negra y le damos fondo
  azul para distinguirlo
*/

canvas {
    background-color: lightblue;
    border-style: solid;
    border-width: 1px;
    border-color: black;
    border-radius: 5px;
  }

/* Imagen deshabilitada
   Se usa para leerla desde js y
   meterla en el canvas */
   #logo-urjc {
    display: none;
  }

Desde el fichero javascript accedemos al elemento imagen y usando el método drawImage() lo insertamos en el canvas, haciendo que la esquina superior izquierda de la imagen esté en la coordenada (15, 18)

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 500;
canvas.height = 250;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Leer la imagen del documento html
//-- Esta deshabilitada
var logo = document.getElementById("logo-urjc");

logo.onload = ()=> {
  //-- Insertar la imagen en el canvas, una vez que
  //-- ya esté cargada!
  ctx.drawImage(logo, 15,18);
};

Este es el resultado en el navegador:

Lista de los métodos más utilizados del contexto 2D del canvas en JavaScript

  • fillRect(x, y, width, height): Dibuja un rectángulo sólido en el canvas.
  • strokeRect(x, y, width, height): Dibuja el contorno de un rectángulo en el canvas.
  • clearRect(x, y, width, height): Borra el contenido de un área rectangular del canvas.
  • beginPath(): Inicia un nuevo camino de dibujo.
  • closePath(): Cierra el camino de dibujo actual.
  • moveTo(x, y): Mueve el " lápiz" a las coordenadas especificadas sin dibujar una línea.
  • lineTo(x, y): Dibuja una línea desde la posición actual hasta las coordenadas especificadas.
  • stroke(): Dibuja el contorno del camino actual.
  • fill(): Rellena el área del camino actual con el color especificado.
  • arc(x, y, radio, inicioAngulo, finAngulo, sentidoHorario): Dibuja un arco de circunferencia.
  • arcTo(x1, y1, x2, y2, radio): Dibuja un arco tangente a la línea definida por los puntos (x1, y1) y (x2, y2).
  • quadraticCurveTo(cpx, cpy, x, y): Dibuja una curva cuadrática Bezier desde el punto actual hasta el punto final, utilizando el punto de control especificado.
  • bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): Dibuja una curva de Bezier cúbica desde el punto actual hasta el punto final, utilizando los dos puntos de control especificados.
  • drawImage(img, x, y): Dibuja una imagen, un elemento de video o un elemento canvas en el canvas.
  • fillText(text, x, y): Dibuja texto relleno en el canvas.
  • strokeText(text, x, y): Dibuja texto contorneado en el canvas.

Hay muchos más métodos y propiedades disponibles que puedes explorar:

Canvas Rendering Context 2D

Wooclap Time!

Programando animaciones en Javascript

Como ya sabemos pintar en el canvas, podemos empezar a hacer animaciones, y mover objetos por el canvas.

Bucle principal: requestAnimationFrame()

En el bucle principal de la animación se tienen que realizar las tareas comentadas en el apartado inicial, que recordamos aquí:

  1. Actualizar las posiciones de los elementos: Cálculos relativos a la física del movimiento
  2. Borrar el canvas
  3. Pintar los elementos visibles
  4. Repetir

En el objeto window que nos proporciona la interfaz del navegador existe un método llamado requestAnimationFrame() al que se le pasa como parámetro la función que hace de bucle principal, que típicamente la denominamos la función de update (actualización)

Este método se encarga de garantizar que la actualización se hace a una frecuencia de 60Hz (siempre que sea posible)

El esqueleto de nuestros programas de animación será siempre así:

//-- Declaración de variables y objetos

//-- Obtención del canvas y de los elementos HTML a usar

//-- Función principal de actualización
function update() 
{
  //-- Implementación del algoritmo de animación:

  //-- 1) Actualizar posición de los elementos

  //-- 2) Borrar el canvas

  //-- 3) Pintar los elementos en el canvas

  //-- 4) Repetir
  requestAnimationFrame(update);
}

//-- Otras funciones....

//-- ¡Que comience la fiesta! Hay que llamar a update la primera vez
update();

Actualización de elementos: Física

Para tener elementos móviles en nuestra animación, y que parezcan reales, utilizamos las ecuaciones de la cinemática de partículas.

Estas ecuaciones permiten calcular y predecir la posición, la velocidad y la aceleración de los objetos en movimiento en función del tiempo, lo que ayuda a simular el movimiento de manera precisa y convincente.

Al aplicar las ecuaciones de la cinemática de partículas en una animación, puedes lograr que los elementos móviles se comporten de manera coherente y realista.

Por ejemplo, puedes utilizar estas ecuaciones para determinar la trayectoria de un objeto en movimiento, su velocidad en diferentes momentos de la animación y cómo acelera o desacelera a lo largo del tiempo.

Otros ejemplos pueden ser:

  1. Calcular la trayectoria de un objeto en movimiento, ya sea en línea recta o en un movimiento curvo.
  2. Controlar la velocidad de un objeto a medida que se mueve a lo largo de su trayectoria.
  3. Simular la aceleración o desaceleración de un objeto debido a fuerzas externas, como la gravedad o la fricción.
  4. Animar colisiones entre objetos y calcular las velocidades resultantes después de la colisión.
  5. Crear efectos de movimiento, como rebotes o movimientos oscilatorios, utilizando ecuaciones de movimiento más complejas.

En cada iteración del bucle principal estas ecuaciones se deben recalcular para obtener las variables de estado de nuestros elementos: posiciones, velocidades, etc...

Un movimiento sencillo de implementar es el rectilíneo uniforme.

Si nuestro elemento se está moviendo horizontalmente hay que actualizar su posición x sumándole la velocidad, que será una constante.

Si la velocidad está definida en la constante VELX, el cálculo a realizar es una simple suma:

//-- Fisica del movimiento rectilíneo uniforme horizontal
x = x + VELX;

El signo de la velocidad determina hacia dónde se desplaza el elemento: En el sentido positivo del eje de las Xs o en el negativo.

En el caso del movimiento rectilíneo uniforme bidimensional (y no sólo en línea recta) bastará con incrementar de manera independientes sus variables x,y con las velocidades en el eje vertical y horizontal: VELX, VELY:

//-- Física del movimiento rectilíneo uniforme bidimensional
x = x + VELX;
y = y + VELY;

Si lo que queremos implementar es un movimiento rectilíneo uniformemente acelerado, necesitamos una constante nueva: la aceleración en los ejes X e Y: ACCELX, ACCELY.

Ahora las velocidades NO son constantes, sino que van aumentando debido a la aceleración. Los cálculos a realizar son los siguientes:

//-- Física del movimiento uniformemente acelerado bidimensional

//-- Actualizar las posiciones según la velocidad actual
x = x + velx;
y = y + vely;

//-- Actualizar las velocidades según las aceleraciones
velx = velx + ACCELX;
vely = vely + ACCELY;

Mezclando ambos tipos de movimientos: el uniformemente acelerado y el rectilíneo uniforme obtenemos otros, como el tiro parabólico

Además de las ecuaciones que describen el tipo de movimiento, necesitamos condiciones especiales adicionales para detectar colisiones y realizar rebotes.

La física del rebote es también muy sencilla de programar: basta con comprobar la posición del elemento, y si ha llegado a la pared límite hay que cambiar el signo de su velocidad.

Así por ejemplo si hay una bola que se mueve horizontalmente hacia una pared situada en la derecha, con velocidad velx, al llegar a la posición de la pared deberemos cambiar el signo de la velocidad en el eje horizontal:

//-- Rebote en pared vertical: cambiar el signo de la velocidad x
velx = -velx;

Borrado del canvas

El borrado del canvas, para poder pintar el siguiente frame, se realiza con el método ctx.clearRect() al que le pasamos como argumentos la posición del origen y de la esquina inferior derecha.

Este método se usa para borrar un área del cavas, pero si le pasamos el área completa lo borrará todo.

La instrucción a ejecutar siempre será esta:

ctx.clearRect(0, 0, canvas.width, canvas.height);

Donde canvas es el elemento HTML del canvas.

Ejemplo 8: Animando un objeto en reposo

En este primer ejemplo mostraremos el esquema de animación, pero aplicado a un objeto que está en reposo, es decir, con velocidad 0, por lo que no se mueve.

Sin embargo el bucle principal de la animación sí está activo.

La posición del elemento la podemos cambiar manualmente, desde la consola del navegador.

En los ficheros html y css tenemos lo mismo que en los ejemplos anteriores:

  • Fichero Ej-08.html:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="anim-01.js" defer></script>
    <link rel="stylesheet" href="anim-01.css">
    <title>Ejemplo 8: Animación</title>
</head>
<body>
    <h2>Canvas</h2>
    <canvas id="canvas"></canvas>
</body>
</html>
  • Fichero anim-01.css
/* Estilo del canvas
  Lo bordeamos con una línea negra y le damos fondo
  azul para distinguirlo
*/

canvas {
    background-color: lightblue;
    border-style: solid;
    border-width: 1px;
    border-color: black;
    border-radius: 5px;
  }
  • Fichero anim-01.js
console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Posición del elemento a animar
let x = 0;
let y = 0;

//-- Función principal de animación
function update() 
{
  console.log("test");
  //-- Algoritmo de animación:
  //-- 1) Actualizar posiciones de los elementos
  //-- Por hacer

  //-- 2) Borrar el canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //-- 3) Dibujar los elementos visibles
  ctx.beginPath();
    ctx.rect(x, y, 20, 20);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
  ctx.closePath();

  //-- 4) Volver a ejecutar update cuando toque
  requestAnimationFrame(update);
}

//-- ¡Que empiece la función!
update();

Se definen las variables x,y para almacenar la posición del elemento.

En la función principal (update) se imprime un mensaje en la consola (test) para comprobar que está funcionando.

Como el elemento está en reposo, no se actualiza su posición. O visto de otra manera, al ser la velocidad 0 la actualización de sus coordenadas sería:

//-- El objeto está en reposo (velocidad = 0)
x = x + 0;
y = y + 0;

El elemento a animar está constantemente siendo redibujado, pero siempre en la misma posición, por eso no vemos movimiento:

Al ejecutarlo observamos en la consola que efectivamente el mensaje de test está siendo constantemente actualizado.

Si ahora desde la consola cambiamos las coordenadas del objeto, asignando valores a x, y, veremos cómo efectivamente cambia de posición, aunque sigue en reposo, sin moverse.

Ejemplo 9: Movimiento horizontal de un rectángulo

Para lograr que el rectángulo se mueva con movimiento rectilínea uniforme, horizontalmente, sólo hay que asignarle una velocidad en x, e implementar su física correspondiente dentro de la función update()

Los cambios son los siguientes. Primero definimos la variable de la velocidad: velx

//-- Velocidad horizontal del objeto
let velx = 1;

Y ahora implementamos su física:

 //-- (física del movimiento rectilíneo uniforme)
  x = x + velx;

Este es el código completo:

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Coordenadas del objeto
let x = 0;
let y = 10;

//-- Velocidad horizontal del objeto
let velx = 1;

//-- Función principal de animación
function update() 
{
  console.log("test");
  //-- Algoritmo de animación:
  //-- 1) Actualizar posición del  elemento
  //-- (física del movimiento rectilíneo uniforme)
  x = x + velx;

  //-- 2) Borrar el canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //-- 3) Dibujar los elementos visibles
  ctx.beginPath();
    ctx.rect(x, y, 20, 20);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
  ctx.closePath();

  //-- 4) Volver a ejecutar update cuando toque
  requestAnimationFrame(update);
}

//-- ¡Que empiece la función!
update();

Al recargar la página veremos cómo el rectángulo rojo se mueve hacia la derecha

Desde la consola podemos controlar su física.

Asignamos el valor x = 0 para volver al inicio, y cambiamos la velocidad a diferentes valores para comprobar que efectivamente se mueve más rápidamente: velx = 2, velx = 5,...

¡Ya tenemos nuestra primera animación! 😀️

Ejemplo 10: Rebote del rectángulo

Vamos a modificar la física para que cuando el rectángulo alcance la posición de la derecha del canvas, rebote y vuelva hacia atrás.

Para lograr este rebote hay que detectar primero cuándo se alcanza el borde derecho y luego cambiar de signo su velocidad

Añadimos esto en la física:

//-- Comprobar colisión con borde derecho
  //-- Si se alcanza la anchura del canvas, se cambia la velocidad
  //-- de signo (rebote)
  if (x >= canvas.width) {
    velx = -velx;
  }

También modificamos la velocidad inicial a 2, para que vaya más rápido.

El código final que nos queda es este:

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Coordenadas del objeto
let x = 0;
let y = 10;

//-- Velocidad horizontal del objeto
let velx = 2;

//-- Función principal de animación
function update() 
{
  console.log("test");
  //-- Algoritmo de animación:
  //-- 1) Actualizar posición del  elemento
  //-- (física del movimiento rectilíneo uniforme)

  //-- Comprobar colisión con borde derecho
  //-- Si se alcanza la anchura del canvas, se cambia la velocidad
  //-- de signo (rebote)
  if (x >= canvas.width) {
    velx = -velx;
  }

  //-- Actualizar la posición
  x = x + velx;

  //-- 2) Borrar el canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //-- 3) Dibujar los elementos visibles
  ctx.beginPath();
    ctx.rect(x, y, 20, 20);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
  ctx.closePath();

  //-- 4) Volver a ejecutar update cuando toque
  requestAnimationFrame(update);
}

//-- ¡Que empiece la función!
update();

Al recargar la página vemos su funcionamiento.

Efectivamente ahora el rectángulo rebota al llegar al extremo derecho del canvas:

Ejemplo 11: Rectángulo confinado horizontalmente

Para que nuestro rectángulo se quede confinado dentro del canvas, rebotando a izquierda y derecha indefinidamente, sólo hay que añadir la condición de comprobación de que el borde izquierdo se ha alcanzado.

Esto ocurre cuando su posición x sea menor que 0

 //-- Condición de rebote en extremos del canvas
  if (x < 0 || x >= (canvas.width - 20) ) {
    velx = -velx;
  }
  • Código completo:
console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Coordenadas del objeto
let x = 0;
let y = 10;

//-- Velocidad horizontal del objeto
let velx = 3;

//-- Función principal de animación
function update() 
{
  console.log("test");
  //-- Algoritmo de animación:
  //-- 1) Actualizar posición del  elemento
  //-- (física del movimiento rectilíneo uniforme)

   //-- Condición de rebote en extremos del canvas
   if (x < 0 || x >= (canvas.width - 20) ) {
    velx = -velx;
  }

  //-- Actualizar la posición
  x = x + velx;

  //-- 2) Borrar el canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //-- 3) Dibujar los elementos visibles
  ctx.beginPath();
    ctx.rect(x, y, 20, 20);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
  ctx.closePath();

  //-- 4) Volver a ejecutar update cuando toque
  requestAnimationFrame(update);
}

//-- ¡Que empiece la función!
update();

En esta animación vemos el funcionamiento.

Ahora nuestro rectángulo está confinado dentro de los límites del canvas...

Ejemplo 12: Movimiento rectilíneo uniforme bidimensional

Para que nuestro rectángulo se mueva en línea recta pero en cualquier dirección, y no sólo horizontalmente, hay que superponer el movimiento en el eje y.

Tenemos que definir ambas velocidades: la horizontal y la vertical.

Vamos a darle un valor bajo a la vertical:

//-- Velocidades del objeto
let velx = 3;
let vely = 0.2;

Y su física hay que añadir la actualización de la variable y:

//-- Actualizar la posición
  x = x + velx;
  y = y + vely;
  • Código completo:
console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Coordenadas del objeto
let x = 0;
let y = 10;

//-- Velocidades del objeto
let velx = 3;
let vely = 0.2;

//-- Función principal de animación
function update() 
{
  console.log("test");
  //-- Algoritmo de animacion:
  //-- 1) Actualizar posición del  elemento
  //-- (física del movimiento rectilineo uniforme)

   //-- Condición de rebote en extremos del canvas
   if (x < 0 || x >= (canvas.width - 20) ) {
    velx = -velx;
  }

  //-- Actualizar la posición
  x = x + velx;
  y = y + vely;

  //-- 2) Borrar el canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //-- 3) Dibujar los elementos visibles
  ctx.beginPath();
    ctx.rect(x, y, 20, 20);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
  ctx.closePath();

  //-- 4) Volver a ejecutar update cuando toque
  requestAnimationFrame(update);
}

//-- ¡Que empiece la función!
update();

En la animación vemos el resultado.

Nuestro objeto se mueve también verticalmente, pero con velocidad más lenta.

Ejemplo 13: Rectángulo rebotando dentro del canvas en 2D

Para terminar, haremos que nuestro rectángulo se quede confinado dentro del canvas, rebotando tanto en los límites verticales como horizontales del canvas.

Basta con añadir las condiciones para la coordenada y:

//-- Condición de rebote en extremos horizontales del canvas
  if (y <= 0 || y > 80) {
    vely = -vely;
  }

Además cambiamos los valores de las velocidades, para que se mueva más rápido por el eje y:

//-- Velocidades del objeto
let velx = 3;
let vely = 1;

El código completo queda así:

console.log("Ejecutando JS...");

const canvas = document.getElementById("canvas");

//-- Definir el tamaño del canvas
canvas.width = 300;
canvas.height = 100;

//-- Obtener el contexto del canvas
const ctx = canvas.getContext("2d");

//-- Coordenadas del objeto
let x = 0;
let y = 10;

//-- Velocidades del objeto
let velx = 3;
let vely = 1;

//-- Función principal de animación
function update() 
{
  console.log("test");
  //-- Algoritmo de animación:
  //-- 1) Actualizar posición del  elemento
  //-- (física del movimiento rectilíneo uniforme)

   //-- Condición de rebote en extremos verticales del canvas
   if (x < 0 || x >= (canvas.width - 20) ) {
    velx = -velx;
  }

  //-- Condición de rebote en extremos horizontales del canvas
  if (y <= 0 || y > 80) {
    vely = -vely;
  }

  //-- Actualizar la posición
  x = x + velx;
  y = y + vely;

  //-- 2) Borrar el canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  //-- 3) Dibujar los elementos visibles
  ctx.beginPath();
    ctx.rect(x, y, 20, 20);

    //-- Dibujar
    ctx.fillStyle = 'red';

    //-- Rellenar
    ctx.fill();

    //-- Dibujar el trazo
    ctx.stroke()
  ctx.closePath();

  //-- 4) Volver a ejecutar update cuando toque
  requestAnimationFrame(update);
}

//-- Empezar la animación
update();

Y este es el resultado:

Sistema Solar

Otro ejemplo de animación es el siguientes sistema solar.

Este es el código html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sistema Solar</title>

    <script src="sistema-solar.js" defer></script>
</head>
<body>
    <canvas id="canvas" width="300" height="300"></canvas>
</body>
</html>

Y este es el código javascript para la animación:

const sun = new Image();
const moon = new Image();
const earth = new Image();

function init() {
  sun.src = "canvas_sun.png";
  moon.src = "canvas_moon.png";
  earth.src = "canvas_earth.png";

  window.requestAnimationFrame(draw);
}

function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");

  ctx.globalCompositeOperation = "destination-over";
  ctx.clearRect(0, 0, 300, 300); // clear canvas

  ctx.fillStyle = "rgb(0 0 0 / 40%)";
  ctx.strokeStyle = "rgb(0 153 255 / 40%)";
  ctx.save();
  ctx.translate(150, 150);

  // Earth
  const time = new Date();
  ctx.rotate(
    ((2 * Math.PI) / 60) * time.getSeconds() +
      ((2 * Math.PI) / 60000) * time.getMilliseconds(),
  );
  ctx.translate(105, 0);
  ctx.fillRect(0, -12, 40, 24); // Shadow
  ctx.drawImage(earth, -12, -12);

  // Moon
  ctx.save();
  ctx.rotate(
    ((2 * Math.PI) / 6) * time.getSeconds() +
      ((2 * Math.PI) / 6000) * time.getMilliseconds(),
  );
  ctx.translate(0, 28.5);
  ctx.drawImage(moon, -3.5, -3.5);
  ctx.restore();

  ctx.restore();

  ctx.beginPath();
  ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
  ctx.stroke();

  ctx.drawImage(sun, 0, 0, 300, 300);

  window.requestAnimationFrame(draw);
}

init();

En los enlaces a continuación puedes encontrar las imágenes para completar el sistema solar (o una parte).

Sun Earth Moon

Panorámica de Madrid

En este otro ejemplo podemos ver como hacer una imagen panorámica de un lugar, en este caso Madrid.

Este es el html donde situaremos nuestro canvas.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Panoramica Madrid</title>

    <script src="panoramica-madrid.js" defer></script>
</head>
<body>
    <canvas id="canvas" width="800" height="400">Panorámica de Madrid</canvas>
</body>
</html>

Y este la animación javascript para hacer rotar la imagen.

const img = new Image();

// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.
//img.src = "capitan_meadows_yosemite_national_park.jpg";
img.src = "panoramica-madrid-01.jpg";
//img.src = "panoramica-madrid-02.jpg";
//img.src = "panoramica-madrid-03.jpg";
//img.src = "panoramica-madrid-04.jpg";
const canvasXSize = 800;
const canvasYSize = 400;
const speed = 30; // lower is faster
const scale = 1.05;
const y = -4.5; // vertical offset

// Main program
const dx = 0.75;
let imgW;
let imgH;
let x = 0;
let clearX;
let clearY;
let ctx;

img.onload = () => {
  imgW = img.width * scale;
  imgH = img.height * scale;

  if (imgW > canvasXSize) {
    // Image larger than canvas
    x = canvasXSize - imgW;
  }

  // Check if image dimension is larger than canvas
  clearX = Math.max(imgW, canvasXSize);
  clearY = Math.max(imgH, canvasYSize);

  // Get canvas context
  ctx = document.getElementById("canvas").getContext("2d");

  // Set refresh rate
  return setInterval(draw, speed);
};

function draw() {
  ctx.clearRect(0, 0, clearX, clearY); // clear the canvas

  // If image is <= canvas size
  if (imgW <= canvasXSize) {
    // Reset, start from beginning
    if (x > canvasXSize) {
      x = -imgW + x;
    }

    // Draw additional image1
    if (x > 0) {
      ctx.drawImage(img, -imgW + x, y, imgW, imgH);
    }

    // Draw additional image2
    if (x - imgW > 0) {
      ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
    }
  } else {
    // Image is > canvas size
    // Reset, start from beginning
    if (x > canvasXSize) {
      x = canvasXSize - imgW;
    }

    // Draw additional image
    if (x > canvasXSize - imgW) {
      ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
    }
  }

  // Draw image
  ctx.drawImage(img, x, y, imgW, imgH);

  // Amount to move
  x += dx;
}

Prueba con diferentes imágenes, yo he utilizado la primera para el ejemplo.

Panoramica Madrid 1 Panoramica Madrid 2 Panoramica Madrid 3 Panoramica Madrid 4 Yosemite

Cambiando las imágenes de manera aleatoria

Utilizando las imágenes de Madrid, podemos por ejemplo que se cargue una diferente cada vez, y además que la imagen cambie cuando se cumple determinada condición.

Esto nos da un efecto de marco de fotos digital bastante interesante.

Para conseguirlo hay que cambiar un poco la estructura de la función de animación y añadir algo de código.

Esta es una posible solución:

let img = new Image();

//-- Array que almacena el nombre de las imágenes
const image_sources = [];

src_1 = "capitan_meadows_yosemite_national_park.jpg";
src_2 = "panoramica-madrid-01.jpg";
src_3 = "panoramica-madrid-02.jpg";
src_4 = "panoramica-madrid-03.jpg";
src_5 = "panoramica-madrid-04.jpg";

//image_sources.push(src_1);
image_sources.push(src_2);
image_sources.push(src_3);
image_sources.push(src_4);
image_sources.push(src_5);

function get_random_img_source () {

    let max = image_sources.length - 1;
    let imgPick = Math.floor(Math.random() * max);

    return image_sources[imgPick];

}

// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.
img.src = get_random_img_source();

console.log(img.src);

const canvasXSize = 800;
const canvasYSize = 400;
const speed = 30; // lower is faster
const scale = 1.05;
const y = -4.5; // vertical offset

// Main program
const dx = 0.75;
let imgW;
let imgH;
let x = 0;
let clearX;
let clearY;
let ctx;

// Get canvas context
ctx = document.getElementById("canvas").getContext("2d");

function set_canvas_image() {
 
  imgW = img.width * scale;
  imgH = img.height * scale;

  if (imgW > canvasXSize) {
    // Image larger than canvas
    x = canvasXSize - imgW;
  }

  // Check if image dimension is larger than canvas
  clearX = Math.max(imgW, canvasXSize);
  clearY = Math.max(imgH, canvasYSize);

}

function draw() {
  ctx.clearRect(0, 0, clearX, clearY); // clear the canvas

  // If image is <= canvas size
  if (imgW <= canvasXSize) {
    // Reset, start from beginning
    if (x > canvasXSize) {
      x = -imgW + x;
    }

    // Draw additional image1
    if (x > 0) {
      ctx.drawImage(img, -imgW + x, y, imgW, imgH);
    }

    // Draw additional image2
    if (x - imgW > 0) {
      ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
    }
  } else {
    // Image is > canvas size
    // Reset, start from beginning
    if (x > canvasXSize) {
      x = canvasXSize - imgW;
    }

    // Draw additional image
    if (x > canvasXSize - imgW) {
      ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
    }
  }

  // Draw image
  ctx.drawImage(img, x, y, imgW, imgH);

  // Amount to move
  x += dx;

  // Encuentra una condición de cambio de imagen
  //console.log( 'x: ' + x + ' imgW: ' + imgW + ' Math.abs(x): ' + Math.abs(x) );
  //console.log('x: ' + x + ' dx: ' + dx)

  // Condición de cambio de imagen
  if ( Math.abs(x) <= dx ) {
    
    last_img = img.src;
    img.src = get_random_img_source();  

    while(last_img == img.src) {
      img.src = get_random_img_source();
    }

    set_canvas_image();
  }

}

set_canvas_image();

setInterval(draw, speed);

Física de movimiento

Como ves, hay muchas posibilidades para utilizar el canvas, esta es solo una pequeña muestra.

En el próximo laboratorio, veremos más ejemplos sobre la física del movimiento.

Autor

Jesús Parrado Alameda (jesusgpa)

Creditos

Licencia

Enlaces

⚠️ **GitHub.com Fallback** ⚠️