El masking de líneas y el efecto de revelado
Por qué el revelado clásico necesita un envoltorio que recorte, qué hace exactamente la opción mask, y cómo se construía a mano antes de que existiera.
El efecto por el que la gente descubre SplitText es siempre el mismo: unas líneas de texto que suben desde debajo de una frontera invisible, como si estuvieran escritas en un rodillo que gira. Ese efecto parece un problema de animación y es un problema de recorte: el texto no aparece, siempre estuvo ahí, simplemente estaba fuera de una zona visible. Construirlo a mano requiere una capa extra de elementos por cada línea, y esa capa es justo lo que la opción mask genera por ti.
- Explicar por qué el revelado necesita un contenedor que recorte y no basta con animar la opacidad.
- Usar
maskpara generar los envoltorios y acceder a ellos desde la instancia. - Construir el mismo efecto a mano y entender qué automatiza la opción.
- Elegir entre enmascarar líneas, palabras o caracteres según el resultado buscado.
El revelado es un recorte, no una aparición
Piensa en el efecto sin animación de por medio. Tienes una línea de texto desplazada cien por cien hacia abajo respecto a su posición normal, y un contenedor del alto exacto de una línea que recorta todo lo que se salga. En ese estado, la línea está completamente fuera del contenedor y no se ve nada. Anima el desplazamiento hasta cero y la línea entra en la zona visible desde abajo.
Eso es todo. No hay opacidad, no hay filtros, no hay máscaras de imagen. Hay un contenedor que recorta y contenido que se mueve dentro de él.
La diferencia con animar la opacidad es visualmente enorme y conviene nombrarla: con un desvanecido, la línea entera aparece a la vez con la misma intensidad. Con un recorte, la línea entra progresivamente por abajo, y en cada instante hay una frontera nítida entre lo que ya se ve y lo que no. Esa frontera es lo que hace que el efecto se lea como material —papel que sube, tipografía impresa en un cilindro— y no como digital.
La razón de que haga falta una capa extra es de anidamiento. La línea generada por SplitText es el elemento que se mueve, así que el recorte tiene que estar en su padre. Si le pones el recorte a la propia línea, estarás recortando su contenido en lugar de recortarla a ella, y el efecto no ocurre.
La opción mask
mask acepta "lines", "words" o "chars", uno solo, no una combinación. Envuelve cada elemento de ese tipo en un contenedor adicional con recorte aplicado.
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
SplitText.create(".titular", {
type: "lines",
mask: "lines",
autoSplit: true,
onSplit: (self) =>
gsap.from(self.lines, {
yPercent: 100,
duration: 0.9,
stagger: 0.12,
ease: "power4.out",
}),
});
Los envoltorios generados quedan accesibles en self.masks, por si necesitas animarlos también. Y si has puesto un nombre de clase con linesClass, los envoltorios reciben ese mismo nombre con el sufijo -mask, lo que permite darles estilo desde el CSS sin buscarlos en JavaScript.
SplitText.create(".titular", {
type: "lines",
mask: "lines",
linesClass: "linea", // los envoltorios seran ".linea-mask"
});
.linea-mask { display: block; }
Fíjate en que la animación ya no lleva autoAlpha. Con el recorte no hace falta: el texto no tiene que desvanecerse porque simplemente está fuera de la zona visible. Añadir opacidad encima suaviza la frontera y arruina precisamente lo que hace bueno al efecto.
Un contenedor que recorta corta cualquier cosa que se salga, incluidas las sombras de texto y las partes bajas de letras como la jota o la pe si el alto del envoltorio se queda justo. Si ves las letras decapitadas por abajo, el envoltorio no tiene sitio para los descendentes de la tipografía: la solución es dar algo de padding-bottom a la línea y compensar con un margen negativo, o subir la line-height.
Hacerlo a mano
Merece la pena montarlo una vez sin la opción, porque entender el mecanismo te permite variarlo después.
const split = SplitText.create(".titular", { type: "lines", linesClass: "linea" });
// Envolver cada linea en un contenedor que recorta
split.lines.forEach((linea) => {
const envoltorio = document.createElement("div");
envoltorio.className = "linea-mask";
linea.parentNode.insertBefore(envoltorio, linea);
envoltorio.appendChild(linea);
});
gsap.from(split.lines, {
yPercent: 100,
duration: 0.9,
stagger: 0.12,
ease: "power4.out",
});
.linea-mask { overflow: clip; display: block; }
.linea { display: block; will-change: transform; }
Con eso ves las tres piezas: el envoltorio que recorta, la línea que se mueve, y el bloque en ambos para que las transformaciones funcionen y el recorte tenga altura.
Merece la pena preferir overflow: clip a overflow: hidden. hidden convierte al elemento en un contenedor de scroll, lo que significa que si algo dentro recibe el foco, el navegador lo desplazará dentro de ese contenedor y verás la línea movida sin motivo aparente. clip recorta sin crear contenedor de scroll y no tiene ese efecto.
Y el motivo por el que quieres usar la opción integrada en lugar de este código: la versión manual se rompe con autoSplit. En cada redivisión, SplitText restaura el HTML original y tus envoltorios desaparecen, así que tendrías que rehacerlos dentro de onSplit y en el orden correcto. La opción mask los regenera sola.
Enmascarar palabras o caracteres
mask: "words" produce un efecto distinto y a menudo mejor para titulares cortos: cada palabra entra por su cuenta desde debajo de su propia frontera, y con un stagger pequeño el resultado es una cascada que se lee como escritura.
mask: "chars" lleva la idea al extremo y casi siempre es demasiado. Con caracteres, cada letra tiene su propio recorte, y como las letras son estrechas, el efecto se convierte en ruido visual en lugar de en movimiento legible. Además multiplica los nodos por dos: cada carácter pasa a ser dos elementos.
La regla práctica, ordenada de más usada a menos: líneas para párrafos y titulares de varias líneas; palabras para titulares de una línea donde quieres que se note el ritmo; caracteres casi nunca.
// Titular corto, entrada por palabras con recorte
SplitText.create("h1", {
type: "words",
mask: "words",
onSplit: (self) =>
gsap.from(self.words, {
yPercent: 130,
duration: 0.6,
stagger: 0.06,
ease: "power3.out",
}),
});
Hay una razón perceptiva de por qué el revelado con recorte se siente tan superior a un desvanecido, y no es cuestión de gusto. El sistema visual humano tiene mecanismos dedicados a detectar movimiento coherente de bordes, y esos mecanismos son extraordinariamente sensibles: distinguen un objeto que se desplaza detrás de una oclusión de un objeto que cambia de intensidad, y les asignan interpretaciones completamente distintas. Un desvanecido comunica “esto no estaba y ahora está”, que es un suceso sin causa física; el texto se materializa de la nada. Un recorte comunica “esto ya existía y estaba tapado”, que es un suceso con una explicación física inmediata y que el cerebro procesa como oclusión, la misma señal que usa constantemente para deducir profundidad y ordenación en el mundo real. Por eso el efecto se lee como si el texto estuviera impreso sobre un material que se mueve, y por eso añadir opacidad encima lo estropea: introduces una señal de aparición que contradice la señal de oclusión, y el cerebro recibe dos explicaciones incompatibles del mismo suceso. La consecuencia práctica va más allá del texto y merece aplicarse a toda tu interfaz: siempre que puedas elegir entre revelar algo por opacidad o revelarlo por oclusión, la oclusión produce una animación que se percibe más sólida por menos movimiento. Y hay un corolario económico agradable: el recorte no cuesta más que el desvanecido. Un desplazamiento y una opacidad se resuelven ambos en el compositor; la diferencia entre un efecto que parece plantilla y uno que parece diseñado son diez píxeles de overflow bien puestos.
- Monta el mismo revelado con
autoAlphay conmask: "lines", ponlos uno junto al otro y compáralos. - Añade
autoAlphaencima del revelado enmascarado y observa cómo se suaviza la frontera. Decide cuál prefieres. - Construye los envoltorios a mano y comprueba qué pasa al activar
autoSplitsin adaptar el código. - Sustituye
overflow: hiddenporoverflow: clipy comprueba el comportamiento al enfocar un enlace dentro del texto. - Prueba
mask: "words"ymask: "chars"sobre el mismo titular y cuenta los nodos generados en cada caso.