Curso HTML – Módulo 12: Gráficos en HTML con SVG y Canvas

Curso HTML – Módulo 12: Gráficos en HTML con SVG y Canvas

Los gráficos en HTML permiten crear iconos, diagramas, mapas, visualizaciones y animaciones dentro de una página web. Un plano interactivo, un gráfico de barras y un juego 2D pueden parecer similares en pantalla, pero cada uno plantea necesidades distintas.

En este módulo trabajaremos con SVG y Canvas. Con SVG describiremos figuras que el navegador puede escalar y tratar como elementos individuales. Con Canvas usaremos JavaScript para dibujar sobre una superficie de píxeles y actualizarla cuando cambie la escena.

Empezaremos con formas sencillas y terminaremos con dos ejemplos completos: un gráfico de barras accesible en SVG y una animación en Canvas con controles de reproducción.

Curso de HTML de TecnoPS
Módulo anterior: 11. Layout y diseño de página.
Módulo actual: 12. Gráficos en HTML con SVG y Canvas.
Próximo módulo: 13. APIs y funcionalidades avanzadas.

Objetivos del módulo

Al finalizar este módulo deberías ser capaz de:

  • Distinguir entre gráficos vectoriales y gráficos de píxeles.
  • Crear figuras y trazados mediante <svg>.
  • Configurar viewBox y adaptar un SVG a su contenedor.
  • Elegir cómo incorporar un SVG en una página.
  • Dibujar formas, texto e imágenes con el contexto 2d de Canvas.
  • Animar una escena con requestAnimationFrame().
  • Adaptar el dibujo al tamaño visible y a la densidad de píxeles.
  • Ofrecer alternativas accesibles y elegir una técnica según el proyecto.

1. Dos formas de crear gráficos en HTML

Imagina un círculo azul. En SVG puedes describirlo indicando su centro, su radio y su color. El navegador usa esa descripción para dibujar el círculo a distintos tamaños. También puedes localizar el elemento y cambiar su color con CSS o JavaScript.

En Canvas puedes dibujar el mismo círculo, pero el resultado queda pintado en una superficie de píxeles. Si más tarde quieres moverlo, tu programa debe guardar su posición y volver a dibujar la escena. Canvas no conserva un elemento DOM separado para cada figura.

Característica SVG Canvas
Representación Elementos y formas descritos mediante marcado. Superficie de dibujo sobre la que se generan píxeles.
Cambiar una figura Modificar su elemento, sus atributos o sus estilos. Actualizar los datos de la escena y dibujar de nuevo.
Interacción Las figuras pueden recibir eventos del DOM. El programa identifica qué figura coincide con las coordenadas del evento.
Usos habituales Iconos, diagramas, mapas y gráficos con elementos seleccionables. Juegos 2D, dibujo libre, edición de imágenes y simulaciones.

Puedes consultar la definición de la superficie de dibujo y su contenido alternativo en el estándar HTML de WHATWG.

2. Tu primer gráfico SVG

SVG significa Scalable Vector Graphics, es decir, gráficos vectoriales escalables. El elemento <svg> contiene la escena y las etiquetas interiores definen sus figuras.

<svg viewBox="0 0 200 120" width="200" height="120"
     role="img" aria-label="Círculo azul sobre un fondo claro">
  <rect x="0" y="0" width="200" height="120" rx="12" fill="#eaf2ff" />
  <circle cx="100" cy="60" r="35" fill="#1767c0" />
</svg>

<rect> crea el fondo. Sus atributos x e y indican la posición de la esquina superior izquierda. rx redondea las esquinas. El elemento <circle> coloca el centro en (100, 60) y usa un radio de 35 unidades.

En el sistema de coordenadas inicial, el eje X aumenta hacia la derecha y el eje Y hacia abajo. Por eso, si cambias cy=»60″ por cy=»80″, el círculo baja.

3. Qué hace viewBox

viewBox define el rectángulo de coordenadas internas que quieres mostrar. Sus cuatro valores son mínimo X, mínimo Y, ancho y alto.

