Lienzo desde cero: Transformaciones y degradados
() translation by (you can also view the original English article)
En este artículo, voy a guiarte por las transformaciones en el lienzo, así como por las sombras y los degradados. Las transformaciones son un conjunto de métodos muy valiosos que te permiten empezar a ser creativo con la forma de dibujar objetos en el lienzo. ¡Empecemos después del salto!
Configuración
Vas a utilizar la misma plantilla HTML de los artículos anteriores, así que abre tu editor favorito y pega el siguiente código:
1 |
<!DOCTYPE html>
|
2 |
|
3 |
<html>
|
4 |
<head>
|
5 |
<title>Canvas from scratch</title> |
6 |
<meta charset="utf-8"> |
7 |
|
8 |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script> |
9 |
|
10 |
<script>
|
11 |
$(document).ready(function() { |
12 |
var canvas = document.getElementById("myCanvas"); |
13 |
var ctx = canvas.getContext("2d"); |
14 |
});
|
15 |
</script>
|
16 |
</head>
|
17 |
|
18 |
<body>
|
19 |
<canvas id="myCanvas" width="500" height="500"> |
20 |
<!-- Insert fallback content here -->
|
21 |
</canvas>
|
22 |
</body>
|
23 |
</html>
|
Aquí no tenemos más que una página HTML básica con un elemento canvas
y algo de JavaScript que se ejecuta después de que el DOM se haya cargado. No es una locura.
Traducciones en acción
Traducir esencialmente desplaza todo el sistema de coordenadas.
Una de las transformaciones más simples en el lienzo es translate
. Esto te permite mover el punto de origen del contexto de renderizado 2d; la posición (0, 0) en el lienzo. Déjame mostrarte lo que esto significa.
Primero, coloca un cuadrado en el lienzo en la posición (0, 0):
1 |
ctx.fillRect(0, 0, 100, 100); |
Se dibujará en el borde superior izquierdo del lienzo. Aun así, no hay nada fuera de lo común.



Ahora, intenta trasladar el contexto de renderizado 2d y dibujar otro cuadrado en la misma posición:
1 |
ctx.save(); |
2 |
ctx.translate(100, 100); |
3 |
ctx.fillStyle = "rgb(0, 0, 255)"; |
4 |
ctx.fillRect(0, 0, 100, 100); |
5 |
ctx.restore(); |
¿Qué crees que pasará? Ten una estrella de oro si has adivinado que el nuevo cuadrado se dibujará en la posición (100, 100). No hay tiempo de juego para los que adivinaron mal. Lo siento.



¿Qué ha pasado aquí entonces? En cuanto al código para dibujar el segundo cuadrado, lo has dibujado en el mismo lugar que el primer cuadrado. La razón es que básicamente has desplazado todo el sistema de coordenadas del lienzo para que su posición (0, 0) esté ahora en el lugar (100, 100).



¿Tiene un poco más de sentido ahora? Espero que sí. Puede costar un poco hacerse a la idea, pero es un concepto sencillo una vez que lo entiendes.
Probablemente no usarás mucho esta transformación por sí sola, ya que podrías simplemente dibujar el segundo cuadrado en (100, 100) para conseguir el mismo efecto. Sin embargo, la belleza del translate
es que puedes combinarla con otras transformaciones para hacer cosas muy interesantes.
Veamos la siguiente transformación de la lista.
Cómo escalar tus visuales
Como probablemente has adivinado, la transformación de scale
se utiliza para cambiar el tamaño. Más específicamente, la transformación de escala se utiliza para escalar el contexto de renderizado 2d.
Elimina el código con el que trabajaste en el ejemplo de translate
, y añade el siguiente código:
1 |
ctx.fillRect(100, 100, 100, 100); |
Esto dibujará un cuadrado estándar en la posición (100, 100), con un ancho y un alto de 100 píxeles. Entonces, ¿cómo escalamos esto?



