Saltar al contenido

textPath: texto sobre una ruta

Cómo se proyecta el texto sobre una curva, startOffset y su relación con pathLength, el lado de la ruta, y por qué el texto en un arco cerrado sale del revés en la mitad inferior.

La proyección

textPath va dentro de un text y referencia una ruta con href:

HTML
<svg viewBox="0 0 300 120" width="600">  <defs>    <path id="onda" d="M 20 80 Q 90 20 150 70 T 280 60" fill="none" />  </defs>  <path d="M 20 80 Q 90 20 150 70 T 280 60" fill="none"        stroke="#45475a" stroke-dasharray="3 3" />  <text font-size="20" fill="#cdd6f4" font-family="system-ui, sans-serif">    <textPath href="#onda">Texto que sigue la curva</textPath>  </text></svg>

Fíjate en tres cosas del marcado.

La ruta de referencia está en defs y no se pinta. La copia visible es otro elemento con la misma d. Si quisieras que la ruta se viera, podrías dejarla fuera de defs con un stroke, y el textPath la referenciaría igual.

El text no lleva x ni y: la posición la da la ruta. Si pones x e y, se ignoran para la componente que la ruta controla.

El texto se coloca recorriendo la ruta desde su inicio. Cada glifo se sitúa en el punto correspondiente a su avance acumulado y se rota según la tangente en ese punto. Esa rotación es lo que hace que el texto parezca doblarse.

La ruta puede ser cualquiera: una recta (lo que equivale a un texto normal rotado), un arco, una espiral. Solo tiene que ser un elemento de geometría al que se pueda apuntar.

startOffset

startOffset decide dónde, a lo largo de la ruta, empieza el texto. Acepta una longitud en unidades de usuario o un porcentaje de la longitud total de la ruta.

HTML
<textPath href="#onda" startOffset="50%">Centrado</textPath>

Combinado con text-anchor="middle" en el text, el startOffset pasa a ser el punto medio del texto, no su inicio. Esa es la combinación para centrar un rótulo en un arco:

HTML
<text font-size="18" text-anchor="middle">  <textPath href="#arco" startOffset="50%">Centrado en el arco</textPath></text>

Y aquí conecta con algo del nivel 17: si la ruta lleva pathLength="100", el startOffset en unidades pasa a estar en esa escala ficticia, así que startOffset="25" es el 25 por ciento sin escribir el símbolo. Es útil cuando generas las posiciones desde datos y prefieres trabajar con números.

Un startOffset negativo o mayor que la longitud coloca el texto fuera de la ruta, donde no se dibuja. No es un error, simplemente no se ve.

El texto del revés

El caso clásico: un sello circular con texto arriba y abajo. El texto de arriba sale bien; el de abajo sale invertido y leyéndose de derecha a izquierda desde dentro del círculo.

No es un fallo: el texto sigue la tangente, y en la mitad inferior de un círculo recorrido en sentido horario la tangente apunta hacia la izquierda. Las letras están correctamente orientadas respecto a la curva; lo que pasa es que la curva va hacia allá.

Hay dos soluciones.

Una: usar dos rutas con sentidos opuestos. La de arriba en sentido horario, la de abajo en antihorario. Se consigue cambiando el sweep-flag del arco:

HTML
<defs>  <!-- Arco superior, horario -->  <path id="arriba" fill="none"        d="M 30 100 A 70 70 0 0 1 170 100" />  <!-- Arco inferior, antihorario, para que el texto salga derecho -->  <path id="abajo" fill="none"        d="M 30 100 A 70 70 0 0 0 170 100" /></defs><text font-size="16" text-anchor="middle" fill="#cdd6f4">  <textPath href="#arriba" startOffset="50%">TEXTO SUPERIOR</textPath></text><text font-size="16" text-anchor="middle" fill="#cdd6f4">  <textPath href="#abajo" startOffset="50%">TEXTO INFERIOR</textPath></text>

Dos: usar el atributo side. SVG 2 añadió side="right" a textPath, que coloca el texto al otro lado de la ruta y con la orientación invertida. Es más elegante y su soporte no es universal; compruébalo antes de depender de él. La solución de las dos rutas funciona en todas partes.

Un tercer detalle del sello: el texto queda pegado a la ruta por su línea base, así que en el arco superior las letras van por fuera y en el inferior por dentro. Para desplazarlo hacia fuera o hacia dentro se usa un dy en el text:

HTML
<text font-size="16" text-anchor="middle" dy="-6">  <textPath href="#abajo" startOffset="50%">TEXTO INFERIOR</textPath></text>

Lo que no cabe

Si el texto es más largo que la ruta, el exceso no se dibuja. Se corta en seco, sin puntos suspensivos y sin aviso. Es un comportamiento distinto del de un texto normal, que sí se sale del lienzo y se recorta contra el viewport: aquí desaparece antes.

Eso convierte a textPath en un elemento delicado con contenido dinámico. Un rótulo curvo con un nombre de producto puede verse completo con un producto y cortado con otro.

La defensa es medir. getComputedTextLength() sobre el text devuelve la longitud del texto, y getTotalLength() sobre la ruta devuelve la de la ruta. Si el primero excede al segundo, hay que reducir el tamaño de fuente o acortar el texto:

JS
function ajustarATrayecto(textEl, rutaEl, tamMax = 18, tamMin = 9) {  const largoRuta = rutaEl.getTotalLength();  for (let t = tamMax; t >= tamMin; t -= 0.5) {    textEl.style.fontSize = `${t}px`;    if (textEl.getComputedTextLength() <= largoRuta) return t;  }  return tamMin;}

El bucle es feo y funciona. La alternativa elegante es textLength, que se ve en la lección siguiente, con sus propias contrapartidas.