Dibujando formas con canvas
Ahora que hemos preparado nuestro entorno canvas, podemos entrar en detalles de cómo dibujar en el canvas. Al final de este artÃculo, habrás aprendido cómo dibujar rectángulos, triángulos, lÃneas, arcos y curvas, familiarizándote con algunas de las formas básicas. Trabajar con trazados es esencial a la hora de dibujar objetos en el canvas y veremos cómo hacerlo.
La cuadrÃcula
Antes de empezar a dibujar, tenemos que hablar de la cuadrÃcula del canvas o del espacio de coordenadas. Nuestra estructura HTML de la página anterior tenÃa un elemento de canvas de 150 pÃxeles de ancho y 150 pÃxeles de alto.

Normalmente, 1 unidad en la cuadrÃcula corresponde a 1 pÃxel en el canvas. El origen de esta cuadrÃcula se sitúa en la esquina superior izquierda en la coordenada (0,0). Todos los elementos se colocan en relación con este origen. Asà que la posición de la esquina superior izquierda del cuadrado azul se sitúa a x pÃxeles de la izquierda y a y pÃxeles de la parte superior, en la coordenada (x,y). Más adelante en este tutorial veremos cómo podemos trasladar el origen a una posición diferente, rotar la cuadrÃcula e incluso escalarla, pero por ahora nos ceñiremos a la posición por defecto.
Dibujar rectángulos
A diferencia de SVG, <canvas> sólo admite dos formas primitivas: rectángulos y trazados (listas de puntos conectados por lÃneas). Todas las demás formas deben crearse combinando uno o más trazados. Por suerte, tenemos un surtido de funciones de dibujo de trazados que hacen posible componer formas muy complejas.
Primero veamos el rectángulo. Hay tres funciones que dibujan rectángulos en el canvas:
fillRect(x, y, width, height)-
Dibuja un rectángulo relleno.
strokeRect(x, y, width, height)-
Dibuja un contorno rectangular.
clearRect(x, y, width, height)-
Borra el área rectangular especificada, haciéndola totalmente transparente.
Cada una de estas tres funciones toma los mismos parámetros. x e y especifican la posición en el canvas (relativa al origen) de la esquina superior izquierda del rectángulo. width y height proporcionan el tamaño del rectángulo.
A continuación se muestra la función draw() de la página anterior, pero ahora hace uso de estas tres funciones.
Ejemplo de forma rectangular
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
ctx.fillRect(25, 25, 100, 100);
ctx.clearRect(45, 45, 60, 60);
ctx.strokeRect(50, 50, 50, 50);
}
}
La salida de este ejemplo se muestra a continuación.
La función fillRect() dibuja un gran cuadrado negro de 100 pÃxeles en cada lado. La función clearRect() borra un cuadrado de 60x60 pÃxeles del centro, y luego se llama a strokeRect() para crear un contorno rectangular de 50x50 pÃxeles dentro del cuadrado borrado.
En las próximas páginas veremos dos métodos alternativos para clearRect(), y también veremos cómo cambiar el color y el estilo de trazo de las formas renderizadas.
A diferencia de las funciones de trazado que veremos en la siguiente sección, las tres funciones de rectángulo dibujan inmediatamente en el canvas.
Dibujar trazados (paths)
Veamos ahora los trazados. Un trazado es una lista de puntos, conectados por segmentos de lÃneas que pueden ser de diferentes formas, curvas o no, de diferente anchura y de diferente color. Un trazado, o incluso un sub-trazado, puede ser cerrado. Para hacer formas usando trazados, damos algunos pasos adicionales:
- Primero, se crea el camino.
- Luego, se utiliza comandos de dibujo para dibujar en el trazado.
- Una vez creado el trazado, puedes trazar o rellenar el trazado para renderizarlo.
Aquà están las funciones utilizadas para realizar estos pasos:
beginPath()-
Crea un nuevo trazado. Una vez creado, los futuros comandos de dibujo se dirigen al trazado y se utilizan para construirlo.
- Métodos de trazado (path)
-
Métodos para establecer diferentes trazados para los objetos.
closePath()-
Añade una lÃnea recta al trazado, que va al inicio del sub-trazado actual.
stroke()-
Dibuja la forma trazando su contorno.
fill()-
Dibuja una forma sólida rellenando el área de contenido del trazado.
El primer paso para crear un trazado es llamar a beginPath(). Internamente, los trazados se almacenan como una lista de sub-trazados (lÃneas, arcos, etc.) que juntos forman una forma. Cada vez que se llama a este método, la lista se restablece y podemos empezar a dibujar nuevas formas.
Nota:
Cuando el trazado actual está vacÃo, como por ejemplo inmediatamente después de llamar a beginPath(), o en un canvas recién creado, el primer comando de construcción del trazado siempre se trata como un moveTo(), independientemente de lo que realmente sea. Por esta razón, casi siempre querrá establecer especÃficamente su posición inicial después de reiniciar un trazado.
El segundo paso es llamar a los métodos que realmente especifican los trazados a dibujar. Los veremos en breve.
El tercer paso, y opcional, es llamar a closePath(). Este método intenta cerrar la forma dibujando una lÃnea recta desde el punto actual hasta el inicio. Si la forma ya ha sido cerrada o sólo hay un punto en la lista, esta función no hace nada.
Nota:
Cuando se llama a fill(), cualquier forma abierta se cierra automáticamente, por lo que no es necesario llamar a closePath(). Este no es el caso cuando se llama a stroke().
Dibujar un triángulo
Por ejemplo, el código para dibujar un triángulo serÃa algo asÃ:
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(75, 50);
ctx.lineTo(100, 75);
ctx.lineTo(100, 25);
ctx.fill();
}
}
El resultado se ve asÃ:
Mover la pluma
Una función muy útil, que en realidad no dibuja nada sino que se convierte en parte de la lista de trazados descrita anteriormente, es la función moveTo(). La mejor manera de pensar en esto es como levantar un bolÃgrafo o un lápiz de un lugar en un pedazo de papel y colocarlo en el siguiente.
moveTo(x, y)-
Mueve la pluma a las coordenadas especificadas por
xey.
Cuando se inicializa el canvas o se llama a beginPath(), normalmente se querrá utilizar la función moveTo() para colocar el punto de partida en otro lugar. También podemos usar moveTo() para dibujar trazados no conectados. Echa un vistazo a la cara sonriente de abajo.
Para probarlo por ti mismo, puedes utilizar el siguiente fragmento de código. Sólo tienes que pegarlo en la función draw() que vimos antes.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // CÃrculo externo
ctx.moveTo(110, 75);
ctx.arc(75, 75, 35, 0, Math.PI, false); // Boca (en el sentido de las agujas del reloj)
ctx.moveTo(65, 65);
ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // Ojo izquierdo
ctx.moveTo(95, 65);
ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // Ojo derecho
ctx.stroke();
}
}
El resultado se ve asÃ:
Si quisieras ver las lÃneas conectadas, puedes eliminar las lÃneas que llaman a moveTo().
Nota:
Para saber más sobre la función arc(), consulte la sección Arcos más abajo.
LÃneas
Para dibujar lÃneas rectas, utilice el método lineTo().
lineTo(x, y)-
Dibuja una lÃnea desde la posición actual de dibujo hasta la posición especificada por
xey.
Este método toma dos argumentos, x e y, que son las coordenadas del punto final de la lÃnea. El punto de partida depende de los trazados anteriores, donde el punto final del trazado anterior es el punto de partida del siguiente, etc. El punto de partida también puede cambiarse utilizando el método moveTo().
El ejemplo siguiente dibuja dos triángulos, uno relleno y otro contorneado.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
// Triángulo relleno
ctx.beginPath();
ctx.moveTo(25, 25);
ctx.lineTo(105, 25);
ctx.lineTo(25, 105);
ctx.fill();
// Triángulo contorneado
ctx.beginPath();
ctx.moveTo(125, 125);
ctx.lineTo(125, 45);
ctx.lineTo(45, 125);
ctx.closePath();
ctx.stroke();
}
}
Esto comienza llamando a beginPath() para iniciar un nuevo trazado de forma. A continuación, utilizamos el método moveTo() para mover el punto de partida a la posición deseada. Debajo de esto, se dibujan dos lÃneas que forman los dos lados del triángulo.
Notará la diferencia entre el triángulo relleno y el trazado. Esto se debe, como se ha mencionado anteriormente, a que las formas se cierran automáticamente cuando se rellena un trazado, pero no cuando se traza. Si omitimos el closePath() para el triángulo trazado, sólo se habrÃan dibujado dos lÃneas, no un triángulo completo.
Arcos
Para dibujar arcos o cÃrculos, utilizamos los métodos arc() o arcTo().
arc(x, y, radius, startAngle, endAngle, counterclockwise)-
Dibuja un arco centrado en la posición (x, y) con radio r que comienza en startAngle y termina en endAngle yendo en la dirección indicada por counterclockwise (por defecto en el sentido de las agujas del reloj).
arcTo(x1, y1, x2, y2, radius)-
Dibuja un arco con los puntos de control y el radio dados, conectado al punto anterior por una lÃnea recta.
Veamos con más detalle el método arc, que toma seis parámetros: x e y son las coordenadas del centro del cÃrculo sobre el que se dibujará el arco. El parámetro radio se explica por sà mismo. Los parámetros startAngle y endAngle definen los puntos inicial y final del arco en radianes, a lo largo de la curva del cÃrculo. Se miden desde el eje x. El parámetro counterclockwise es un valor Booleano que, cuando es true, dibuja el arco en sentido contrario a las agujas del reloj; en caso contrario, el arco se dibuja en sentido de las agujas del reloj.
Nota:
Los ángulos en la función arc se miden en radianes, no en grados. Para convertir los grados en radianes puedes utilizar la siguiente expresión de JavaScript: radianes = (Math.PI/180)*grados.
El siguiente ejemplo es un poco más complejo que los que hemos visto anteriormente. Dibuja 12 arcos diferentes, todos con diferentes ángulos y rellenos.
Los dos bucles for son para recorrer las filas y columnas de arcos. Para cada arco, iniciamos un nuevo trazado llamando a beginPath(). En el código, cada uno de los parámetros del arco está en una variable para mayor claridad, pero no necesariamente se harÃa eso en la vida real.
Las coordenadas x e y deberÃan ser lo suficientemente claras. radius y startAngle son fijos. endAngle comienza en 180 grados (medio cÃrculo) en la primera columna y se incrementa en pasos de 90 grados, culminando en un cÃrculo completo en la última columna.
La sentencia para el parámetro clockwise hace que la primera y tercera fila se dibujen como arcos en el sentido de las agujas del reloj y la segunda y cuarta fila como arcos en sentido contrario. Por último, la sentencia if hace que la mitad superior tenga arcos trazados y la mitad inferior arcos rellenos.
Nota: Este ejemplo requiere un canvas ligeramente más grande que los otros de esta página: 150 x 200 pÃxeles.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 3; j++) {
ctx.beginPath();
const x = 25 + j * 50; // Coordenada x
const y = 25 + i * 50; // Coordenada y
const radius = 20; // Radio del Arco
const startAngle = 0; // Punto inicial del CÃrculo
const endAngle = Math.PI + (Math.PI * j) / 2; // Punto final del CÃrculo
const counterclockwise = i % 2 !== 0; // En el sentido de las agujas del reloj o en sentido contrario
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
if (i > 1) {
ctx.fill();
} else {
ctx.stroke();
}
}
}
}
}
Curvas de Bézier y cuadráticas
El siguiente tipo de trayectorias disponibles son las Curvas de Bézier, disponibles en las variedades cúbica y cuadrática. Se utilizan generalmente para dibujar formas orgánicas complejas.
quadraticCurveTo(cp1x, cp1y, x, y)-
Dibuja una curva cuadrática de Bézier desde la posición actual de la pluma hasta el punto final especificado por
xey, utilizando el punto de control especificado porcp1xycp1y. bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)-
Dibuja una curva cúbica de Bézier desde la posición actual de la pluma hasta el punto final especificado por
xey, utilizando los puntos de control especificados por (cp1x,cp1y) y (cp2x, cp2y).
La diferencia entre ellas es que una curva de Bézier cuadrática tiene un punto inicial y otro final (puntos azules) y sólo un punto de control (indicado por el punto rojo) mientras que una curva de Bézier cúbica utiliza dos puntos de control.

