wandres.dev
ONTOLOGÍA · El mapa del gráfico 2D en la web

Los vecinos del mapa: WebGL, WebGPU y la Paint API

Delimitar el territorio del 2D situando lo que queda justo fuera de él, y saber cuándo un problema deja de ser de canvas o SVG y pasa a ser de otra cosa.

⏱ 16 min

Un mapa sin fronteras no orienta. Alrededor del canvas 2D y de SVG hay al menos cuatro tecnologías que dibujan y que la gente confunde con ellos: los contextos WebGL y WebGPU del mismo elemento <canvas>, la Paint API de CSS, los filtros y máscaras que CSS toma prestados de SVG, y el vídeo. Saber qué hace cada una evita las dos equivocaciones simétricas: usar la GPU para pintar cuatro rectángulos y quedarse en el 2D cuando el problema ya es tridimensional.

🎯 Al terminar esta lección sabrás
  • Distinguir los cuatro contextos que puede tener un elemento <canvas> y qué modelo ofrece cada uno.
  • Explicar por qué WebGL y WebGPU no sustituyen al canvas 2D en la mayoría de los casos.
  • Situar la Paint API de CSS y los filtros SVG usados desde CSS dentro del mapa.
  • Reconocer la señal concreta que indica que un problema 2D se ha convertido en un problema de GPU.

Un elemento, cuatro contextos

El elemento <canvas> no es “el canvas 2D”. Es una superficie de dibujo genérica a la que pides un contexto, y el contexto determina qué API tienes. Los identificadores válidos son cuatro.

const c = document.createElement('canvas');
c.getContext('2d');            // CanvasRenderingContext2D
c.getContext('webgl');         // WebGLRenderingContext (WebGL 1)
c.getContext('webgl2');        // WebGL2RenderingContext
c.getContext('webgpu');        // GPUCanvasContext
c.getContext('bitmaprenderer');// ImageBitmapRenderingContext

Son cinco identificadores en realidad, contando bitmaprenderer, que no dibuja nada: solo muestra un ImageBitmap que produjiste en otro sitio. Y hay una regla que sorprende a todo el mundo la primera vez: un canvas solo admite un tipo de contexto en toda su vida. Una vez que has pedido "2d", pedir "webgl" sobre el mismo elemento devuelve null para siempre. Si necesitas los dos modelos, necesitas dos elementos.

La diferencia entre el contexto 2D y los de GPU no es de potencia sino de nivel de abstracción. El contexto 2D te da primitivas con significado: rutas, texto, imágenes, degradados, composición. WebGL y WebGPU te dan triángulos, búferes y programas que se ejecutan por vértice y por píxel. Dibujar un rectángulo con esquinas redondeadas y un borde punteado son dos líneas en 2D y varios cientos en WebGL, porque en WebGL tienes que teselar la forma, escribir el sombreador que decide el color de cada fragmento y montar la maquinaria de estado.

Cuándo el 2D deja de bastar

Hay tres señales claras, y ninguna es “quiero que vaya rápido”.

La primera es el cálculo por píxel. Si lo que quieres es aplicar una transformación a cada píxel de la imagen —un desenfoque grande, una distorsión, un efecto de iluminación, un mezclado de varias texturas— el 2D te obliga a hacerlo en JavaScript sobre un array, píxel a píxel, en el hilo principal. En GPU eso es un sombreador de fragmentos que se ejecuta en paralelo sobre millones de píxeles. La diferencia no es de dos veces: es de dos o tres órdenes de magnitud.

La segunda es la tercera dimensión de verdad: perspectiva, profundidad con búfer de profundidad, iluminación con normales, cámaras. Se puede simular una pseudoperspectiva en 2D proyectando a mano, y para un efecto aislado compensa; para una escena, no.

La tercera es la cantidad de instancias con estado propio. Cien mil partículas con su posición, velocidad y color, actualizadas cada fotograma, son un caso claro de cómputo en GPU: los datos ya están en memoria de vídeo y no tienen por qué volver.

Fuera de esas tres, el 2D es la respuesta, y no por comodidad: los navegadores modernos aceleran el canvas 2D por GPU internamente. Cuando rellenas un rectángulo, hay bastantes posibilidades de que eso acabe siendo una llamada de dibujo en la GPU igualmente, solo que sin que tú hayas tenido que escribirla.