Las propiedades en escala son multiplicadores para las dimensiones x e y.
La transformación de scale
se utiliza de forma similar a la de translate
, en el sentido de que se llama antes de dibujar los objetos a los que se quiere aplicar. Es importante señalar que las propiedades en scale
son multiplicadores para las dimensiones x e y. Esto significa que una scale
de (1, 1) multiplicaría el tamaño del contexto de renderizado 2d por uno, dejándolo del mismo tamaño que tenía antes. Una scale
de (5, 5) multiplicaría el tamaño del contexto de renderizado 2d por cinco, haciéndolo cinco veces más grande que antes. Es sencillo.
En tu caso quieres duplicar el tamaño del cuadrado, así que aplicas una scale
de (2, 2):
1 |
ctx.save(); |
2 |
ctx.scale(2, 2); |
3 |
ctx.fillRect(100, 100, 100, 100); |
4 |
ctx.restore(); |
Lo que resulta en un cuadrado que es dos veces el tamaño:



Sin embargo, fíjate en que el cuadrado se dibuja ahora en una posición diferente a la que tenía antes de aplicar la scale
. La razón de esto es que la scale
multiplica el tamaño de todo en el contexto de renderizado 2d, incluyendo las coordenadas. En tu caso, la posición (100, 100) se convierte ahora en (200, 200); las coordenadas tienen el doble de tamaño que tendrían sin ser escaladas.
Para evitar esto, podemos realizar un translate
que mueva el origen del contexto de renderizado 2d a la posición en la que quieres dibujar el cuadrado. Si luego aplicas la scale
y dibujas el cuadrado en la posición (0, 0), su posición no se desplazará:
1 |
ctx.save(); |
2 |
ctx.translate(100, 100); |
3 |
ctx.scale(2, 2); |
4 |
ctx.fillRect(0, 0, 100, 100); |
5 |
ctx.restore(); |
El resultado es un cuadrado que es el doble de grande que el original, pero que se dibuja en la misma posición que el original:



Ser consciente de estas pequeñas peculiaridades de las transformaciones es lo que realmente ayuda a la hora de utilizarlas. La mayoría de los problemas comunes con las transformaciones parecen ser el resultado de no entender completamente cómo funcionan.
Elementos giratorios
Hasta ahora, todas las transformaciones con las que se ha tratado han sido bastante poco emocionantes. Afortunadamente, la transformación de rotate
está aquí para salvar el día, y es fácilmente mi favorita del grupo.
Estoy seguro de que rotate
no necesita presentación, así que vamos a rotar un cuadrado 45 grados (recuerda que los grados deben estar en radianes):
1 |
ctx.save(); |
2 |
ctx.rotate(Math.PI/4); // Rotate 45 degrees (in radians) |
3 |
ctx.fillRect(100, 100, 100, 100); |
4 |
ctx.restore(); |
Que posiciona un cuadrado en (100, 100) y rota... ¡woah, espera! Esto no se ve bien:



¿Ves lo que ha pasado? El cuadrado parece estar tratando de escapar de la ventana del navegador, en lugar de girar en el lugar en la posición (100, 100). Esto se debe a que rotate
, como todas las transformaciones, afecta a todo el contexto de renderizado 2d, y no a los objetos individualmente.
A continuación se muestra una ilustración de lo que ocurre con el sistema de coordenadas cuando se realiza una rotate
de 45 grados:



¿Notas cómo todo el sistema de coordenadas ha girado 45 grados desde el punto de origen (0, 0)? Esto es lo que causó que el cuadrado pareciera escaparse de la ventana del navegador, simplemente porque la posición (100, 100) había sido rotada de golpe en el borde del navegador.
La forma más sencilla de evitar este problema es combinar rotate
con translate
, de esta manera:
1 |
ctx.save(); |
2 |
ctx.translate(150, 150); // Translate to centre of square |
3 |
ctx.rotate(Math.PI/4); // Rotate 45 degrees |
4 |
ctx.fillRect(-50, -50, 100, 100); // Centre at the rotation point |
5 |
ctx.restore(); |
Al realizar translate
se mueve el punto de origen del contexto de renderizado 2d (0, 0) a lo que debería ser el punto central del cuadrado (150, 150). Esto significa que cualquier rotación se basará ahora en la posición (150, 150). Si luego dibujas un cuadrado con una posición x e y negativa, igual a la mitad de la anchura y la altura del cuadrado, acabarás dibujando un cuadrado que parece haber sido girado alrededor de su punto central:



La transformación de rotate
es probablemente la más difícil de entender. Es importante recordar que las transformaciones se realizan en todo el contexto de renderizado 2d, y, si quieres rotar una forma alrededor de su punto central, tendrás que combinar rotate
con translate
.
Pasemos a algo un poco más impresionante visualmente.
Añadir sombras
Añadir sombras a los objetos es deliciosamente sencillo.
El lienzo se suministra con algunas propiedades para manipular la apariencia de los objetos que se dibujan en él, y un conjunto de estas propiedades permite añadir sombras.
Añadir sombras a los objetos es deliciosamente sencillo. Simplemente requiere que la propiedad shadowColor
se establezca en el contexto de renderizado 2d a un color que no sea negro transparente, y que cualquiera de las propiedades shadowBlur
, shadowOffsetX
o shadowOffsetY
se establezca a un valor distinto de 0.
Prueba el siguiente código:
1 |
ctx.save(); |
2 |
ctx.shadowBlur = 15; |
3 |
ctx.shadowColor = "rgb(0, 0, 0)"; |
4 |
ctx.fillRect(100, 100, 100, 100); |
5 |
ctx.restore(); |
Esto dará a la sombra un desenfoque de quince píxeles, y establecerá el color en negro sólido:



Hasta el momento es algo bastante normal.
Si estableces el shadowBlur
a 0, cambia el shadowColor
a un gris claro, y da un shadowOffsetX
y shadowOffsetY
positivos:
1 |
ctx.save(); |
2 |
ctx.shadowBlur = 0; |
3 |
ctx.shadowOffsetX = 6; |
4 |
ctx.shadowOffsetY = 6; |
5 |
ctx.shadowColor = "rgba(125, 125, 125, 0.5)"; // Transparent grey |
6 |
ctx.fillRect(300, 100, 100, 100); |
7 |
ctx.restore(); |
Terminará con una sombra sólida que aparece ligeramente a la derecha y debajo del objeto que se ha dibujado:



Aunque las sombras son geniales, pueden acaparar muchos recursos.
Es importante recordar que las sombras afectan a todo lo que se dibuja después de ser definidas, por lo que es útil utilizar los métodos de save
y restore
para evitar tener que restablecer las propiedades de las sombras una vez que las hayas utilizado.
Ten en cuenta que el rendimiento puede verse afectado cuando aplicas una sombra a muchos objetos al mismo tiempo. En algunos casos, puede valer la pena usar una imagen PNG con una sombra en lugar de dibujar un objeto manualmente y aplicar una sombra dinámica usando código. Cubriremos cómo usar imágenes con canvas en la próxima entrega de esta serie.
Creación de degradados
Puedes crear dos tipos de degradados en el lienzo: lineales y radiales.
La última característica que quiero cubrir contigo en este tutorial son los gradientes. Hay dos tipos de gradientes en el lienzo, el primero es el gradiente lineal (recto). Puedes crear un gradiente lineal usando el método createLinearGradient
(sorprendentemente), que se ve así en pseudo-código:
1 |
ctx.createLinearGradient(startX, startY, endX, endY); |
El primer conjunto de dos argumentos son la posición x e y del inicio del gradiente, y el segundo conjunto de argumentos son la posición x e y del final del gradiente. También es importante señalar que un gradiente en el lienzo es en realidad un tipo de valor de color, por lo que se aplican a las propiedades fillStyle y strokeStyle.
Este es un ejemplo de cómo crear un gradiente lineal que va desde la parte superior del lienzo hasta la parte inferior:
1 |
var gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); |
2 |
gradient.addColorStop(0, "rgb(255, 255, 255)"); |
3 |
gradient.addColorStop(1, "rgb(0, 0, 0)"); |
4 |
|
5 |
ctx.save(); |
6 |
ctx.fillStyle = gradient; |
7 |
ctx.fillRect(0, 0, canvas.width, canvas.height); |
8 |
ctx.restore(); |
Observa cómo asigna el gradiente a una variable, y luego utiliza esa variable para llamar al método addColorStop
. Este método te permite establecer el color en puntos particulares a lo largo del gradiente. Por ejemplo, la posición 0 representaría el inicio del gradiente (la primera posición x e y), y 1 representaría el final del gradiente (la segunda posición x e y). También puedes utilizar puntos decimales entre 0 y 1 para asignar un color en un punto diferente a lo largo del gradiente, como 0,5 sería la mitad del camino.
Aplicando la variable gradiente a la propiedad fillStyle
, se obtiene un bonito gradiente que va del blanco (en la posición 0 en la parte superior del lienzo), al negro (en la posición 1 en la parte inferior del lienzo):



