textAlign y textBaseline: qué significa cada línea
Controlar el punto de anclaje del texto en los dos ejes, entender las líneas base tipográficas y elegir la correcta para cada caso.
textAlign: el eje horizontal
Cinco valores:
start(por defecto): el borde inicial según la dirección del texto. Con dirección de izquierda a derecha equivale aleft.end: el borde final. Con esa misma dirección equivale aright.leftyright: absolutos, ignoran la dirección.center: el punto es el centro horizontal del texto.
La distinción entre start/end y left/right importa en interfaces multilingües. Con ctx.direction = 'rtl', un textAlign = 'start' alinea a la derecha, que es lo correcto para árabe o hebreo. left seguiría alineando a la izquierda.
ctx.font = '20px system-ui';ctx.fillStyle = '#89b4fa';ctx.strokeStyle = '#f38ba8'; ctx.lineWidth = 1;ctx.beginPath(); ctx.moveTo(200.5, 10); ctx.lineTo(200.5, 130); ctx.stroke();['left', 'center', 'right'].forEach((a, i) => { ctx.textAlign = a; ctx.fillText(`texto ${a}`, 200, 40 + i * 34);});textAlign es la forma correcta de centrar texto. La alternativa —medir el ancho y restar la mitad— funciona, es más código y da resultados ligeramente distintos porque el ancho medido no siempre coincide con el ancho de avance que usa el motor de alineación.
textBaseline: las líneas
Aquí hay más sustancia, porque las líneas base no son invención del canvas sino conceptos tipográficos con significado real.
alphabetic (por defecto): la línea sobre la que se apoyan las letras latinas, griegas y cirílicas. Es la línea base de verdad en la tipografía occidental. Las partes descendentes caen por debajo.
ideographic: la línea base de los ideogramas del este asiático, que se apoyan un poco más abajo que la alfabética.
hanging: la línea base colgante, usada en escrituras índicas como el devanagari, donde los caracteres cuelgan de una línea superior.
top: el borde superior de la caja de la fuente, definida por las métricas del fichero de fuente.
bottom: el borde inferior de esa misma caja.
middle: el punto medio de la caja em, aproximadamente a media altura de la x, no a la mitad de la caja completa.
const lineas = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];ctx.font = '28px Georgia, serif';lineas.forEach((base, i) => { const y = 40 + i * 44; ctx.strokeStyle = '#f38ba8'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(20, y + 0.5); ctx.lineTo(400, y + 0.5); ctx.stroke(); ctx.textBaseline = base; ctx.fillStyle = '#cdd6f4'; ctx.fillText('Hpgjéx 漢字', 30, y);});Ejecutar ese fragmento y ver dónde cae la línea roja respecto al texto vale más que cualquier descripción.
Centrar verticalmente: el problema real
La tarea más común es centrar texto dentro de una caja, y la solución obvia —textBaseline = 'middle' con la Y en el centro de la caja— es la correcta la mayoría de las veces y produce un resultado ligeramente alto en algunos casos.
La razón es que middle se define respecto a la caja de la fuente, que reserva espacio para los ascendentes y descendentes máximos que la fuente pueda tener, no respecto al texto concreto que estás dibujando. Una cadena sin descendentes parece desplazada hacia arriba, porque se ha reservado espacio debajo para descendentes que no existen.
Cuando el centrado tiene que ser óptico y no métrico, hay que usar las métricas reales del texto:
function centrarOptico(ctx, texto, cx, cy) { const m = ctx.measureText(texto); const alto = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent; const y = cy + alto / 2 - m.actualBoundingBoxDescent; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText(texto, cx, y);}Esa versión centra la tinta real. Tiene una contrapartida importante: el resultado depende del contenido, así que dos etiquetas en la misma fila con contenidos distintos quedarán a alturas ligeramente distintas. Para etiquetas de una tabla o de un eje, eso es peor que el desajuste que corrige.
| Situación | Estrategia |
|---|---|
| Etiquetas en fila, contenido variable | middle, métrico, consistente entre etiquetas |
| Un texto centrado en un botón o círculo | middle está bien; óptico si se nota |
| Un título grande y aislado | Óptico, la diferencia se ve |
| Texto sobre una línea base compartida | alphabetic, siempre |
Un ejemplo con varios tamaños
<!doctype html><meta charset="utf-8"><canvas id="c" width="560" height="220" style="background:#1e1e2e"></canvas><script> const ctx = document.getElementById('c').getContext('2d'); function metrica(x, yBase, valor, unidad, etiqueta) { ctx.textBaseline = 'alphabetic'; ctx.textAlign = 'left'; ctx.font = '700 52px system-ui'; ctx.fillStyle = '#cdd6f4'; ctx.fillText(valor, x, yBase); const anchoValor = ctx.measureText(valor).width; ctx.font = '400 20px system-ui'; ctx.fillStyle = '#89b4fa'; ctx.fillText(unidad, x + anchoValor + 6, yBase); ctx.font = '400 14px system-ui'; ctx.fillStyle = '#7f849c'; ctx.fillText(etiqueta, x, yBase + 24); } metrica(40, 90, '1.284', 'req/s', 'Peticiones por segundo'); metrica(300, 90, '99,7', '%', 'Disponibilidad'); ctx.strokeStyle = '#45475a'; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(20, 90.5); ctx.lineTo(540, 90.5); ctx.stroke(); ctx.setLineDash([]);</script>La línea discontinua muestra el invariante: los cuatro fragmentos de texto, con dos tamaños y dos pesos, se apoyan sobre la misma recta. Cambiar cualquiera de ellos a middle rompería la alineación de inmediato.
Interacción con la dirección
Una última nota. ctx.direction afecta a start y end de textAlign, y también al orden visual de los caracteres en textos bidireccionales. El canvas aplica el algoritmo bidireccional de Unicode al dibujar, así que una cadena mixta de árabe y latín se reordena correctamente. Lo que no puede hacer es reordenar entre llamadas distintas: si dibujas la cadena en fragmentos, cada fragmento se reordena por separado y el resultado es incorrecto. Para texto bidireccional, dibuja la cadena completa en una sola llamada.