<svg viewBox="0 0 200 120" width="400" height="240">
  <circle cx="100" cy="60" r="35" fill="#1767c0" />
</svg>

El dibujo sigue utilizando un espacio interno de 200 × 120 unidades, pero en este caso se presenta en un área de 400 × 240. El círculo conserva sus coordenadas y el navegador escala su representación.

Para integrarlo en un diseño adaptable como el del módulo anterior, puedes dejar que el gráfico se reduzca cuando su contenedor sea estrecho.

.grafico-svg {
  display: block;
  max-width: 100%;
  height: auto;
}

Por defecto, preserveAspectRatio conserva las proporciones y centra el dibujo cuando la relación de aspecto del área visible y la del viewBox difieren. Con preserveAspectRatio=»none» puedes estirar el dibujo en ambos ejes. Pruébalo con un círculo para ver cómo se convierte en una elipse. La especificación de coordenadas de SVG explica cómo se relacionan estos espacios.

4. Formas básicas de SVG

Elemento Qué dibuja Atributos habituales
<rect> Rectángulo. x, y, width, height, rx.
<circle> Círculo. cx, cy, r.
<ellipse> Elipse. cx, cy, rx, ry.
<line> Segmento entre dos puntos. x1, y1, x2, y2.
<polyline> Secuencia de segmentos. points.
<polygon> Figura que une el último punto con el primero. points.
<path> Trazado que puede combinar líneas, curvas y arcos. d.

fill controla el relleno. stroke define el color del trazo y stroke-width su grosor. En una línea o un trazado abierto, puedes usar fill=»none» si solo quieres mostrar el contorno.

<svg viewBox="0 0 200 100" width="200" height="100"
     role="img" aria-label="Línea quebrada ascendente">
  <polyline points="10,85 55,55 100,65 145,30 190,15"
            fill="none" stroke="#1767c0" stroke-width="4"
            stroke-linecap="round" stroke-linejoin="round" />
</svg>

5. Trazados con path

<path> lee una secuencia de instrucciones en su atributo d. Por ejemplo, el siguiente trazado dibuja una montaña con una cima redondeada.

<svg viewBox="0 0 120 90" width="120" height="90" role="img" aria-label="Silueta de una montaña">
  <path d="M 10 75 L 48 27 Q 60 12 72 27 L 110 75 Z" fill="#19765b" />
</svg>
  • M mueve el punto inicial a (10, 75).
  • L dibuja un segmento hasta (48, 27).
  • Q dibuja una curva cuadrática que usa (60, 12) como punto de control y termina en (72, 27).
  • El segundo L lleva el trazado a (110, 75).
  • Z une el último punto con el inicial.

También encontrarás H y V para segmentos horizontales y verticales, C para curvas cúbicas y A para arcos. Las versiones en minúscula expresan posiciones relativas al punto actual. Para practicar, cambia las coordenadas de la Q y observa cómo se modifica la cima.

6. Agrupar, transformar y reutilizar figuras

Con <g> puedes aplicar una transformación a varias figuras como conjunto. Este ejemplo define una señal una vez y la coloca en dos posiciones.

<svg viewBox="0 0 160 70" width="160" height="70"
     role="img" aria-label="Dos señales circulares con una marca de verificación">
  <defs>
    <g id="m12-senal">
      <circle cx="0" cy="0" r="22" fill="#19765b" />
      <path d="M -10 0 L -3 7 L 11 -8"
            fill="none" stroke="white" stroke-width="4"
            stroke-linecap="round" stroke-linejoin="round" />
    </g>
  </defs>

  <use href="#m12-senal" transform="translate(40 35)" />
  <use href="#m12-senal" transform="translate(120 35)" />
</svg>

<defs> guarda definiciones que se pueden referenciar. <use> reutiliza la figura y translate() desplaza su sistema de coordenadas. En un grupo también puedes utilizar rotate() y scale(). El orden de las transformaciones puede cambiar el resultado.