Pero no siempre hay que utilizar degradados lineales; también se pueden crear degradados radiales.
Los gradientes radiales se crean con el método createRadialGradient
, que tiene este aspecto en pseudocódigo:
1 |
ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius); |
El primer conjunto de tres argumentos son la posición x e y, así como el radio del círculo al inicio del gradiente, y los tres argumentos finales representan la posición x e y, así como el radio del círculo al final del gradiente.
Suena confuso, ¿verdad? Lo es un poco, así que vamos a crear un gradiente radial para ver qué pasa:
1 |
var gradient = ctx.createRadialGradient(350, 350, 0, 50, 50, 100); |
2 |
gradient.addColorStop(0, "rgb(0, 0, 0)"); |
3 |
gradient.addColorStop(1, "rgb(125, 125, 125)"); |
4 |
|
5 |
ctx.save(); |
6 |
ctx.fillStyle = gradient; |
7 |
ctx.fillRect(0, 0, canvas.width, canvas.height); |
8 |
ctx.restore(); |
Has creado un gradiente radial que tiene un punto inicial en (350, 350) con un radio de 0, y un punto final en (50, 50) con un radio de 100. ¿Puedes adivinar qué aspecto tendrá? 20 puntos si adivinaste que se vería así:



Si eres como yo, esto no es lo que esperaba ver. Ya he utilizado degradados radiales en aplicaciones como Adobe Photoshop, ¡Y no se parecen en nada a esto! Entonces, ¿por qué tiene este aspecto? Bueno, eso es lo que se supone que debe parecer, extrañamente.
Mira este diagrama que describe exactamente cómo funciona un gradiente radial en el lienzo:



Interesante, ¿Verdad? Básicamente te permite crear una forma de cono, pero ¿Qué pasa si quieres crear un degradado radial adecuado como el de Photoshop? Afortunadamente, es sencillo.
Para crear un gradiente radial adecuado solo hay que colocar los dos círculos del gradiente exactamente en la misma posición x e y, asegurándose de que uno de los círculos del gradiente sea mayor que el otro:
1 |
var canvasCentreX = canvas.width/2; |
2 |
var canvasCentreY = canvas.height/2; |
3 |
|
4 |
var gradient = ctx.createRadialGradient(canvasCentreX, canvasCentreY, 250, canvasCentreX, canvasCentreY, 0); |
5 |
gradient.addColorStop(0, "rgb(0, 0, 0)"); |
6 |
gradient.addColorStop(1, "rgb(125, 125, 125)"); |
7 |
|
8 |
ctx.save(); |
9 |
ctx.fillStyle = gradient; |
10 |
ctx.fillRect(0, 0, canvas.width, canvas.height); |
11 |
ctx.restore(); |
El código anterior crea un gradiente radial que se sitúa en el centro del lienzo. Uno de los círculos del gradiente tiene un radio de 0, mientras que el otro tiene un radio de 250. El resultado es un gradiente radial tradicional que se desplaza desde el centro del lienzo hacia el exterior, así:



¡Eso se ve mejor! Sinceramente, me quedé sorprendido cuando vi cómo se implementaban los gradientes radiales en el lienzo. Apuesto a que ha confundido a mucha gente al ver esa forma cónica. Oh, bueno, al menos ahora sabes cómo crear los adecuados.
Vale la pena señalar que los gradientes en el lienzo también son operaciones bastante intensivas. Si quieres cubrir todo el lienzo con un degradado, yo consideraría primero aplicar un fondo de degradado CSS3 al propio elemento del lienzo.
Conclusión
En este artículo, hemos revisado cómo realizar transformaciones básicas en el lienzo, incluyendo traslaciones, escalado y rotación. También has aprendido a añadir sombras a los objetos y a crear degradados. No parece gran cosa, pero las transformaciones, en particular, constituyen la espina dorsal de algunas de las cosas más geniales que se pueden lograr en el lienzo.
En la siguiente entrada de "Canvas from Scratch", vamos a romper con el dibujo de objetos y echar un vistazo a cómo manipular imágenes y vídeo en el lienzo. Aquí es donde las cosas empiezan a ponerse realmente interesantes. ¡Manténte en sintonía!