Bienvenidos a la tercera parte de nuestro tutorial sobre como crear un blob animado usando solo Canvas API
.
Si te perdiste las partes anteriores puedes encontrarlas a continuación:
- Parte I: Aprenderemos a utilizar el API de HTML Canvas para dibujar formas primitivas. link
- Parte II: Haremos un pequeño anillo 3D proyectado en 2 dimensiones. link
- Parte III: Haremos una Esfera completa y le añadiremos rotación en los tres ejes. Este
- Parte IV: Distorsionaremos la esfera para hacerla parecer un
blob
. (Próximamente)
- Parte V: Le agregaremos interacciones con el mouse. (Próximamente)
- Parte VI: Le agregaremos controles interactivos para ‘jugar’ con los parametros de nuestra esfera. (Próximamente)
Recapitulación:
Hasta ahora, hemos hecho una circunferencia. El buen observador puede que haya notado que nuestra última representación de la circunferencia tiene un ligero problema: Los círculos más cercanos a la cámara se ven igual de grandes que los más alejados, así no se comportan los objetos en la vida real, corrijamoslo rápidamente.
Corrigiendo nuestra perspectiva
1
2
3
4
5
6
7
8
9
10
11
|
//...
const point_in_2d = {
x: getProjection(distance, point.x, point.z, z_camera),
y: getProjection(distance, point.y, point.z, z_camera),
}
radius = radius + (point.z/20) // AGREGAMOS ESTO
ctx.fillStyle = color; // cambiamos el color de relleno a rojo
// ...
}
|
Al hacer esto, reducimos los circulos ligeramente más cercanos a la cámara.
Dibujando una esfera
Ahora ya podemos dibujar una esfera. Visto de cierta manera una esfera no es más que una serie de circunferencias de distinto radio, así que tenemos que dibujar varias circunferencias reduciendo poco a poco su circunferencia hasta llegar a los polos.
Primero, refactoricemos nuestro generador de circunferencia a una función draw_circumference()
1
2
3
4
5
6
7
8
9
|
function draw_circumference(points_in_circle, radius, fixed_y) {
const arc_distance = (2 * Math.PI) / points_in_circle;
for(var n = 0; n < points_in_circle; n++){
const x = Math.cos(arc_distance * n) * radius;
const y = fixed_y; // nuestro circulo será paralelo al plano Y.
const z = Math.sin(arc_distance * n) * radius;
points.push({x, y, z}); // creamos el punto y lo insertamos en el arreglo.
}
}
|
Ahora dibujaremos un cilindro. Para hacerlo, simplemente llamares a la función cambiando la posición y de la circunferencia:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function draw_circumference(points_in_circle, radius, fixed_y){
const arc_distance = (2 * Math.PI) / points_in_circle;
for(var n = 0; n < points_in_circle; n++){
const x = Math.cos(arc_distance * n) * radius;
const y = fixed_y; // nuestro circulo será paralelo al plano Y.
const z = Math.sin(arc_distance * n) * radius;
points.push({x, y, z}); // creamos el punto y lo insertamos en el arreglo.
}
}
const radius = 20; // radio del circulo
const points_in_circle = 100;
const arc_distance = (2 * Math.PI) / points_in_circle;
// dibujando un cilindro.
draw_circumference(points_in_circle, radius, 0);
draw_circumference(points_in_circle, radius, 1);
draw_circumference(points_in_circle, radius, 2);
draw_circumference(points_in_circle, radius, 3);
draw_circumference(points_in_circle, radius, 4);
draw_circumference(points_in_circle, radius, 5);
|
Cuando empezamos a repetir código muy similar es un indicador que estamos haciendo algo mal (DRY). Refactorizémoslo para usar un bucle for
.
1
2
3
4
5
6
7
8
9
|
// dibujando un cilindro.
const radius = 20; // radio del circulo
const points_in_circle = 100;
const arc_distance = (2 * Math.PI) / points_in_circle;
const levels = 20;
for (var i = 0; i < levels; i++){
draw_circumference(points_in_circle, radius, i);
}
|
Ahora solo tenemos que ajustar la variable levels para cambiar la cantidad de circulos:
Si queremos hacer un circulo podriamos intentar modificar el radio en cada nivel hasta quedarnos con un radio de 1.
1
2
3
4
5
6
7
8
9
10
|
// dibujando un cilindro.
const radius = 20; // radio del circulo
const points_in_circle = 100;
const arc_distance = (2 * Math.PI) / points_in_circle;
const levels = 20;
for (var i = 0; i < levels; i++){
const radius_mod = radius - i;
draw_circumference(points_in_circle, radius_mod, i);
}
|
Pero como podemos ver, eso nos daría un cono, dado que el radio de una esfera no se reduce linealmente. 😐
Ahora, tenemos que hacer un poquito de matemáticas para calcular cuantos niveles necesitamos para construir nuestro circulo, y por cuanto reducir el radio de nuestra esfera. Como siempre, puedes saltarte esta parte.
La forma más sencilla de verlo, es dibujar una circunferencia rotada en 90 grados y veremos que el radio de nuestra circunferencia rotada vendría a ser el coseno(angulo)
actual. Mientras que el seno(angulo)
nos dará la distancia del centro.
1
2
|
let radius = Math.cos(beta) * this.radius
let y = Math.sin(beta) * this.radius;
|
Reemplazemos estas formulas en nuestro código:
1
2
3
4
5
6
7
8
9
10
|
// dibujando una esfera.
const radius = 20; // radio del circulo
const points_in_circle = 100;
const arc_distance = (2 * Math.PI) / points_in_circle;
for (var i = 0; i < points_in_circle; i++){
const radius_mod = Math.cos(arc_distance * i) * radius;
let y = Math.sin(arc_distance * i) * radius;
draw_circumference(points_in_circle, radius_mod, y);
}
|
Wow. Ya tenemos nuestra esfera: 🔴.
Por si acaso, este es el código completo que tenemos hasta ahora:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
<html lang="en">
<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">
<title>Sphere</title>
<style>
/* ESTILOS */
</style>
</head>
<body>
<h1> Sphere. </h1>
<h2> <span id="x"> X </span> WeMake. </h2>
<canvas id="canvas"></canvas>
<script>
// 0. Setup
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const w = canvas.width = 720 // Ancho: esto puede variarse al tamaño que quieran
const h = canvas.height = 720 // Alto: esto puede variarse al tamaño que quieran
function getProjection(distance, xy, z, z_camera) {
return ((distance * xy) / (z_camera- z))
}
const points = [] // lista o arreglo de puntos
function draw_circumference(points_in_circle, radius, fixed_y){
const arc_distance = (2 * Math.PI) / points_in_circle;
for(var n = 0; n < points_in_circle; n++){
const x = Math.cos(arc_distance * n) * radius;
const y = fixed_y; // nuestro circulo será paralelo al plano Y.
const z = Math.sin(arc_distance * n) * radius;
points.push({x, y, z}); // creamos el punto y lo insertamos en el arreglo.
}
}
// dibujando un cilindro.
const radius = 20; // radio del circulo
const points_in_circle = 100;
const arc_distance = (2 * Math.PI) / points_in_circle;
for (var i = 0; i < points_in_circle; i++){
const radius_mod = Math.cos(arc_distance * i) * radius;
let y = Math.sin(arc_distance * i) * radius;
draw_circumference(points_in_circle, radius_mod, y);
}
function rotateX(point, amount){
let y = point.y
point.y = (y * Math.cos(amount)) + (point.z * Math.sin(amount) * -1.0)
point.z = (y * Math.sin(amount)) + (point.z * Math.cos(amount))
return point;
}
function radianToDegrees(angleInRadians){
return angleInRadians * Math.PI / 180;
}
function draw_point(point, color = 'red', radius = 5){
const distance = 1000; // distancia de la camara al plano
const z_camera = 100; // distancia de la camara en Z
point = rotateX(point, radianToDegrees(-10)) // Rotamos ligeramente en 10 deg
const point_in_2d = {
x: getProjection(distance, point.x, point.z, z_camera),
y: getProjection(distance, point.y, point.z, z_camera),
}
radius = radius + (point.z/20) // AGREGAMOS ESTO
ctx.fillStyle = color; // cambiamos el color de relleno a rojo
ctx.beginPath() //
ctx.arc(point_in_2d.x + w/2, // coordenada en X
point_in_2d.y + h/2, // coordenada en Y
radius, // radio del arco
0, // angulo de Inicio
2 * Math.PI, // angulo de fin
true) // Antihorario/Horario
ctx.fill() // Pintamos las figuras cerradas que tenemos pendientes
}
for(const point of points){
draw_point(point);
}
</script>
</body>
</html>
|
Por último, en esta parte animaremos nuestra esfera para que rote ligeramente en los tres ejes.
Primero, definiremos las funciones de rotación en los otros ejes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
function rotateX(point, amount){
let y = point.y
point.y = (y * Math.cos(amount)) + (point.z * Math.sin(amount) * -1.0)
point.z = (y * Math.sin(amount)) + (point.z * Math.cos(amount))
return point;
}
function rotateY(point, amount){
let x = point.x
point.x = (x * Math.cos(amount)) + (point.z * Math.sin(amount) * -1.0)
point.z = (x * Math.sin(amount)) + (point.z * Math.cos(amount))
return point;
}
function rotateZ(point, amount){
let x = point.x
point.x = (x * Math.cos(amount)) + (point.y * Math.sin(amount) * -1.0)
point.y = (x * Math.sin(amount)) + (point.y * Math.cos(amount))
return point;
}
|
Ahora, quitaremos la rotación en nuestra función draw_point
1
2
3
4
5
6
7
8
9
10
11
12
|
function draw_point(point, color = 'red', radius = 5){
const distance = 1000; // distancia de la camara al plano
const z_camera = 100; // distancia de la camara en Z
// Comentamos esta linea, ya no vamos a rotar el circulo de manera fija
// point = rotateX(point, radianToDegrees(-10)) // Rotamos ligeramente en 10 deg
const point_in_2d = {
x: getProjection(distance, point.x, point.z, z_camera),
y: getProjection(distance, point.y, point.z, z_camera),
}
// ...
|
Ahora en nuestro bucle de dibujo haremos la rotación:
1
2
3
4
5
6
|
for(const point of points){
let mod_point = rotateX(point, radianToDegrees(-10)) // Rotamos ligeramente en 10 deg
mod_point = rotateY(mod_point, radianToDegrees(-10)) // Rotamos ligeramente en 10 deg
mod_point = rotateZ(mod_point, radianToDegrees(-10)) // Rotamos ligeramente en 10 deg
draw_point(mod_point);
}
|
Por último, rotaremos la esfera ligeramente digamos 60 grados por segundo, es decir para que de una vuelta completa en 6 segundos:
1
2
3
4
5
6
7
8
9
10
|
const rotation_degree = 1;
window.setInterval(()=>{
for(const point of points){
let mod_point = rotateX(point, radianToDegrees(rotation_degree))
mod_point = rotateY(mod_point, radianToDegrees(rotation_degree))
mod_point = rotateZ(mod_point, radianToDegrees(rotation_degree))
draw_point(mod_point);
}
}, 1000/60)
|
Veamos como se ve:
¿Qué sucedió? ¿Por qué se convirtió nuestra esfera en un círculo?
El problema es que estamos pintando una esfera rotada encima de una esfera rotada, sin limpiar
nuestro canvas, al hacer eso los puntos se sobreponen y se pinta un círculo completo.
Para corregirlo, borraremos el canvas
en cada iteración
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const rotation_degree = 1;
window.setInterval(()=>{
// guardamos el estado y limpiamos el canvas
ctx.save()
ctx.clearRect(0, 0, w, h)
for(const point of points){
let mod_point = rotateX(point, radianToDegrees(rotation_degree))
mod_point = rotateY(mod_point, radianToDegrees(rotation_degree))
mod_point = rotateZ(mod_point, radianToDegrees(rotation_degree))
draw_point(mod_point);
}
// recuperamos el estado
ctx.restore()
}, 1000/60)
|
Quizá podemos bajar el radio de nuestro circulo para apreciar mejor su rotación:
1
2
3
|
//...
function draw_point(point, color = 'red', radius = 2){
//...
|
Perfecto, eso es todo para este post, en la siguiente parte veremos como distorsionarla para que parezca el blob y le agregaremos un poco más de estilo para que se vea mejor en 3D.
Hasta la próxima parte.
Código completo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
<html lang="en">
<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">
<title>Sphere</title>
<style>
/* ESTILOS */
</style>
</head>
<body>
<h1> Sphere. </h1>
<h2> <span id="x"> X </span> WeMake. </h2>
<canvas id="canvas"></canvas>
<script>
// 0. Setup
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const w = canvas.width = 720 // Ancho: esto puede variarse al tamaño que quieran
const h = canvas.height = 720 // Alto: esto puede variarse al tamaño que quieran
function getProjection(distance, xy, z, z_camera) {
return ((distance * xy) / (z_camera- z))
}
const points = [] // lista o arreglo de puntos
function draw_circumference(points_in_circle, radius, fixed_y){
const arc_distance = (2 * Math.PI) / points_in_circle;
for(var n = 0; n < points_in_circle; n++){
const x = Math.cos(arc_distance * n) * radius;
const y = fixed_y; // nuestro circulo será paralelo al plano Y.
const z = Math.sin(arc_distance * n) * radius;
points.push({x, y, z}); // creamos el punto y lo insertamos en el arreglo.
}
}
// dibujando un cilindro.
const radius = 20; // radio del circulo
const points_in_circle = 100;
const arc_distance = (2 * Math.PI) / points_in_circle;
for (var i = 0; i < points_in_circle; i++){
const radius_mod = Math.cos(arc_distance * i) * radius;
let y = Math.sin(arc_distance * i) * radius;
draw_circumference(points_in_circle, radius_mod, y);
}
function rotateX(point, amount){
let y = point.y
point.y = (y * Math.cos(amount)) + (point.z * Math.sin(amount) * -1.0)
point.z = (y * Math.sin(amount)) + (point.z * Math.cos(amount))
return point;
}
function rotateY(point, amount){
let x = point.x
point.x = (x * Math.cos(amount)) + (point.z * Math.sin(amount) * -1.0)
point.z = (x * Math.sin(amount)) + (point.z * Math.cos(amount))
return point;
}
function rotateZ(point, amount){
let x = point.x
point.x = (x * Math.cos(amount)) + (point.y * Math.sin(amount) * -1.0)
point.y = (x * Math.sin(amount)) + (point.y * Math.cos(amount))
return point;
}
function radianToDegrees(angleInRadians){
return angleInRadians * Math.PI / 180;
}
function draw_point(point, color = 'red', radius = 2){
const distance = 1000; // distancia de la camara al plano
const z_camera = 100; // distancia de la camara en Z
// point = rotateX(point, radianToDegrees(-10)) // Rotamos ligeramente en 10 deg
const point_in_2d = {
x: getProjection(distance, point.x, point.z, z_camera),
y: getProjection(distance, point.y, point.z, z_camera),
}
radius = radius + (point.z/20) // AGREGAMOS ESTO
ctx.fillStyle = color; // cambiamos el color de relleno a rojo
ctx.beginPath() //
ctx.arc(point_in_2d.x + w/2, // coordenada en X
point_in_2d.y + h/2, // coordenada en Y
radius, // radio del arco
0, // angulo de Inicio
2 * Math.PI, // angulo de fin
true) // Antihorario/Horario
ctx.fill() // Pintamos las figuras cerradas que tenemos pendientes
}
const rotation_degree = 1;
window.setInterval(()=>{
// guardamos el estado y limpiamos el canvas
ctx.save()
ctx.clearRect(0, 0, w, h)
for(const point of points){
let mod_point = rotateX(point, radianToDegrees(rotation_degree))
mod_point = rotateY(mod_point, radianToDegrees(rotation_degree))
mod_point = rotateZ(mod_point, radianToDegrees(rotation_degree))
draw_point(mod_point);
}
// recuperamos el estado
ctx.restore()
}, 1000/60)
</script>
</body>
</html>
|