Las definiciones también sirven para reutilizar degradados, máscaras y recortes. En una página con varios SVG en línea, utiliza identificadores únicos para que cada referencia encuentre su definición. Puedes ampliar estos elementos en la especificación de estructura de SVG.

7. Cómo incorporar un SVG en una página

Técnica Cuándo utilizarla Accesibilidad
<img src=»icono.svg»> Para cargar un archivo SVG como imagen reutilizable. Usa alt para describir su función o alt=»» si es decorativo.
<svg> en línea Cuando necesitas modificar las figuras desde CSS o JavaScript de la página. Si es una ilustración informativa, dale un nombre accesible. Si solo decora, puedes ocultarla a las tecnologías de asistencia.
background-image Para fondos, texturas y adornos de CSS. Incluye en el HTML cualquier información necesaria para entender la página.
<img src="mapa-del-recinto.svg"
     alt="Plano del recinto: la entrada está al sur y las aulas al norte"
     width="480" height="320">

Para guardar un SVG como archivo independiente, añade xmlns=»http://www.w3.org/2000/svg» al elemento raíz. Al escribir <svg> directamente dentro de un documento HTML, el navegador identifica el espacio de nombres al interpretar el marcado.

Si vas a insertar el marcado de un SVG procedente de usuarios o de terceros, trátalo como contenido activo y aplica el proceso de saneamiento de tu aplicación antes de introducirlo en el documento.

8. SVG, CSS e interacción

Un SVG en línea puede heredar el color del texto mediante currentColor. Este enlace combina un icono decorativo y una etiqueta visible.

<a href="https://tecnops.es/curso-html-modulo-9-medios-en-html/">
  <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false">
    <path d="M 8 5 L 19 12 L 8 19 Z" fill="currentColor" />
  </svg>
  Módulo 9: Medios en HTML
</a>

El navegador presenta el texto como nombre del enlace. El triángulo lo acompaña visualmente. Esta misma técnica funciona con un <button> si el icono representa una acción.

En gráficos interactivos puedes cambiar clases o atributos para destacar una figura. Si esa acción es necesaria para utilizar el gráfico, ofrece una forma de ejecutarla con el teclado. Una opción sencilla es usar botones HTML para seleccionar los datos y reflejar la selección en el SVG.

9. Ejemplo completo de SVG: gráfico de barras accesible

Guarda el siguiente código como grafico-svg.html y ábrelo en el navegador. El gráfico compara los ejercicios completados durante tres semanas. La tabla proporciona los mismos datos en un formato fácil de consultar, incluso si las etiquetas del SVG se ven pequeñas en una pantalla estrecha.

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Ejercicios completados por semana | SVG</title>
  <style>
    body {
      max-width: 48rem;
      margin: 2rem auto;
      padding-inline: 1rem;
      font: 1rem/1.6 system-ui, sans-serif;
      color: #17263a;
    }

    figure {
      margin: 0 0 2rem;
    }

    svg {
      display: block;
      max-width: 100%;
      height: auto;
      margin-block-end: 0.75rem;
      background: #f1f6fd;
      border-radius: 0.75rem;
    }

    table {
      width: 100%;
      border-collapse: collapse;
    }

    caption {
      margin-block-end: 0.5rem;
      font-weight: 700;
      text-align: left;
    }

    th,
    td {
      padding: 0.5rem;
      border-bottom: 1px solid #bac7d8;
      text-align: left;
    }
  </style>
