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:
<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.
<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:
<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:
<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:
<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:
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.