measureText y todas sus métricas
Usar el objeto TextMetrics más allá de width: los ascensos y descensos reales, los de la caja de fuente, y para qué sirve exactamente cada uno.
Las dos familias
const m = ctx.measureText('Hpgjy');El objeto devuelto contiene, agrupadas por lo que describen:
El avance. width es la distancia horizontal que el cursor avanzaría al escribir ese texto. Es la métrica que usa el motor para colocar el texto siguiente.
La tinta real, lo que ocupa este texto concreto con esta fuente:
actualBoundingBoxLeft: distancia desde el punto de anclaje hasta el borde izquierdo de la tinta, positiva hacia la izquierda.actualBoundingBoxRight: hasta el borde derecho, positiva hacia la derecha.actualBoundingBoxAscent: desde la línea base hasta el punto más alto de la tinta, positiva hacia arriba.actualBoundingBoxDescent: desde la línea base hasta el punto más bajo, positiva hacia abajo.
La caja de la fuente, lo que la fuente reserva independientemente del texto:
fontBoundingBoxAscentyfontBoundingBoxDescent: los extremos de la caja de la fuente respecto a la línea base.
Y un tercer grupo, con soporte más irregular entre motores: emHeightAscent, emHeightDescent, hangingBaseline, alphabeticBaseline e ideographicBaseline, que dan las posiciones de las líneas base alternativas y los extremos de la caja em. Están en la especificación pero no siempre implementadas; compruébalas antes de depender de ellas.
width no es el ancho de la tinta
La diferencia entre width y la suma de las dos métricas laterales reales es real y a veces grande.
width es la suma de los avances de los glifos, ajustada por kerning. Es lo que ocupa el texto en el flujo.
La caja real es lo que ocupan los píxeles pintados. Puede ser mayor que el avance, cuando hay glifos que sobresalen de su caja de avance —una f cursiva, cualquier tipografía caligráfica— y puede ser menor, cuando el último glifo tiene espacio lateral por la derecha, que es lo habitual.
ctx.font = 'italic 64px Georgia, serif';const m = ctx.measureText('fj');console.log('avance:', m.width);console.log('tinta:', m.actualBoundingBoxLeft + m.actualBoundingBoxRight);// En muchas fuentes la tinta es MAS ancha que el avanceEsa diferencia explica un fallo muy concreto: dibujas un rectángulo de fondo de anchura m.width detrás del texto y el texto se sale por los lados. Para eso hay que usar la caja real, no el avance.
La caja envolvente exacta
Con las cuatro métricas de la caja real se obtiene el rectángulo que encierra exactamente la tinta, relativo al punto de anclaje:
function cajaDeTexto(ctx, texto, x, y) { const m = ctx.measureText(texto); return { x: x - m.actualBoundingBoxLeft, y: y - m.actualBoundingBoxAscent, ancho: m.actualBoundingBoxLeft + m.actualBoundingBoxRight, alto: m.actualBoundingBoxAscent + m.actualBoundingBoxDescent, };}Esa función es correcta con cualquier textAlign y textBaseline, porque las métricas de caja real ya están medidas respecto al punto de anclaje tal como lo interpretan esas propiedades. Es un detalle importante y poco conocido: las métricas cambian cuando cambias textAlign.
ctx.textAlign = 'left';console.log(ctx.measureText('Hola').actualBoundingBoxLeft); // proximo a 0ctx.textAlign = 'center';console.log(ctx.measureText('Hola').actualBoundingBoxLeft); // la mitad del anchoEso significa que puedes medir con la alineación que vas a usar y las coordenadas te salen directamente, sin corregir nada.
// Fondo ajustado detras de una etiqueta centradactx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.font = '600 20px system-ui';const caja = cajaDeTexto(ctx, 'Etiqueta', 200, 100);const p = 8;ctx.fillStyle = 'rgba(137,180,250,0.2)';ctx.beginPath();ctx.roundRect(caja.x - p, caja.y - p, caja.ancho + p * 2, caja.alto + p * 2, 6);ctx.fill();ctx.fillStyle = '#cdd6f4';ctx.fillText('Etiqueta', 200, 100);Reales frente a de fuente: cuándo cada una
La elección entre las dos familias tiene una regla clara y es la conclusión práctica de la lección.
Usa las métricas reales cuando la caja deba ajustarse a este texto: un fondo que envuelve una etiqueta, un rectángulo de selección, un recorte, la caja envolvente para hit testing, un centrado óptico.
Usa las métricas de fuente cuando la caja deba ser consistente entre textos distintos: la altura de una fila de tabla, el alto de línea de un párrafo, la altura de un botón, el espaciado vertical de una lista de etiquetas.
// Alto de linea consistente, independiente del contenidofunction altoDeLinea(ctx) { const m = ctx.measureText('Hxg'); return m.fontBoundingBoxAscent + m.fontBoundingBoxDescent;}El error de mezclarlas produce un síntoma característico: una lista donde las filas tienen alturas distintas según si su texto lleva tildes o descendentes. La fila con “Página” es más alta que la de “TOTAL”, y eso es feo y no se atribuye fácilmente.
Una caché de medición
function creadorDeMedidor(ctx) { const cache = new Map(); const MAX = 5000; return function medir(texto) { const clave = ctx.font + ' ' + ctx.textAlign + ' ' + texto; let m = cache.get(clave); if (m === undefined) { const t = ctx.measureText(texto); m = { width: t.width, izq: t.actualBoundingBoxLeft, der: t.actualBoundingBoxRight, asc: t.actualBoundingBoxAscent, desc: t.actualBoundingBoxDescent, fAsc: t.fontBoundingBoxAscent, fDesc: t.fontBoundingBoxDescent, }; if (cache.size >= MAX) cache.clear(); cache.set(clave, m); } return m; };}Dos decisiones de esa caché. La clave incluye la fuente y la alineación porque las métricas dependen de ambas; olvidar cualquiera de las dos produce medidas erróneas que aparecen solo cuando cambias de estilo. Y se copian los valores en un objeto plano en lugar de guardar el TextMetrics, porque ese objeto puede ser pesado y porque los objetos planos son mucho más rápidos de leer en un bucle caliente.