</head>
<body>
  <main>
    <h1>Ejercicios completados por semana</h1>

    <figure>
      <svg viewBox="0 0 420 230" width="420" height="230"
           role="img" aria-labelledby="titulo-grafico"
           aria-describedby="descripcion-grafico">
        <title id="titulo-grafico">Gráfico de barras de ejercicios por semana</title>
        <desc id="descripcion-grafico">
          Se completaron 4 ejercicios la primera semana, 7 la segunda
          y 10 la tercera. Los valores también figuran en la tabla.
        </desc>

        <line x1="40" y1="180" x2="390" y2="180"
              stroke="#60748e" stroke-width="2" />

        <g fill="#1767c0">
          <rect x="67" y="124" width="70" height="56" rx="4" />
          <rect x="175" y="82" width="70" height="98" rx="4" />
          <rect x="283" y="40" width="70" height="140" rx="4" />
        </g>

        <g fill="#17263a" text-anchor="middle" font-size="14">
          <text x="102" y="115">4</text>
          <text x="210" y="73">7</text>
          <text x="318" y="31">10</text>

          <text x="102" y="204">Semana 1</text>
          <text x="210" y="204">Semana 2</text>
          <text x="318" y="204">Semana 3</text>
        </g>
      </svg>

      <figcaption>
        La tercera semana se completaron seis ejercicios más que la primera.
      </figcaption>
    </figure>

    <table>
      <caption>Datos del gráfico</caption>
      <thead>
        <tr>
          <th scope="col">Semana</th>
          <th scope="col">Ejercicios completados</th>
        </tr>
      </thead>
      <tbody>
        <tr><th scope="row">Semana 1</th><td>4</td></tr>
        <tr><th scope="row">Semana 2</th><td>7</td></tr>
        <tr><th scope="row">Semana 3</th><td>10</td></tr>
      </tbody>
    </table>
  </main>
</body>
</html>

Las barras comparten una base en y = 180. Cada ejercicio equivale a 14 unidades de altura. La primera barra mide 4 × 14 = 56 y empieza en 180 − 56 = 124. Como el eje Y crece hacia abajo, una barra más alta empieza en una coordenada menor.

Para convertir este ejercicio en un gráfico generado desde datos, usa una única lista de valores para calcular las alturas, las etiquetas y la tabla. Así evitas que se desactualice una parte al cambiar otra.

10. Canvas y el contexto de dibujo 2D

<canvas> reserva la superficie. Para pintar en ella necesitas obtener un contexto de dibujo. En este módulo usaremos getContext(«2d»), que proporciona métodos para dibujar figuras, texto e imágenes.

<canvas id="dibujo" width="320" height="180" role="img" aria-label="Rectángulo azul de ejemplo">
  Rectángulo azul de ejemplo.
</canvas>

<script>
  const lienzo = document.querySelector("#dibujo");
  const contexto = lienzo.getContext("2d");

  if (contexto) {
    contexto.fillStyle = "#1767c0";
    contexto.fillRect(40, 30, 160, 90);
  }
</script>

fillRect(x, y, ancho, alto) pinta un rectángulo. En el ejemplo, su esquina superior izquierda está en (40, 30). El navegador usa fillStyle como color de relleno.

Cuando omites los atributos width y height, el tamaño inicial del bitmap es de 300 × 150 píxeles. CSS puede mostrarlo a otro tamaño. Si amplías mucho su tamaño visible sin ajustar la resolución interna, el resultado puede verse borroso.

11. Trazados, círculos y texto en Canvas

Los trazados se construyen paso a paso. beginPath() inicia un trazado nuevo. moveTo() establece el punto inicial, lineTo() añade un segmento y stroke() pinta su contorno.

// Ejecuta este fragmento después de obtener un contexto 2D.
contexto.beginPath();
contexto.moveTo(25, 135);
contexto.lineTo(95, 80);
contexto.lineTo(155, 110);
contexto.strokeStyle = "#1767c0";
contexto.lineWidth = 4;
contexto.stroke();

contexto.beginPath();
contexto.arc(240, 85, 30, 0, 2 * Math.PI);
contexto.fillStyle = "#19765b";
contexto.fill();

contexto.font = "16px system-ui";
contexto.fillStyle = "#17263a";
contexto.fillText("Un círculo en Canvas", 150, 150);

arc() recibe el centro, el radio y los ángulos inicial y final en radianes. Un recorrido de 0 a 2 × π completa una vuelta. fillText() dibuja el texto en el bitmap. Si ese texto transmite información importante, incluye también su equivalente en HTML.

12. Estado, transformaciones e imágenes en Canvas

El contexto conserva propiedades como el color de relleno, el grosor de línea y la transformación actual. Con save() y restore() puedes limitar cambios a una parte del dibujo.

