fillStyle: colores, gradientes y patrones
Dominar los tres tipos de pintura del canvas, sus sistemas de coordenadas y el error de rendimiento que comete todo el mundo con los gradientes.
Colores
La cadena admite cualquier sintaxis de color de CSS que el navegador entienda: nombres, hexadecimal, rgb(), hsl(), oklch(), color(). La conversión ocurre al asignar.
Un detalle útil: leer fillStyle después de asignarlo devuelve una forma normalizada, no la cadena que escribiste. Eso permite comprobar si un color era válido, porque una cadena inválida se ignora y la propiedad conserva el valor anterior.
ctx.fillStyle = '#89b4fa';console.log(ctx.fillStyle); // '#89b4fa'ctx.fillStyle = 'no-es-un-color';console.log(ctx.fillStyle); // sigue siendo '#89b4fa'Esa normalización tiene un uso práctico: convertir cualquier notación de color a un formato manejable sin escribir un parser.
Gradientes: cuatro constructores
ctx.createLinearGradient(x0, y0, x1, y1);ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);ctx.createConicGradient(anguloInicio, cx, cy);Los tres devuelven un CanvasGradient al que se le añaden paradas de color con addColorStop(posicion, color), donde la posición va de 0 a 1.
El lineal interpola entre dos puntos. La dirección del gradiente es la recta que los une; perpendicularmente el color es constante.
El radial interpola entre dos círculos, no entre dos radios del mismo círculo. Eso permite gradientes descentrados, que es lo que da el aspecto de esfera iluminada:
const g = ctx.createRadialGradient(90, 80, 5, 110, 110, 90);g.addColorStop(0, '#f9e2af');g.addColorStop(0.5, '#fab387');g.addColorStop(1, '#11111b');ctx.fillStyle = g;ctx.beginPath();ctx.arc(110, 110, 90, 0, Math.PI * 2);ctx.fill();El cónico interpola alrededor de un punto según el ángulo. El ángulo inicial va en radianes y el cero apunta a la derecha, como en arc. Es lo que se usa para ruedas de color y para indicadores circulares con degradado.
Fuera del rango de las paradas, el gradiente extiende el color de la parada extrema. No se repite ni se desvanece.
El sistema de coordenadas de los gradientes
Aquí está el desconcierto clásico. Las coordenadas de un gradiente están en el espacio de usuario del contexto en el momento de pintar, no en el de la forma que se rellena.
Eso significa que si creas un gradiente de (0, 0) a (100, 0) y lo usas para rellenar un rectángulo situado en x = 300, el rectángulo saldrá de un color plano, porque a partir de x = 100 el gradiente extiende su última parada.
// Mal: el gradiente esta en el origen, la forma esta lejosconst g = ctx.createLinearGradient(0, 0, 100, 0);g.addColorStop(0, '#89b4fa'); g.addColorStop(1, '#f38ba8');ctx.fillStyle = g;ctx.fillRect(300, 20, 100, 60); // sale rosa plano// Bien: coordenadas del gradiente relativas a la formaconst g2 = ctx.createLinearGradient(300, 0, 400, 0);g2.addColorStop(0, '#89b4fa'); g2.addColorStop(1, '#f38ba8');ctx.fillStyle = g2;ctx.fillRect(300, 20, 100, 60); // degradado correctoHay una alternativa mucho más limpia: crear el gradiente siempre en el origen y trasladar el contexto. Como las coordenadas del gradiente se interpretan con la matriz activa, un translate mueve el gradiente con la forma.
const g = ctx.createLinearGradient(0, 0, 100, 0);g.addColorStop(0, '#89b4fa'); g.addColorStop(1, '#f38ba8');function caja(x, y) { ctx.save(); ctx.translate(x, y); ctx.fillStyle = g; // el MISMO gradiente, reutilizado ctx.fillRect(0, 0, 100, 60); ctx.restore();}caja(20, 20); caja(160, 20); caja(300, 20);Ese patrón resuelve el problema de las coordenadas y el de la reutilización a la vez, y es la forma correcta de usar gradientes en una escena con muchos elementos.
Patrones
const patron = ctx.createPattern(fuente, repeticion);La fuente puede ser una imagen, un canvas, un ImageBitmap, un vídeo o un OffscreenCanvas. La repetición admite 'repeat', 'repeat-x', 'repeat-y' y 'no-repeat'.
Si la fuente es una imagen que aún no ha cargado, createPattern devuelve null. Hay que esperar.
Los patrones también viven en el espacio del contexto y arrancan en el origen. Pero tienen algo que los gradientes no: una matriz propia, que se ajusta con setTransform y admite un DOMMatrix.
const patron = ctx.createPattern(textura, 'repeat');patron.setTransform(new DOMMatrix().translate(20, 20).rotate(30).scale(0.5));ctx.fillStyle = patron;ctx.fillRect(0, 0, 400, 300);Esa matriz se compone con la del contexto, lo que permite mover, girar y escalar el patrón sin mover la forma. Es la forma correcta de hacer un fondo texturado que se desplaza con el scroll o un rayado que gira.
Un patrón generado a partir de un canvas pequeño es la forma más eficiente de rellenar áreas grandes con una textura procedural:
function patronRayas(ctx, colorA, colorB, ancho = 8) { const t = document.createElement('canvas'); t.width = t.height = ancho * 2; const c = t.getContext('2d'); c.fillStyle = colorA; c.fillRect(0, 0, ancho * 2, ancho * 2); c.strokeStyle = colorB; c.lineWidth = ancho; c.beginPath(); c.moveTo(-ancho, ancho); c.lineTo(ancho, -ancho); c.moveTo(0, ancho * 2); c.lineTo(ancho * 2, 0); c.moveTo(ancho, ancho * 3); c.lineTo(ancho * 3, ancho); c.stroke(); return ctx.createPattern(t, 'repeat');}Una utilidad de caché
Para gradientes que dependen de parámetros, esta caché resuelve el problema sin ensuciar el código:
function creadorDeGradientes(ctx) { const cache = new Map(); return function gradiente(clave, construir) { let g = cache.get(clave); if (!g) { g = construir(ctx); cache.set(clave, g); } return g; };}const grad = creadorDeGradientes(ctx);function pintarBarra(x, y, w, h, color) { ctx.save(); ctx.translate(x, y); ctx.fillStyle = grad(`barra:${color}:${h}`, c => { const g = c.createLinearGradient(0, 0, 0, h); g.addColorStop(0, color); g.addColorStop(1, '#11111b'); return g; }); ctx.fillRect(0, 0, w, h); ctx.restore();}La clave incluye todo lo que afecta al gradiente. Si el canvas cambia de tamaño y los gradientes dependen de la altura, hay que vaciar la caché en el redimensionado, o incluir la altura en la clave como hace el ejemplo.