La aceleración por GPU del canvas 2D no es tuya, y por eso desaparece

Que el canvas 2D se acelere por hardware es cierto y es lo que hace que sea viable, pero es una decisión del navegador, no un contrato. El motor mantiene heurísticas para decidir si un canvas concreto vive en GPU o en memoria del sistema, y cambia de opinión sobre la marcha. Hay varias cosas que provocan esa degradación silenciosa: pedir el contexto con willReadFrequently, leer píxeles muy a menudo, superar el presupuesto de memoria de vídeo cuando tienes muchos canvas vivos a la vez, canvas demasiado grandes, o ciertos modos de composición y filtros que el backend no puede resolver en GPU. Y no hay ninguna API que te diga en qué modo estás. El síntoma es siempre el mismo y siempre desconcierta: el mismo código que iba a sesenta fotogramas por segundo en una pestaña va a doce en otra sin que hayas cambiado nada. Cuando eso pase, la primera hipótesis no debe ser tu algoritmo: debe ser que perdiste la aceleración. Y la forma de comprobarlo es reducir el número de canvas vivos y quitar las lecturas de píxeles, en ese orden.

La Paint API y los filtros que CSS toma de SVG

Hay dos puentes entre los modelos que conviene tener en el mapa porque resuelven casos concretos muy bien.

El primero es la CSS Painting API, parte de Houdini. Permite registrar un módulo de trabajo con una clase que tiene un método paint(ctx, size, props), donde ctx es un contexto 2D restringido, y usar el resultado como valor de background-image mediante paint(). La ventaja es que ese dibujo se comporta como CSS: se recalcula cuando cambia el tamaño del elemento, participa en la cascada, lee propiedades personalizadas. La desventaja es el soporte: es Chromium desde hace años y sigue sin estar en los otros motores, así que solo sirve como mejora progresiva sobre un fondo de reserva.

// paint-rayas.js, registrado con CSS.paintWorklet.addModule('paint-rayas.js')
registerPaint('rayas', class {
  static get inputProperties() { return ['--raya-color', '--raya-ancho']; }
  paint(ctx, size, props) {
    const color = props.get('--raya-color').toString().trim() || '#89b4fa';
    const ancho = parseFloat(props.get('--raya-ancho')) || 8;
    ctx.fillStyle = color;
    for (let x = -size.height; x < size.width; x += ancho * 2) {
      ctx.beginPath();
      ctx.moveTo(x, size.height);
      ctx.lineTo(x + size.height, 0);
      ctx.lineTo(x + size.height + ancho, 0);
      ctx.lineTo(x + ancho, size.height);
      ctx.fill();
    }
  }
});

El segundo puente va en la otra dirección: CSS puede usar recursos SVG. Las propiedades filter, clip-path y mask aceptan una referencia a un elemento SVG del documento, y con eso obtienes desde CSS toda la potencia del pipeline de filtros de SVG sin salir del modelo de cajas. Es el atajo que resuelve una gran parte de los efectos que la gente intenta hacer a mano en canvas.

.foto { filter: url(#papel); clip-path: url(#recorte); }

El resto de los vecinos

Quedan dos que se confunden menos pero conviene nombrar.

El vídeo es un modelo aparte: un flujo de fotogramas decodificado por hardware que el compositor pinta directamente. Su relación con el canvas es que un <video> es una fuente válida para drawImage, lo que convierte al canvas en la herramienta estándar para analizar o procesar fotogramas de vídeo y de cámara. Esa es la puerta de entrada a la visión por computador en el navegador.

Las fuentes tipográficas son, técnicamente, gráficos vectoriales con un motor de rasterización propio y muy afinado, con sugerencias de rejilla y suavizado subpíxel que ni SVG ni canvas replican. Es la razón de que el texto renderizado por el navegador se vea mejor que el mismo texto dibujado a mano sobre una rejilla, y un argumento serio para no meter texto largo dentro de un canvas.

Con eso el mapa está cerrado: en el centro los tres modelos, y alrededor la GPU, la Paint API, los filtros SVG usados desde CSS, el vídeo y el texto. Todo lo demás son librerías construidas encima de alguno de ellos.