contexto.save();
contexto.translate(160, 90);
contexto.rotate(Math.PI / 8);
contexto.fillStyle = "#19765b";
contexto.fillRect(-40, -20, 80, 40);
contexto.restore();

El rectángulo se dibuja alrededor de su centro, que ahora está en (160, 90). Al restaurar el contexto recuperas la transformación y el estilo anteriores. El dibujo permanece en el lienzo.

Para dibujar un archivo de imagen, espera a que esté cargado antes de llamar a drawImage(). Este fragmento supone que ya tienes un contexto y un archivo paisaje.jpg en la misma carpeta.

const imagen = new Image();

imagen.addEventListener("load", () => {
  const ancho = 240;
  const alto = ancho * imagen.naturalHeight / imagen.naturalWidth;
  contexto.drawImage(imagen, 10, 10, ancho, alto);
});

imagen.addEventListener("error", () => {
  console.error("No se pudo cargar paisaje.jpg");
});

imagen.src = "paisaje.jpg";

Con drawImage() también puedes seleccionar un rectángulo de origen y pintarlo en otro rectángulo de destino. Es una técnica habitual para usar una hoja de sprites. Si dibujas una imagen de otro origen, necesitas autorización CORS de ese servidor para leer los píxeles o exportar el resultado. Cuando corresponda, configura imagen.crossOrigin = «anonymous» antes de asignar src.

13. Tamaño visible y nitidez del lienzo

En Canvas conviene distinguir dos tamaños:

  • Tamaño visible: el espacio que el lienzo ocupa en la página, medido en píxeles CSS.
  • Resolución interna: los píxeles disponibles para dibujar, controlados por canvas.width y canvas.height.

En una pantalla con devicePixelRatio = 2, un lienzo de 320 × 180 píxeles CSS puede usar un bitmap de 640 × 360 para representar bordes más nítidos. Si escalas el contexto en la misma proporción, puedes seguir programando el dibujo con coordenadas cercanas a los tamaños CSS.

Al asignar un nuevo valor a canvas.width o canvas.height, se limpia el dibujo y se restablece el estado del contexto. Por eso debes ajustar el tamaño cuando cambie, volver a establecer la transformación y redibujar. En una pantalla de alta densidad, limitar la escala puede ser un compromiso razonable entre nitidez y consumo de memoria.

14. Animación con requestAnimationFrame

requestAnimationFrame() solicita al navegador una llamada antes de un próximo repintado. Para mantener una animación, cada llamada solicita la siguiente mientras siga activa.

El callback recibe una marca de tiempo. Úsala para calcular los segundos transcurridos y multiplicarlos por la velocidad. Por ejemplo, a 140 píxeles por segundo, durante 0,02 segundos la figura avanza 2,8 píxeles. El movimiento depende así del tiempo y no de cuántos fotogramas dibuje el dispositivo.

Un ciclo habitual tiene tres pasos:

  1. Actualizar las posiciones a partir del tiempo transcurrido.
  2. Limpiar el fotograma anterior y dibujar el nuevo.
  3. Solicitar otro fotograma si la reproducción sigue activa.

15. Ejemplo completo de Canvas: una pelota animada

Guarda el siguiente código como animacion-canvas.html. La pelota empieza detenida y puedes reproducir, pausar o reiniciar la animación con botones. El lienzo conserva su proporción al cambiar el ancho de la ventana.

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pelota animada | Canvas 2D</title>
  <style>
    body {
      max-width: 42rem;
      margin: 2rem auto;
      padding-inline: 1rem;
      font: 1rem/1.6 system-ui, sans-serif;
      color: #17263a;
    }

    canvas {
      display: block;
      width: 100%;
      aspect-ratio: 16 / 9;
      background: #eef5ff;
      border-radius: 0.75rem;
    }

    .controles {
      display: flex;
      flex-wrap: wrap;
      gap: 0.75rem;
      margin-block: 1rem;
    }

    button {
      padding: 0.65rem 1rem;
      font: inherit;
    }

    button:focus-visible {
      outline: 3px solid #9a4400;
      outline-offset: 3px;
    }
  </style>
