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.
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 tamanoconsole.log(ctx.font); // sigue siendo '20px system-ui'ctx.font = '20 px system-ui'; // INVALIDA: espacio entre numero y unidadctx.font = 'bold system-ui'; // INVALIDA: falta el tamanoEl 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'; // restaurarSon 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 workerconst 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.
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.