Los parámetros x e y de estos dos métodos son las coordenadas del punto final. Los parámetros cp1x y cp1y son las coordenadas del primer punto de control, y cp2x y cp2y son las coordenadas del segundo punto de control.
El uso de las curvas cuadráticas y cúbicas de Bézier puede ser un reto, porque a diferencia de los programas de dibujo vectorial como Adobe Illustrator, no tenemos información visual directa sobre lo que estamos haciendo. Esto hace que sea bastante difÃcil dibujar formas complejas. En el siguiente ejemplo, dibujaremos algunas formas orgánicas simples, pero si tienes tiempo y, sobre todo, paciencia, se pueden crear formas mucho más complejas.
No hay nada muy difÃcil en estos ejemplos. En ambos casos vemos cómo se dibuja una sucesión de curvas que finalmente dan lugar a una forma completa.
Curvas cuadráticas de Bézier
Este ejemplo utiliza múltiples curvas cuadráticas de Bézier para representar un globo de voz.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
// Ejemplo de curvas cuadráticas
ctx.beginPath();
ctx.moveTo(75, 25);
ctx.quadraticCurveTo(25, 25, 25, 62.5);
ctx.quadraticCurveTo(25, 100, 50, 100);
ctx.quadraticCurveTo(50, 120, 30, 125);
ctx.quadraticCurveTo(60, 120, 65, 100);
ctx.quadraticCurveTo(125, 100, 125, 62.5);
ctx.quadraticCurveTo(125, 25, 75, 25);
ctx.stroke();
}
}
Curvas cúbicas de Bézier
Este ejemplo dibuja un corazón utilizando curvas cúbicas de Bézier.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
// Ejemplo de curvas cúbicas
ctx.beginPath();
ctx.moveTo(75, 40);
ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
ctx.fill();
}
}
Rectángulos
Además de los tres métodos que vimos en Dibujar rectángulos, que dibujan formas rectangulares directamente en el canvas, existe también el método rect(), que añade un trazado rectangular a un trazado actualmente abierto.
rect(x, y, width, height)-
Dibuja un rectángulo cuya esquina superior izquierda está especificada por (
x,y) con elwidthyheightespecificados.
Antes de que se ejecute este método, se llama automáticamente al método moveTo() con los parámetros (x,y). En otras palabras, la posición actual de la pluma se restablece automáticamente a las coordenadas por defecto.
Hacer combinaciones
Hasta ahora, cada ejemplo de esta página ha utilizado sólo un tipo de función de trazado por forma. Sin embargo, no hay ninguna limitación en cuanto al número o los tipos de trazados que puedes utilizar para crear una forma. Asà que en este último ejemplo, vamos a combinar todas las funciones de trazado para crear un conjunto de personajes de juegos muy famosos.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
roundedRect(ctx, 12, 12, 150, 150, 15);
roundedRect(ctx, 19, 19, 150, 150, 9);
roundedRect(ctx, 53, 53, 49, 33, 10);
roundedRect(ctx, 53, 119, 49, 16, 6);
roundedRect(ctx, 135, 53, 49, 33, 10);
roundedRect(ctx, 135, 119, 25, 49, 10);
ctx.beginPath();
ctx.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7, false);
ctx.lineTo(31, 37);
ctx.fill();
for (let i = 0; i < 8; i++) {
ctx.fillRect(51 + i * 16, 35, 4, 4);
}
for (i = 0; i < 6; i++) {
ctx.fillRect(115, 51 + i * 16, 4, 4);
}
for (i = 0; i < 8; i++) {
ctx.fillRect(51 + i * 16, 99, 4, 4);
}
ctx.beginPath();
ctx.moveTo(83, 116);
ctx.lineTo(83, 102);
ctx.bezierCurveTo(83, 94, 89, 88, 97, 88);
ctx.bezierCurveTo(105, 88, 111, 94, 111, 102);
ctx.lineTo(111, 116);
ctx.lineTo(106.333, 111.333);
ctx.lineTo(101.666, 116);
ctx.lineTo(97, 111.333);
ctx.lineTo(92.333, 116);
ctx.lineTo(87.666, 111.333);
ctx.lineTo(83, 116);
ctx.fill();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.moveTo(91, 96);
ctx.bezierCurveTo(88, 96, 87, 99, 87, 101);
ctx.bezierCurveTo(87, 103, 88, 106, 91, 106);
ctx.bezierCurveTo(94, 106, 95, 103, 95, 101);
ctx.bezierCurveTo(95, 99, 94, 96, 91, 96);
ctx.moveTo(103, 96);
ctx.bezierCurveTo(100, 96, 99, 99, 99, 101);
ctx.bezierCurveTo(99, 103, 100, 106, 103, 106);
ctx.bezierCurveTo(106, 106, 107, 103, 107, 101);
ctx.bezierCurveTo(107, 99, 106, 96, 103, 96);
ctx.fill();
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(101, 102, 2, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(89, 102, 2, 0, Math.PI * 2, true);
ctx.fill();
}
}
// Una función auxiliar para dibujar un rectángulo con esquinas redondeadas.
function roundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
}
La imagen resultante se ve asÃ:
No vamos a repasar esto en detalle, ya que en realidad es sorprendentemente sencillo. Las cosas más importantes a tener en cuenta son el uso de la propiedad fillStyle en el contexto de dibujo, y el uso de una función auxiliar (en este caso roundedRect()). El uso de funciones auxiliares para las partes del dibujo que se hacen a menudo puede ser muy útil y reducir la cantidad de código que se necesita, asà como su complejidad.
Volveremos a ver fillStyle, con más detalle, más adelante en este tutorial. AquÃ, todo lo que estamos haciendo es utilizarlo para cambiar el color de relleno de los trazados desde el color por defecto de negro a blanco, y luego de nuevo.
Objetos Path2D
Como hemos visto en el último ejemplo, puede haber una serie trazados y comandos de dibujo para dibujar objetos en su canvas. Para simplificar el código y mejorar el rendimiento, el objeto Path2D, disponible en las versiones recientes de los navegadores, le permite almacenar en caché o grabar estos comandos de dibujo. De este modo, se pueden reproducir los trazados rápidamente.
Veamos cómo podemos construir un objeto Path2D:
Path2D()-
El constructor
Path2D()devuelve un objetoPath2Drecién instanciado, opcionalmente con otra ruta como argumento (crea una copia), u opcionalmente con una cadena de caracteres formada por datos de un trazado SVG path.
new Path2D(); // Objeto Path2D vacÃo
new Path2D(path); // Copia de otro objecto Path2D
new Path2D(d); // Path2D a partir de datos de un trazado (SVG path)
Todos los Métodos de trazado como moveTo, rect, arc o quadraticCurveTo, etc., que hemos conocido anteriormente, están disponibles en los objetos Path2D.
La API Path2D también añade una forma de combinar trazados mediante el método addPath. Esto puede ser útil cuando se quiere construir objetos a partir de varios componentes, por ejemplo.
Path2D.addPath(path [, transform])-
Añade un trazado al trazado actual con una matriz de transformación opcional.
Ejemplo de Path2D
En este ejemplo, estamos creando un rectángulo y un cÃrculo. Ambos se almacenan como un objeto Path2D, para que estén disponibles para su uso posterior. Con la nueva API Path2D, varios métodos se han actualizado para aceptar opcionalmente un objeto Path2D para utilizarlo en lugar del trazado actual. AquÃ, stroke y fill se utilizan con un argumento de trazado para dibujar ambos objetos en el canvas, por ejemplo.
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
const rectangle = new Path2D();
rectangle.rect(10, 10, 50, 50);
const circle = new Path2D();
circle.arc(100, 35, 25, 0, 2 * Math.PI);
ctx.stroke(rectangle);
ctx.fill(circle);
}
}
Uso de trazados (SVG paths)
Otra poderosa caracterÃstica de la nueva API Path2D del canvas es el uso de datos de trazados o SVG path para inicializar los trazados en el canvas. Esto podrÃa permitirle pasar los datos del trazado y reutilizarlos tanto en el SVG como en el canvas.
El trazado se moverá al punto (M10 10) y luego se moverá horizontalmente 80 puntos a la derecha (h 80), luego 80 puntos hacia abajo (v 80), luego 80 puntos a la izquierda (h -80), y luego de vuelta al inicio (z). Puedes ver este ejemplo en el constructor Path2D.
const p = new Path2D("M10 10 h 80 v 80 h -80 Z");