</head>
<body>
  <main>
    <h1>Pelota animada en Canvas</h1>

    <p id="descripcion">
      La pelota azul se mueve de izquierda a derecha y cambia de
      dirección al alcanzar un borde.
    </p>

    <canvas id="lienzo" width="640" height="360"
            role="img" aria-label="Pelota azul animada"
            aria-describedby="descripcion">
      Demostración de una pelota que rebota entre dos bordes.
    </canvas>

    <div class="controles">
      <button id="reproducir" type="button" disabled>Reproducir</button>
      <button id="reiniciar" type="button" disabled>Reiniciar</button>
    </div>

    <p id="estado" role="status">Animación detenida.</p>
  </main>

  <script>
    const lienzo = document.querySelector("#lienzo");
    const contexto = lienzo.getContext("2d");
    const reproducir = document.querySelector("#reproducir");
    const reiniciar = document.querySelector("#reiniciar");
    const estado = document.querySelector("#estado");

    if (contexto) {
      iniciarDemostracion();
    } else {
      estado.textContent = "El contexto de dibujo 2D no está disponible.";
    }

    function iniciarDemostracion() {
      const radio = 18;
      const rapidez = 140;
      let posicion = radio;
      let direccion = 1;
      let ancho = 0;
      let alto = 0;
      let fotograma = null;
      let tiempoAnterior = null;

      function ajustarLienzo() {
        const rectangulo = lienzo.getBoundingClientRect();
        ancho = rectangulo.width;
        alto = rectangulo.height;

        if (ancho === 0 || alto === 0) return;

        const escala = Math.min(window.devicePixelRatio || 1, 2);
        const anchoInterno = Math.max(1, Math.round(ancho * escala));
        const altoInterno = Math.max(1, Math.round(alto * escala));

        if (lienzo.width !== anchoInterno || lienzo.height !== altoInterno) {
          lienzo.width = anchoInterno;
          lienzo.height = altoInterno;
        }

        contexto.setTransform(
          anchoInterno / ancho, 0, 0, altoInterno / alto, 0, 0
        );
      }

      function dibujar() {
        contexto.clearRect(0, 0, ancho, alto);

        contexto.beginPath();
        contexto.arc(posicion, alto / 2, radio, 0, 2 * Math.PI);
        contexto.fillStyle = "#1767c0";
        contexto.fill();
      }

      function adaptarYDibujar() {
        ajustarLienzo();
        const limiteDerecho = Math.max(radio, ancho - radio);
        posicion = Math.min(posicion, limiteDerecho);
        dibujar();
      }

      function animar(tiempo) {
        if (tiempoAnterior !== null) {
          const segundos = Math.min((tiempo - tiempoAnterior) / 1000, 0.05);
          const limiteDerecho = Math.max(radio, ancho - radio);

          posicion += direccion * rapidez * segundos;

          if (posicion >= limiteDerecho) {
            posicion = limiteDerecho;
            direccion = -1;
          } else if (posicion <= radio) {
            posicion = radio;
            direccion = 1;
          }
        }

        tiempoAnterior = tiempo;
        dibujar();
        fotograma = requestAnimationFrame(animar);
      }

      function pausar() {
        if (fotograma !== null) {
          cancelAnimationFrame(fotograma);
        }

        fotograma = null;
        tiempoAnterior = null;
        reproducir.textContent = "Reproducir";
      }

      reproducir.addEventListener("click", () => {
        if (fotograma !== null) {
          pausar();
          estado.textContent = "Animación en pausa.";
          return;
        }

        reproducir.textContent = "Pausar";
        estado.textContent = "Animación en reproducción.";
        fotograma = requestAnimationFrame(animar);
      });

      reiniciar.addEventListener("click", () => {
        pausar();
        posicion = radio;
        direccion = 1;
        dibujar();
        estado.textContent = "Animación reiniciada y detenida.";
      });

      const observador = new ResizeObserver(adaptarYDibujar);
      observador.observe(lienzo);
      window.addEventListener("resize", adaptarYDibujar);

      adaptarYDibujar();
      reproducir.disabled = false;
      reiniciar.disabled = false;
    }
  </script>
