La propiedad font y los ajustes tipográficos del contexto
Escribir la especificación de fuente con la sintaxis exacta que el canvas acepta, y usar las propiedades adicionales de tipografía que el contexto ofrece.
ctx.font usa la sintaxis abreviada de la propiedad font de CSS, con una regla que rompe a mucha gente: el tamaño y la familia son obligatorios, y una especificación inválida se ignora en silencio dejando la anterior. Alrededor de esa propiedad hay media docena de ajustes tipográficos que el contexto ha ido incorporando y que resuelven cosas que antes había que hacer glifo a glifo.
- Escribir especificaciones de fuente válidas con todos sus componentes en el orden correcto.
- Detectar una especificación inválida y sus consecuencias.
- Usar
letterSpacing,wordSpacingydirectioncon criterio. - Cargar y usar una fuente propia dentro del canvas, incluido en un worker.
La sintaxis
La forma general es: estilo opcional, variante opcional, peso opcional, estirado opcional, después el tamaño con una barra y la altura de línea si quieres, y por último la familia.
El tamaño y la familia son obligatorios; el resto es opcional pero, si aparece, tiene que ir antes del tamaño.
ctx.font = '16px sans-serif';
ctx.font = 'bold 24px Georgia, serif';
ctx.font = 'italic 600 18px "Fira Sans", system-ui';
ctx.font = 'italic small-caps bold condensed 20px/1.4 Inter, sans-serif';
La altura de línea se acepta en la sintaxis pero el canvas la ignora por completo, porque no hay concepto de línea. Ponerla no hace daño ni ayuda.
Las unidades de tamaño admitidas son las de CSS. Las relativas como em y rem se resuelven contra el estilo calculado del elemento canvas, lo que hace que dependan del tamaño de fuente heredado. Es una fuente de sorpresas; usa px salvo que sepas exactamente qué estás haciendo.
La asignación inválida
Esta es la regla que hay que memorizar: si la cadena no es una especificación de fuente válida, la asignación se ignora y ctx.font conserva su valor anterior. No hay error, no hay excepción, no hay aviso.
ctx.font = '20px system-ui';
ctx.font = 'Arial'; // INVALIDA: falta el tamano
console.log(ctx.font); // sigue siendo '20px system-ui'
ctx.font = '20 px system-ui'; // INVALIDA: espacio entre numero y unidad
ctx.font = 'bold system-ui'; // INVALIDA: falta el tamano
El diagnóstico es una línea:
function asignarFuente(ctx, especificacion) {
const antes = ctx.font;
ctx.font = especificacion;
if (ctx.font === antes && especificacion !== antes) {
console.warn('Especificación de fuente rechazada:', especificacion);
}
}
Merece la pena en desarrollo, porque el síntoma de una fuente rechazada —texto con el tamaño de la figura anterior— es difícil de atribuir.
Otro detalle: leer ctx.font devuelve una forma normalizada, no la cadena que escribiste. No compares cadenas de fuente esperando igualdad exacta entre navegadores.
Los ajustes adicionales
El contexto 2D ha ido incorporando propiedades que antes no existían y que resuelven problemas concretos.
letterSpacing y wordSpacing aceptan una longitud CSS y aplican espaciado adicional entre caracteres y entre palabras. Antes de existir, conseguir texto espaciado exigía dibujar carácter a carácter midiendo cada uno, lo que además rompía el kerning.
ctx.font = '600 22px system-ui';
ctx.letterSpacing = '3px';
ctx.fillText('ESPACIADO', 20, 40);
ctx.letterSpacing = '0px'; // restaurar
Son propiedades relativamente recientes; no están en todas las versiones que puedas encontrarte en el parque de dispositivos. La detección es directa:
const soportaEspaciado = 'letterSpacing' in ctx;
fontKerning acepta 'auto', 'normal' y 'none'. Desactivar el kerning rara vez interesa salvo para depurar mediciones.
fontStretch, fontVariantCaps y textRendering exponen al canvas las mismas opciones que sus equivalentes de CSS. textRendering con valor 'geometricPrecision' es útil cuando escalas texto: pide al motor que no ajuste las posiciones a la rejilla, con lo que el texto se comporta bien al interpolar tamaños, a costa de verse menos nítido a tamaños pequeños.
direction acepta 'ltr', 'rtl' e 'inherit'. Afecta a la dirección del texto y también al significado de los valores 'start' y 'end' de textAlign.
Todas estas propiedades forman parte del estado y las guarda save.
Cargar una fuente propia
En el hilo principal, la forma correcta es declararla con @font-face en CSS y esperar a que cargue:
await document.fonts.load('700 32px "Mi Fuente"');
ctx.font = '700 32px "Mi Fuente"';
También se puede construir programáticamente, lo que evita depender del CSS y da control sobre cuándo se descarga:
const cara = new FontFace('Mi Fuente', 'url(/fuentes/mifuente.woff2)', {
weight: '400 700', // rango, para una fuente variable
style: 'normal',
});
await cara.load();
document.fonts.add(cara);
ctx.font = '600 32px "Mi Fuente"';
Dentro de un worker, document no existe, pero el conjunto de fuentes sí: se accede como self.fonts y funciona igual.
// dentro de un worker
const cara = new FontFace('Mi Fuente', 'url(/fuentes/mifuente.woff2)');
await cara.load();
self.fonts.add(cara);
ctx.font = '16px "Mi Fuente"';
Ese detalle es imprescindible en cuanto muevas el dibujo a un worker: las fuentes cargadas en el documento no están disponibles allí y hay que cargarlas otra vez en el contexto del worker.
Una fuente variable expone ejes continuos —peso, anchura, inclinación óptica— y en CSS se controlan con font-variation-settings. En el canvas no existe esa propiedad, y ahí se acaba la mayoría de las explicaciones. Pero sí existen las propiedades que se mapean a ejes estándar: el peso dentro de ctx.font mapea al eje de peso, y fontStretch mapea al eje de anchura. Es decir, ctx.font = '350 24px "Inter Variable"' funciona y produce un peso de 350, un valor que ninguna fuente estática ofrece. Eso permite animar el peso de un texto en canvas de forma continua, que es un efecto que en CSS es trivial y en canvas parecía imposible. Para los ejes no estándar —una fuente con un eje de redondez o de grado— no hay camino directo desde la API del contexto, y la técnica que funciona es registrar varias instancias con new FontFace pasando los ajustes de variación en el diccionario de descriptores, cada una con un nombre de familia distinto, y alternar entre ellas desde ctx.font. Es aparatoso y es la única vía. Y hay un aviso de rendimiento que casi nadie da: cambiar el valor de un eje variable invalida la caché de glifos rasterizados del navegador. Animar el peso de un texto grande fotograma a fotograma obliga a rerasterizar todos los glifos en cada valor intermedio, y eso es carísimo. Si el efecto es decorativo, limita el número de valores distintos a una docena y redondea a ellos; la diferencia visual es imperceptible y la de rendimiento es de un orden de magnitud.
Detectar si una fuente está disponible
No hay API para preguntar si una fuente existe, pero hay una técnica fiable basada en medir: si el ancho de un texto con la fuente pedida coincide exactamente con el de la genérica de reserva, la fuente no se aplicó.
function fuenteDisponible(ctx, familia, muestra = 'mmmmmmmmmmlliWWWW') {
const anterior = ctx.font;
const medir = f => { ctx.font = `72px ${f}`; return ctx.measureText(muestra).width; };
const base = { monospace: medir('monospace'), 'sans-serif': medir('sans-serif'),
serif: medir('serif') };
const resultado = Object.entries(base).some(([generica, ancho]) => {
ctx.font = `72px "${familia}", ${generica}`;
return ctx.measureText(muestra).width !== ancho;
});
ctx.font = anterior;
return resultado;
}
La cadena de muestra está elegida para maximizar la diferencia entre fuentes: mezcla caracteres muy anchos y muy estrechos. El tamaño grande amplifica las diferencias por encima del ruido de redondeo. Es la misma técnica que usan las bibliotecas de detección de fuentes desde hace quince años y sigue siendo la única que funciona.