wandres.dev
GSAP VIII · SplitText

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.

⏱ 17 min

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.

🎯 Al terminar esta lección sabrás
  • Explicar por qué el revelado necesita un contenedor que recorte y no basta con animar la opacidad.
  • Usar mask para 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 recorte se lleva mal con las sombras y con los descendentes

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",
    }),
});
El recorte convierte una animación de aparición en una animación de movimiento, y el cerebro los procesa de forma distinta

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.

⚔️ Recortar en lugar de desvanecer
  1. Monta el mismo revelado con autoAlpha y con mask: "lines", ponlos uno junto al otro y compáralos.
  2. Añade autoAlpha encima del revelado enmascarado y observa cómo se suaviza la frontera. Decide cuál prefieres.
  3. Construye los envoltorios a mano y comprueba qué pasa al activar autoSplit sin adaptar el código.
  4. Sustituye overflow: hidden por overflow: clip y comprueba el comportamiento al enfocar un enlace dentro del texto.
  5. Prueba mask: "words" y mask: "chars" sobre el mismo titular y cuenta los nodos generados en cada caso.