</body>
</html>

Cómo funciona la animación

  • posicion guarda la coordenada horizontal de la pelota. La coordenada vertical se calcula como la mitad de la altura disponible.
  • direccion vale 1 cuando va hacia la derecha y −1 cuando va hacia la izquierda.
  • animar() actualiza la posición, comprueba los bordes y solicita el siguiente fotograma.
  • pausar() cancela la solicitud pendiente y limpia la referencia temporal para poder reanudar desde la misma posición.
  • ResizeObserver detecta cambios en el tamaño del lienzo, incluso si los provoca el contenedor.

El ejemplo limita el tiempo utilizado en cada actualización a 50 milisegundos. Si cambias de pestaña y luego vuelves, ese límite evita que todo el tiempo transcurrido se convierta en un salto de la pelota en un único fotograma.

16. Interacción con el puntero en Canvas

Un clic sobre Canvas llega al lienzo. Para saber si el usuario ha pulsado una figura, conviertes las coordenadas del evento a las de tu dibujo y haces una comprobación geométrica.

El siguiente fragmento usa coordenadas CSS, como el ejemplo anterior. Funciona con un lienzo sin borde ni relleno y una pelota cuyo centro y radio ya están definidos en ese mismo sistema de coordenadas.

lienzo.addEventListener("click", (evento) => {
  const rectangulo = lienzo.getBoundingClientRect();
  const x = evento.clientX - rectangulo.left;
  const y = evento.clientY - rectangulo.top;

  const distancia = Math.hypot(x - centroX, y - centroY);

  if (distancia <= radio) {
    console.log("Has pulsado dentro de la pelota.");
  }
});

Si tu escena utiliza otras coordenadas internas, tendrás que convertirlas según su escala o transformación. En una aplicación de dibujo, los Pointer Events permiten trabajar con ratón, lápiz y pantalla táctil mediante una misma familia de eventos. Para acciones como cambiar la herramienta, borrar o guardar, utiliza controles HTML accesibles mediante teclado.

17. Exportar una imagen y conocer otras API gráficas

Canvas puede generar un archivo a partir del dibujo. Este fragmento supone que ya existe un lienzo dibujado y un enlace HTML con id=»descargar». toBlob() crea el PNG de forma asíncrona. La URL del archivo se conserva mientras el enlace esté disponible y se libera al preparar una nueva exportación.

const enlace = document.querySelector("#descargar");
let urlAnterior = null;

function prepararDescarga() {
  lienzo.toBlob((archivo) => {
    if (!archivo) return;

    if (urlAnterior) {
      URL.revokeObjectURL(urlAnterior);
    }

    urlAnterior = URL.createObjectURL(archivo);
    enlace.href = urlAnterior;
    enlace.download = "dibujo.png";
    enlace.textContent = "Descargar dibujo como PNG";
  }, "image/png");
}

En proyectos más exigentes puedes encontrar estas herramientas:

  • Path2D: permite guardar un trazado para reutilizarlo en varias operaciones de dibujo.
  • OffscreenCanvas: permite dibujar en una superficie separada del documento. En configuraciones compatibles, puede utilizarse desde un Web Worker, que veremos en el próximo módulo.
  • WebGL y WebGPU: ofrecen otras formas de renderizar gráficos con la GPU. Utilizan contextos y modelos de programación distintos del contexto 2d.

Si estás optimizando una escena, mide primero dónde se emplea el tiempo. Una capa estática, un tamaño interno excesivo o miles de operaciones repetidas plantean problemas diferentes.

18. Cómo elegir entre SVG y Canvas

Proyecto Punto de partida Motivo
Icono o logotipo SVG. Escala y puede reutilizarse como archivo.
Plano con zonas seleccionables SVG. Puedes identificar y modificar sus elementos de forma individual.
Editor de dibujo libre Canvas. Permite pintar trazos y exportar la superficie como imagen.
Juego 2D con sprites Canvas. Facilita redibujar la escena según el estado del juego.
Gráfico con pocos datos y etiquetas SVG y tabla HTML. Combina una vista visual con valores que se pueden consultar como texto.
Simulación con muchos objetos animados Prototipo en Canvas. Permite medir el coste de redibujar la escena con datos representativos.

También puedes combinar las dos técnicas. Una aplicación podría usar Canvas para una simulación y SVG para los iconos. Los botones, resultados y mensajes pueden seguir formando parte del HTML.

19. Errores frecuentes y buenas prácticas

  • Un SVG se recorta o se deforma. Comprueba los límites del viewBox, su relación de aspecto y las dimensiones del contenedor.
  • Canvas se ve borroso. Compara el tamaño visible con la resolución interna. Ajusta el bitmap a la densidad de pantalla y redibuja.
  • El dibujo desaparece al redimensionar. Vuelve a configurar el contexto después de cambiar width o height y pinta la escena desde su estado actual.
  • Se ven rastros del fotograma anterior. Limpia la superficie con clearRect() o pinta un fondo antes de dibujar la nueva escena.
  • La velocidad depende de la pantalla. Calcula el movimiento a partir del tiempo transcurrido entre fotogramas.
  • Solo se distinguen los datos por el color. Añade valores, etiquetas o patrones. Incluye una tabla cuando sea útil consultar cifras exactas.
  • Una animación empieza sin intervención. Si la reproducción automática aporta algo al proyecto, respeta prefers-reduced-motion y ofrece controles. En el laboratorio, el usuario decide cuándo empezar.

20. Ejercicios prácticos

  1. Crea un icono SVG. Combina un rectángulo, un círculo y un trazado. Muéstralo a 24, 48 y 96 píxeles y comprueba que mantiene la proporción.
  2. Dibuja una casa. Usa <rect> para el cuerpo y <polygon> para el tejado. Agrupa la casa y desplázala sin cambiar las coordenadas de cada parte.
  3. Añade una semana al gráfico. Introduce un cuarto valor, calcula su altura y actualiza la tabla. Comprueba que coinciden el gráfico, los datos y su descripción.
  4. Dibuja un semáforo en Canvas. Utiliza un rectángulo para la carcasa y tres círculos. Acompaña el dibujo con un texto que indique la luz activa.
  5. Modifica la animación. Cambia la rapidez y añade un desplazamiento vertical. La pelota debe permanecer dentro de los cuatro bordes al mover y redimensionar el lienzo.

21. Ejercicios avanzados

Diagrama interactivo en SVG

Crea un plano con tres zonas. Añade un botón HTML por zona. Al pulsarlo, resalta su figura y muestra una descripción en un párrafo. Comprueba que puedes seleccionar las tres zonas con Tab y Enter.

Mini editor de dibujo en Canvas

Permite dibujar con el puntero, elegir un color y borrar el lienzo. Añade una opción de exportación a PNG. Como ampliación, guarda los trazos como una lista de puntos y vuelve a pintarlos después de redimensionar.

La misma escena con SVG y Canvas

Dibuja veinte círculos con ambas técnicas. Añade interacción para cambiar el color de un círculo. En SVG, modifica el elemento. En Canvas, localiza el círculo por coordenadas, cambia su estado y redibuja. Compara el código que necesita cada versión.

22. Conclusión

SVG resulta práctico cuando el gráfico está formado por elementos que quieres escalar, identificar o modificar por separado. Canvas encaja cuando tu programa necesita controlar cómo se dibuja y se actualiza una superficie. Los dos laboratorios muestran esa diferencia: una barra de SVG tiene su propio elemento, mientras que la pelota de Canvas existe como una posición que JavaScript actualiza y vuelve a pintar.

En el próximo módulo ampliaremos el papel de JavaScript en la página con geolocalización, arrastrar y soltar, almacenamiento web y comunicación en tiempo real.

Módulo anterior: 11. Layout y diseño de página.
Siguiente módulo: 13. APIs y funcionalidades avanzadas.