scene.text(...) crea prosa, títulos, párrafos, matemáticas y contenido mixto; scene.text.equation(...) es su atajo para ecuaciones en bloque. Ambas devuelven un Text: un Drawable vectorial que conserva la estructura semántica, se mide con el mismo motor que Layout y permite seleccionar y animar partes sueltas.

from gaanim import GOLD, Scene, TextFlow, part
scene = Scene(frame=(16, 9), background="#0f172a")
formula = part("formula", "$E = ", part("mass", "m", color=GOLD), " c^2$")
copy = scene.text(
    "La energía es ", formula,
    role="body",
    flow=TextFlow(wrap=5, align="center", line_spacing=1.2),
).move_to(0, 0)
scene.play([copy.animate.write(by="part").duration(1.0)])
scene.render()
animation preview

Cada pieza tiene una responsabilidad:

  • TextStyle controla el aspecto de los glifos y las métricas tipográficas.
  • TextFlow controla cómo se componen las líneas dentro del texto.
  • Layout controla la caja exterior: relleno, ajuste, crecimiento y posición.
  • TextSelection señala glifos dentro de un Text; nunca es un hijo independiente de Layout.

Crear texto

Las dos fábricas de texto y los roles tipográficos del tema.

FÁBRICAScene.textscene.text(*content, role=None, style=None, flow=None, font=None, math_font=None, size=None, weight=None, italic=None, color=None, opacity=None, letter_spacing=None, word_spacing=None, baseline=None, wrap=None, text_align=None, line_spacing=None, max_lines=None, overflow=None, direction=None, hyphenate=None, lang=None, markup=None) -> Text
$…$ activa la matemática y \$ escribe un dólar literal. color acepta un Color, una cadena CSS/hex o una tupla RGB(A); None hereda el color del estilo o del tema. lang="es" con hyphenate=True da párrafos justificados con guiones en español. Contenido, delimitadores, roles, métricas o flujos inválidos lanzan TypeError o ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
contentstr | TextPart | TextPartsobligatorioUna o varias cadenas, partes semánticas o grupos de partes. El resultado no puede estar vacío.
rolestr | NoneNoneRol tipográfico. Si todo es matemático se infiere math; si no, body.
style / flowTextStyle | None / TextFlow | NoneNoneEstilo y flujo reutilizables.
font … baselineargumentos con nombreNoneAjustes directos de estilo: fuente, métricas, color, opacidad, espaciado y línea base. Sustituyen a style.
wrap … langargumentos con nombreNoneAjustes directos de flujo: ajuste de línea, alineación, límite de líneas, desbordamiento, dirección, guiones e idioma. Sustituyen a flow.
markupbool | NoneNoneInterpreta *negrita* y _cursiva_. False deja * y _ literales (la matemática $…$ sigue activa); None usa text_markup del tema, que por defecto es True.
Ejemplo
title = scene.text("Movimiento circular", role="title")
note = scene.text("Radio $r = 1.5$", size=0.36, color="#94a3b8").move_to(0, -1)
FÁBRICATypography.equationequation(*content: TextContent, role: Optional[TextRole] = None, style: Optional[TextStyle] = None, flow: Optional[TextFlow] = None, font: Optional[str] = None, math_font: Optional[str] = None, size: Optional[float] = None, weight: Optional[int] = None, italic: Optional[bool] = None, color: Optional[ColorLike] = None, opacity: Optional[float] = None, letter_spacing: Optional[float] = None, word_spacing: Optional[float] = None, baseline: Optional[float] = None, wrap: Optional[TextWrap] = None, text_align: Optional[TextAlign] = None, line_spacing: Optional[float] = None, max_lines: Optional[int] = None, overflow: Optional[TextOverflow] = None, direction: Optional[TextDirection] = None, hyphenate: Optional[bool] = None) -> Text
Envuelve el contenido como $ … $: esos espacios hacen que Typst componga una ecuación en bloque. Cada frontera entre piezas es un espacio normal de Typst, así que Typst decide el espaciado de operadores e identificadores. Sin rol ni tamaño explícitos usa el tamaño matemático de 0.44 unidades. Las filas de una ecuación partida con \ guardan la separación de Typst entre filas con line_spacing=1 y cada unidad de más la ensancha un tamaño de letra, como entre líneas de texto. Contenido vacío o matemática inválida lanzan ValueError al crear la ecuación.
Parámetros
NombreTipoPredeterminadoDescripción
contentstr | TextPart | TextPartsobligatorioEcuación sin los delimitadores $.
opcionesigual que scene.textNoneComparte todo el estilo y el flujo de scene.text (salvo lang y markup).
Ejemplo
from gaanim import GOLD, Scene, part, parts
scene = Scene(frame=(16, 9), background="#0f172a")
equation = scene.text.equation(
    part("sum_force", "sum F_t"),
    "=",
    parts(mass="m", acceleration="a_t"),
).move_to(0, 0)
equation["acceleration"].fill(GOLD)
scene.play([equation.animate.write(by="part").duration(1.0)])
scene.render()
animation preview
FUNCIÓNquantityquantity(value: int | float, unit: str = "", *, decimals: int | None = None, decimal_separator: str = ".") -> str
Escribe cantidades con unidades de forma consistente, como siunitx en LaTeX: un espacio fino entre número y unidad (salvo en °, ′ y ″), unidades en redonda, exponentes y una barra en lugar de una fracción. Así 1 m³ y 181 L quedan espaciados igual. Una unidad mal formada lanza ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
valueint | floatobligatorioEl número; un entero conserva sus cifras.
unitstr""Factores separados por espacios, *, · o ., cada uno con un exponente opcional (m^3, s^-1); / divide por los que siguen: "kg m/s^2".
decimalsint | NoneNoneDecimales fijos; sin él, la forma más corta del número.
decimal_separatorstr"."Un carácter, por ejemplo ",".
Devuelve: str — Matemática Typst para scene.text.equation.
Ejemplo
from gaanim import Scene, quantity
scene = Scene(frame=(16, 9), background="#0f172a")
volumen = scene.text.equation("V =", quantity(1, "m^3"), "=", quantity(1000, "L")).move_to(0, 0.8)
gravedad = scene.text.equation("g =", quantity(9.81, "m/s^2", decimal_separator=",")).move_to(0, -0.6)
scene.play([volumen.animate.write(), gravedad.animate.write()])
scene.render()
animation preview

Roles

Los roles son title, subtitle, kicker, heading, body, caption, label, code y math. Sus tamaños predeterminados, en unidades de escena, son: title 0.64, subtitle 0.48, kicker 0.32, heading 0.48, body 0.40, caption 0.32, label 0.36, code 0.36 y math 0.44. La prosa usa New Computer Modern, el código JetBrains Mono y la matemática New Computer Modern Math. Las tres vienen con Gaanim, así que el texto se ve igual en cualquier equipo y en el navegador. Con un tema, kicker toma el color accent, ideal para la línea corta sobre un título:

kicker = scene.text("MISMO TERREMOTO. TRES EDIFICIOS.", role="kicker").move_to(0, 3.77)
title = scene.text("¿Cuál sufrirá más?", role="title").move_to(0, 3)

El estilo se resuelve en este orden, de menor a mayor prioridad: rol y tema, TextStyle/TextFlow, argumentos directos de scene.text, estilo local de cada part y, por último, selection.fill(...) o text.fill(...) posteriores.

Partes semánticas y matemáticas

Nombra fragmentos del contenido para colorearlos, animarlos o transformarlos después sin contar caracteres.

FUNCIÓNpartpart(name, *content, style=None, font=None, math_font=None, size=None, weight=None, italic=None, color=None, opacity=None, letter_spacing=None, word_spacing=None, baseline=None) -> TextPart
Subárbol semántico inmutable. La ruta de nombres anidados es estable y la usan las selecciones y las transiciones de texto. Nombres repetidos o vacíos, métricas inválidas o contenido anidado inválido lanzan ValueError o TypeError.
Parámetros
NombreTipoPredeterminadoDescripción
namestrobligatorioNombre no vacío y único entre sus hermanos.
contentstr | TextPart | TextPartsobligatorioContenido anidado.
style / estilo directoTextStyle | argumentos con nombreNoneTipografía local que hereda todo el subárbol.
Ejemplo
from gaanim import BLUE, GOLD, Scene, part
scene = Scene(frame=(16, 9), background="#0f172a")
formula = part(
    "formula",
    "$",
    part("variable", "x", color=BLUE),
    " dot 5 = ",
    part("result", "25", color=GOLD),
    "$",
)
text = scene.text("Resultado: ", formula).move_to(0, 0)
scene.play([text.animate.write(by="part", stagger=0.05).duration(1.1)])
scene.render()
animation preview
FUNCIÓNpartsparts(mapping=None, /, **content: str) -> TextParts
Grupo ordenado de partes simples, equivalente a varias llamadas a part(). Dentro de $…$, las entradas contiguas son tokens distintos de Typst y conservan su espaciado ajustado. Usa part() para estilos locales o anidamiento. Entradas vacías, repetidas o mezclar un mapa con argumentos con nombre lanzan ValueError; nombres o valores que no son cadenas, TypeError.
Parámetros
NombreTipoPredeterminadoDescripción
mappingMapping[str, str] | NoneNoneNombres y texto en orden. Admite nombres que no son identificadores de Python, como "tb:dist".
contentstr con nombreobligatorioAtajo para nombres que son identificadores: parts(mass="m") equivale a parts({"mass": "m"}).
Ejemplo
from gaanim import GOLD, Scene, parts
scene = Scene(frame=(16, 9), background="#0f172a")
equation = scene.text.equation(
    "-",
    parts(mass_left="m", gravity="g sin(theta)"),
    "=",
    parts(mass_right="m", length="L", acceleration="theta''"),
).move_to(0, 0)
scene.play([equation.animate.write(by="part").duration(1.2)])
scene.play([equation["gravity"].animate.indicate().duration(0.6)])
scene.play([equation["acceleration"].animate.fill(GOLD).duration(0.6)])
scene.render()
animation preview

Un mapa admite nombres que los argumentos con nombre no pueden expresar:

label = scene.text(parts({"tb:dist": "d = ", "x-1": "4.2 m"}))
label["tb:dist"].fill(GOLD)

Énfasis en línea

Las cadenas admiten un marcado mínimo inspirado en Typst: *negrita*, _cursiva_ y ambos anidados, *_así_*. Se compila en las mismas pasadas que TextStyle, así que funciona con ajuste de línea, selecciones, animaciones, partes y bounds().

from gaanim import GOLD, Scene

scene = Scene(frame=(16, 9), background="#0f172a")
copy = scene.text(
    "Normal, _cursiva_, *negrita* y *_ambas_*.",
    size=0.36,
).move_to(0, 0)
scene.play([copy.animate.write(by="word", stagger=0.05).duration(1.2)])
scene.play([copy.words[2].animate.indicate().duration(0.6)])
scene.render()
animation preview
  • \\* y \\_ escriben los delimitadores literales.
  • El marcado puede cruzar cadenas contiguas y fronteras de part() sin añadir huecos.
  • Dentro de $…$ los marcadores son sintaxis matemática: x_1 es un subíndice y * una multiplicación.
  • Los guiones bajos dentro de palabras (snake_case), los marcadores repetidos (__init__) y una expresión con espacios como 5 * 4 quedan literales.
  • Un delimitador de apertura sin cierre, o un anidamiento cruzado, lanza ValueError.
  • Para contenido técnico lleno de _ o * (tb:dist_comp, X1_2) pasa markup=False: todo queda literal, barras invertidas incluidas, y $…$ sigue siendo matemática. text.become(...) conserva el modo salvo que pases markup=.
  • Theme(text_markup=False) lo hace predeterminado para scene.text, badge y chip.
label = scene.text("Valores: V_e del piso 1 (tb:agriet_xy)", markup=False)

Delimitadores matemáticos

  • $…$ cambia el compositor Typst a matemáticas; $$…$$ usa el mismo compositor y no crea un objeto distinto.
  • scene.text.equation(...) añade $ … $: no escribas los delimitadores.
  • \$ escribe un dólar literal y un delimitador sin pareja lanza ValueError.
  • Con role="code" el texto es literal: todo $, *, _ y barra invertida se muestra tal cual, así que scene.text("$ gaanim init", role="code") o scene.text("stagger(*grows)", role="code") no necesitan escapes.
  • Si todo el contenido es matemático, el rol inferido es math; si mezcla prosa y matemáticas, body.
  • Dentro de la matemática, cada frontera entre piezas es un espacio normal de Typst, así que no hace falta escribir "= ". Fuera de la matemática las fronteras son exactas.
  • Los estilos locales de una parte quedan dentro de la misma ecuación Typst: cambiar su color, fuente o tamaño nunca añade huecos artificiales.
  • La sintaxis puede cruzar fronteras: part("x", "x"), "_1" se compila como x _1 y Typst mantiene _1 como subíndice.

Estilo y flujo

Aspecto de los glifos y composición de las líneas, como valores reutilizables.

CLASETextStyleTextStyle(*, font=None, math_font=None, fallbacks=(), size=None, weight=None, italic=None, color=None, stroke=None, stroke_width=None, opacity=None, letter_spacing=None, word_spacing=None, decorations=(), baseline=None)
Estilo tipográfico inmutable y reutilizable. No tiene ancho, alto, relleno ni ajuste de caja: eso es de Layout. El estilo raíz se aplica a todos los glifos y el de una parte, a los suyos: fuente, tamaño, peso, cursiva, color, contorno, espaciado, decoraciones y línea base. opacity se aplica a todo el texto. Valores inválidos lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
font / math_fontstr | NoneNoneFamilias de prosa y de matemáticas.
fallbacksSequence[str]()Familias de respaldo en orden.
sizefloat | NoneNoneTamaño positivo en unidades de escena.
weightint | NoneNonePeso numérico de 1 a 1000.
color / stroke / stroke_widthColor | None / Color | None / float | NoneNoneRelleno de los glifos y contorno opcional, de stroke_width de ancho (0,02 si solo se da stroke).
opacityfloat | NoneNoneOpacidad de todo el texto, de 0 a 1.
letter_spacing / word_spacingfloat | NoneNoneEspaciado adicional no negativo, en unidades de escena.
decorationsSequence[str]()underline, strike o strikethrough.
baselinefloat | NoneNoneDesplazamiento de la línea base; positivo sube los glifos dentro de su línea.
Ejemplo
display = TextStyle(
    font="New Computer Modern",
    math_font="New Computer Modern Math",
    size=0.42,
    weight=650,
    color=GOLD,
    letter_spacing=0.02,
    decorations=("underline",),
)
heading = scene.text("Resultados", style=display)
CLASETextFlowTextFlow(*, wrap="auto", align="left", line_spacing=1.0, max_lines=None, overflow="clip", direction="auto", hyphenate=False, lang=None)
Opciones inmutables de composición de líneas. Los argumentos directos de scene.text sustituyen a este objeto.
Parámetros
NombreTipoPredeterminadoDescripción
wrap"auto" | False | float"auto""auto" usa el ancho que ofrece Layout (o el área segura); False mantiene una línea salvo saltos explícitos; un número limita el ancho tipográfico.
alignstr"left"left, center, right o justify.
line_spacingfloat1.0Distancia entre líneas base, en múltiplos del tamaño de letra, como line-height en CSS: con 1.2 y letra de 0.4, las líneas quedan a 0.48. Con el valor por defecto, 1.0, las líneas quedan a un tamaño de letra, sin solaparse; súbelo para párrafos largos. Debe ser positivo.
max_lines / overflowint | None / strNone / "clip"Límite de líneas y qué pasa con el resto: clip conserva las palabras que caben (una primera palabra que no cabe se corta entre caracteres), ellipsis las termina con «…» y visible dibuja todas las líneas.
directionstr"auto"auto, ltr o rtl.
hyphenate / langbool / str | NoneFalse / NoneGuiones de Typst y código ISO 639 en minúsculas ("es", "en"…) que elige los patrones; None usa inglés.
Ejemplo
from gaanim import GOLD, Scene, TextFlow
scene = Scene(frame=(16, 9), background="#0f172a", margin=0.375)
body = scene.text(
    "El mismo texto se mide con el ancho que ofrece su tarjeta de Layout v2.",
    role="body",
    flow=TextFlow(wrap="auto", align="justify", line_spacing=1.25),
)
page = scene.layout.column(
    [scene.text("Texto responsive", role="heading").fill(GOLD), body],
    within="safe", width="fill", height="fill", padding=0.35, gap=0.225,
)
scene.play([page.animate.fade_in().duration(0.7)])
scene.render()
animation preview

Sin flow ni text_align, el anclaje de move_to también alinea un texto con saltos de línea explícitos: Anchor.*_LEFT y TextAnchor.BASELINE_LEFT alinean a la izquierda, *_RIGHT a la derecha y los anclajes centrados centran. Un flow o text_align explícitos siempre ganan; el texto de una sola línea conserva la alineación izquierda.

note = scene.text("Fuente: ensayo 3\nEscala 1:50").move_to(7.5, -4, Anchor.BOTTOM_RIGHT)

Posición

Colocación por línea base para que palabras y ecuaciones se alineen.

MÉTODOText.move_tomove_to(x, y, anchor: Anchor | TextAnchor | None = None) | move_to(reference) | move_to(point) -> Text
Una sola línea se coloca por defecto con TextAnchor.BASELINE_CENTER: el centro horizontal visual en x y la línea base en y. Así, textos y ecuaciones con distintas ascendentes, fracciones o tamaños comparten línea base. Un bloque de varias líneas sin anclaje usa su centro visual, y un TextAnchor explícito usa la primera línea. Los anclajes geométricos (Anchor.TOP_LEFT…) se basan en los límites y no garantizan una línea base común. Un Drawable o un AnchorPoint centran el texto sobre la referencia sin seguirla. Un texto gestionado por Layout lanza LayoutOwnershipError; text.animate.move_to(...) sí lo mueve.
Parámetros
NombreTipoPredeterminadoDescripción
x / yfloat | fuente reactivaobligatorioPunto de destino en unidades de escena.
anchorAnchor | str | TextAnchor | NoneNoneAnclaje geométrico (un Anchor o su nombre, como "top_left"), o TextAnchor.BASELINE_LEFT, BASELINE_CENTER o BASELINE_RIGHT sobre la línea base.
Ejemplo
word = scene.text("Tipografía").move_to(0, 1)
equation = scene.text.equation("frac(x_1^2, y_2) = 1").move_to(0, -0.5, TextAnchor.BASELINE_CENTER)
left = scene.text("alineado a la izquierda").move_to(-4, -2, TextAnchor.BASELINE_LEFT)
corner = scene.text("esquina").move_to(-7, 4, Anchor.TOP_LEFT)

Text hereda todos los métodos de Drawable y conserva su tipo al encadenarlos.

MÉTODOText.glow / blur / shadow / no_effects / blendglow(color, radius=0.16, intensity=1.0) · blur(sigma=0.04) · shadow(color, x=0.08, y=-0.08, blur=0.06) · no_effects() · blend(mode="normal") -> Text
Los efectos y el modo de fusión de Drawable, redefinidos para devolver el Text y conservar el anclaje tipográfico de un move_to posterior.
Ejemplo
title = scene.text("Gaanim", role="title").glow(BLUE).move_to(0, 1, TextAnchor.BASELINE_CENTER)

Selecciones

Señalan grafemas, palabras, líneas o partes de un texto para darles estilo o animarlas sin separarlas del texto.

CLASETextSelectiontext[name] · text[index] · text[start:end] · selection[name] -> TextSelection
Selección diferida dentro de su Text. Un rango de grafemas, palabras o líneas selecciona el texto desde la primera hasta la última unidad, puntuación incluida. Cadenas que no son parte ni texto lanzan KeyError; índices fuera de rango, IndexError.
Parámetros
NombreTipoPredeterminadoDescripción
namestrobligatorioUna parte de primer nivel; sigue indexando para bajar por partes anidadas. Una cadena que no es parte selecciona cada aparición literal de ese texto, sin distinguir mayúsculas ni espacios.
index / sliceint | sliceobligatorioGrafemas renderizados; admite índices negativos y rangos contiguos con paso 1.
Ejemplo
from gaanim import BLUE, GOLD, Scene, part
scene = Scene(frame=(16, 9), background="#0f172a")
copy = scene.text(
    "La ", part("concept", "energía"), " depende de ",
    part("formula", "$", part("mass", "m"), " c^2$"),
).move_to(0, 0)
copy["concept"].fill(GOLD)
copy["formula"]["mass"].fill(BLUE)
scene.play([copy.animate.write(by="word").duration(1.0)])
scene.play(copy.words[1].animate.pulse().duration(0.6))
scene.play(copy["formula"]["mass"].animate.focus().duration(0.6))
scene.render()
animation preview
PROPIEDADText.graphemes / words / lines / partsgraphemes · words · lines · parts -> TextQuery
Vistas indexables (TextQuery) sobre las unidades del texto. words sigue los límites de palabra de Unicode, así que la puntuación no es una palabra: "uno dos, tres" tiene tres y words[1:3] selecciona dos, tres. lines sigue los saltos \n explícitos, no las líneas creadas por el ajuste. parts recorre el árbol semántico en profundidad. len(query) cuenta las unidades y "mass" in text.parts comprueba si existe una parte, también con rutas anidadas como "formula.mass".
Ejemplo
copy = scene.text("uno dos, tres")
tail = copy.words[1:3]
if "mass" in copy.parts:
    copy["mass"].fill(GOLD)
MÉTODOTextSelection.fillfill(color: ColorLike) -> TextSelection
Colorea los glifos seleccionados de forma persistente: antes de que el texto se muestre es el color declarado de la selección, y después los glifos cambian de color en el cursor, como con cualquier cambio inmediato. En matemáticas, la parte sigue en la misma ecuación Typst: cambiar su color no añade espacios ni mueve los términos vecinos.
Ejemplo
formula = scene.text.equation("E = ", part("mass", "m"), " c^2")
formula["mass"].fill(GOLD)
MÉTODOTextSelection.markermarker(color: ColorLike | None = None, *, skew: float = 0.05, blend: Literal["normal", "multiply"] = "normal", opacity: float = 0.45, padding: float | None = None) -> TextSelection
Coloca al instante bandas de rotulador detrás de cada línea seleccionada, con los mismos argumentos que animate.marker. Aparecen en el cursor (el inicio de la escena durante la declaración) y no siguen movimientos posteriores del texto.
Ejemplo
quote = scene.text("Lo que no se mide no se puede mejorar")
quote.words[0:2].marker(opacity=0.35)
PROPIEDADTextSelection.annotateannotate: Annotations
Las anotaciones a mano alzada de Drawable.annotate alrededor de una parte del texto. Siguen los glifos aunque el texto se mueva.
Devuelve: Annotations — Constructor de anotaciones alrededor de los glifos seleccionados.
Ejemplo
eq = scene.text.equation("E = ", part("rhs", "m c^2"))
scene.play([eq["rhs"].annotate.underline(color=RED, seed=1).animate.create()])
PROPIEDADTextSelection.animate / TextSelectionAnimation.fill / opacity
Proxy TextSelectionAnimation limitado a los glifos seleccionados. Acepta fill y opacity, combinables (animate.fill(RED).opacity(0.6)); los destinos de transformación, escala, rotación, material o trazo lanzan TypeError. Cada método devuelve un Anim para scene.play.
Ejemplo
eq = scene.text("$E = ", part("mass", "m"), " c^2$")
scene.play([eq.animate.write().duration(0.8), eq["mass"].animate.fill(RED).opacity(0.7).duration(0.8)])
MÉTODOTextSelectionAnimation.indicate / pulse / wiggle / wave / highlight / focusindicate() · pulse() · wiggle() · wave() · highlight() · focus() -> Anim
Énfasis transitorios sobre la selección. indicate hace crecer los glifos 1,2 veces y los tiñe del "accent" del tema (o de su "foreground" si ya tienen el color del acento); pulse los hace crecer dos veces seguidas, como un latido, sin teñirlos; wiggle los sacude de lado a lado; wave levanta cada glifo un tercio de su altura y lo baja, uno tras otro; highlight los tiñe del acento y los devuelve a su color sin moverlos; focus atenúa el resto del texto. No cambian la medición del texto ni provocan reflow.
Ejemplo
eq = scene.text.equation("F = ", part("mass", "m"), " a")
scene.play([eq["mass"].animate.highlight().duration(0.8)])
scene.play([eq["mass"].animate.wave().duration(0.8)])
MÉTODOTextSelectionAnimation.cancelcancel() -> Anim
Tacha la selección con una marca diagonal y atenúa sus glifos. La siguiente transición de texto que reemplace el contenido retira ambos.
MÉTODOTextSelectionAnimation.revealreveal(style: Literal["fade", "wipe", "from_below"] = "fade") -> Anim
Hace aparecer solo los glifos seleccionados con "fade", "wipe" (traza y luego rellena) o "from_below" (fundido con una subida corta), para que un término entre en una ecuación ya visible. Los glifos seleccionados siguen ocultos hasta el reveal, también mientras se escribe el resto. Un estilo desconocido lanza ValueError. Para revelar un texto completo por líneas o palabras, usa Anim.reveal (ver Animaciones).
Ejemplo
eq = scene.text.equation("a^2 + b^2 = ", part("rhs", "c^2"))
scene.play([eq["rhs"].animate.reveal("from_below").duration(0.6)])
MÉTODOTextSelectionAnimation.bracebrace(label: str = "", *, above: bool = False) -> Anim
Dibuja una llave bajo la selección (sobre ella con above=True) y hace aparecer label. La llave y la etiqueta son objetos nuevos, del color de los glifos, que permanecen en pantalla.
Ejemplo
eq = scene.text.equation("E = ", part("mass", "m"), " c^2")
scene.play([eq["mass"].animate.brace("masa")])
MÉTODOTextSelectionAnimation.annotateannotate(label: str, offset: tuple[float, float] = (0.0, 0.6)) -> Anim
Coloca label a offset del centro de la selección con una línea guía. La línea y la etiqueta permanecen; un desplazamiento no finito lanza ValueError.
Ejemplo
eq = scene.text.equation("E = m ", part("c", "c"), "^2")
scene.play([eq["c"].animate.annotate("velocidad de la luz", offset=(0, 0.8))])
MÉTODOTextSelectionAnimation.markermarker(color: ColorLike | None = None, *, skew: float = 0.05, blend: Literal["normal", "multiply"] = "normal", opacity: float = 0.45, padding: float | None = None) -> Anim
Pasa un rotulador detrás de cada línea que abarca la selección. Cada banda cubre la altura de los glifos de su línea más padding y crece desde la izquierda; las líneas se suceden y se reparten la duración según su longitud. Las bandas quedan detrás de los glifos pero delante de lo creado antes del texto, y permanecen. Un blend desconocido, un skew no finito, una opacidad fuera de [0, 1] o un padding negativo lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
colorColor | NoneNoneColor del rotulador; por defecto un amarillo cuyo alfa se multiplica por opacity.
skewfloat0.05Inclinación de cada banda en radianes (positivo sube hacia la derecha), limitada para cubrir líneas largas.
blendstr"normal""normal" pinta la banda encima de lo que tiene debajo; "multiply" la multiplica con el fondo o la tarjeta, como tinta sobre papel (sobre negro no se ve). En ambos casos va detrás de los glifos.
opacityfloat0.45De 0 a 1.
paddingfloat | NoneNoneMargen en unidades del mundo; None usa el 10 % de la altura de línea.
Ejemplo
from gaanim import Scene
scene = Scene(frame=(16, 9), background="#f8f5ee")
quote = scene.text("Lo que no se mide no se puede mejorar", color="#1f2937").move_to(0, 0)
scene.play([quote.words[4:9].animate.marker(skew=0.04).duration(1.2)])
scene.render()
animation preview
MÉTODOTextSelectionAnimation.morph_to / copy_tomorph_to(target: TextSelection) · copy_to(target: TextSelection) -> Anim
Transiciones locales entre partes: morph_to sustituye la selección por el destino y copy_to conserva el origen mientras una copia viaja hasta el destino.
Ejemplo
energy = scene.text("$E = ", part("mass", "m"), " c^2$").move_to(0, 1)
momentum = scene.text("$p = ", part("mass", "m"), " v$").move_to(0, -1)
scene.play([energy["mass"].animate.copy_to(momentum["mass"]).duration(0.8)])

Las animaciones de selección resuelven el código matemático escrito a los glifos que emitió Typst. Primero se busca el fragmento literal; si no aparece, el analizador matemático de Typst convierte nombres de símbolo (theta, sum, arrow.r.long), atajos (<=) y primas (theta'') al mismo Unicode que la ecuación renderizada.

Animación de texto completo

La escritura por grupos, los revelados con máscara, el desenfoque de entrada, el tracking, la máquina de escribir y el efecto de decodificación están en Animaciones (Anim.write, Anim.reveal, Anim.conceal, Anim.blur_in, Anim.tracking, Anim.typewriter, Anim.scramble). Aquí están el animador de rango, que los generaliza, y el tracking inmediato.

MÉTODOText.animatoranimator(by: Literal["grapheme", "word", "line", "part"] = "grapheme", shape: Literal["square", "ramp", "smooth", "ease_in", "ease_out", "triangle", "round"] = "smooth", order: Literal["forward", "reverse", "center", "random"] = "forward", seed: int = 0) -> TextAnimator
Selector de rango por glifo al estilo de los Text Animators de After Effects. Rust evalúa la influencia de cada unidad a partir de su índice y la forma del selector, así que un seek coincide con la reproducción y no se llama a Python por fotograma. reveal, conceal y blur_in son preajustes de este animador. Nombres inválidos lanzan ValueError; sobre un objeto que no es Text, TypeError.
Parámetros
NombreTipoPredeterminadoDescripción
bystr"grapheme"Unidad: grapheme, word, line (explícita) o part. La puntuación se une a su vecina.
shapestr"smooth"Perfil del selector. square, ramp, smooth, ease_in y ease_out llevan cada unidad del estado «fuera» al reposo (revelado); triangle y round suben al estado «fuera» y vuelven (ola).
orderstr"forward"Qué unidad alcanza primero el rango: forward, reverse, center o random.
seedint0Fija la permutación de order="random".
Ejemplo
from gaanim import Scene, parallel

scene = Scene(frame=(16, 9), background="#0f172a")
title = scene.text("Tipografía cinética", role="title").move_to(0, 0)
wave = title.animator(by="grapheme", shape="smooth", order="forward", seed=0)
wave.set(offset=(0, -0.4), opacity=0.0, scale=0.6, rotation=0.2)  # estado "fuera"
scene.play([wave.animate.sweep().duration(1.2)])  # recorre el rango 0 → 1

bob = title.animator(by="word", shape="round").set(offset=(0, 0.3))
scene.play(parallel(bob.animate.sweep().duration(1.0)))  # una ola que pasa
scene.render()
MÉTODOTextAnimator.set / animate
Define el estado «fuera» (influencia 1) y devuelve el animador. Los valores omitidos conservan lo definido antes; los canales nunca definidos quedan en reposo. Cada barrido captura el estado al llamar a sweep(). Valores inválidos lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
offset(float, float) | NoneNoneDesplazamiento en unidades de escena.
opacityfloat | NoneNoneOpacidad absoluta de 0 a 1.
scale / rotationfloat | NoneNoneAlrededor del centro de cada unidad; la rotación en radianes, menos de media vuelta.
blur / trackingfloat | NoneNoneSigma gaussiana y espacio extra entre glifos, en unidades de escena.
colorColor | NoneNoneRelleno sólido.
MÉTODOTextAnimatorAnimation.sweepsweep(start: float = 0.0, end: float = 1.0, *, stagger: Optional[float] = None) -> Anim
Accesible como animator.animate.sweep(...). Devuelve un Anim que mueve el rango de start a end: 0 está antes de la primera unidad y 1 después de la última, así que sweep(1, 0) lo recorre hacia atrás. stagger es el retraso en segundos entre unidades (None lo adapta) y easing suaviza cada unidad. Funciona en scene.play, parallel, sequence y stagger; los barridos de entrada mantienen su primer fotograma hasta empezar.
MÉTODOText.trackingtracking(value: float) -> Self
Fija al instante value unidades de escena extra entre glifos vecinos, sin recomponer el párrafo: cada fila crece desde su borde izquierdo, su centro o su borde derecho según la alineación. 0 restaura el espaciado. Anímalo con animate.tracking(value).
Ejemplo
title = scene.text("Tipografía", role="title").tracking(0.4)
scene.play([title.animate.tracking(0.0).duration(1.2)])

Texto sobre una trayectoria

FÁBRICATypography.on_pathon_path(content: TextContent, path: Drawable, *, align: Literal["start", "center", "end"] = "start", orient: bool = True, reverse: bool = False, offset: float = 0.0, role: Optional[TextRole] = None, style: Optional[TextStyle] = None, font: Optional[str] = None, size: Optional[float] = None, weight: Optional[int] = None, italic: Optional[bool] = None, color: Optional[ColorLike] = None, opacity: Optional[float] = None, letter_spacing: Optional[float] = None, word_spacing: Optional[float] = None, markup: Optional[bool] = None) -> Text
Cada glifo conserva su forma y su espaciado: su centro sobre la línea base se coloca en la trayectoria a la misma longitud de arco que tenía en la línea. Los glifos quedan a la izquierda del sentido de avance; un círculo avanza en sentido antihorario, así que el texto se lee por dentro, y reverse=True lo pone por fuera. En una trayectoria cerrada el texto da la vuelta; en una abierta, los glifos que pasan un extremo siguen en línea recta. La trayectoria se lee en coordenadas de escena donde está path en ese momento y pasa a ser del texto: moverlo la arrastra, y cambiar path después no mueve los glifos. path sigue siendo un drawable normal. Acepta los argumentos de estilo de scene.text. Un align desconocido, un offset no finito, un path de otra escena o un grupo (que no tiene trayectoria propia) y un contenido sin caracteres visibles lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
contentTextContentobligatorioUna línea de texto; nunca se parte.
pathDrawableobligatorioDrawable cuya trayectoria siguen los glifos.
alignstr"start""start", "center" o "end": el texto empieza, se centra o termina en la trayectoria.
orientboolTrueGira cada glifo con la dirección de la trayectoria; con False quedan derechos.
reverseboolFalseRecorre la trayectoria del final al principio.
offsetfloat0.0Desplazamiento inicial, como fracción de la longitud de la trayectoria.
Devuelve: Text — Texto cuyos glifos siguen la trayectoria.
Ejemplo
from gaanim import *

scene = Scene(frame=(16, 9))
circle = scene.geometry.circle(2.4).no_fill().stroke(GRAY, 0.03)
ring = scene.text.on_path("GAANIM · MOTION · DESIGN · ", circle, reverse=True, size=0.5)
scene.play([ring.animate.path_offset(1.0).duration(4).easing(Easing.LINEAR)])
scene.render()
animation preview
MÉTODOText.path_offsetpath_offset(value: float) -> Self
Desplaza al instante un texto creado con on_path por su trayectoria, como fracción de su longitud. Declarado antes del primer play es el desplazamiento inicial; después corta en el cursor. Anímalo con animate.path_offset(value).
Ejemplo
wave = scene.geometry.path([("move", [(-6, 0)]), ("cubic", [(-2, 3), (2, -3), (6, 0)])])
label = scene.text.on_path("una ola de texto", wave, align="center").path_offset(-0.2)
scene.play([label.animate.path_offset(0.2).duration(2)])

Transiciones estructurales

text.animate.transform_to(target) (ver Anim.transform_to en Animaciones) empareja las partes semánticas de dos textos: los términos compartidos viajan a su nuevo sitio y el resto aparece o desaparece. Cada glifo conserva su color, también los de part(), y al terminar queda visible el destino. La transición usa la misma duración para el texto y para el reflow de su Layout. Un destino de otra escena lanza ValueError. Entre textos gestionados por Layouts distintos (por ejemplo, celdas de dos matrices) la transformación es un morph de forma única: el origen conserva su identidad y adopta el contorno, el color y la línea base del destino.

from gaanim import GOLD, Scene, part
scene = Scene(frame=(16, 9), background="#0f172a")
before = scene.text("$x + ", part("obsolete", "3"), " = 7$").move_to(0, 0)
after = scene.text("$x = ", part("result", "4", color=GOLD), "$").move_to(0, 0)
scene.play([before.animate.write().duration(0.8)])
scene.play([before["obsolete"].animate.cancel().duration(0.5)])
scene.play([before.animate.transform_to(after).duration(0.8)])
scene.render()
animation preview

Cambiar el contenido

Sustituye el contenido de un texto sin crear otro objeto.

MÉTODOText.becomebecome(*content: TextContent, role: Optional[TextRole] = None, style: Optional[TextStyle] = None, flow: Optional[TextFlow] = None, markup: Optional[bool] = None) -> None
Reemplaza el contenido conservando la identidad del Text, incrementa su versión estructural y pide reflow a su Layout. Si el texto es libre y ya se mostró, el nuevo contenido lo reemplaza en el cursor, colocado como se declaró el texto (move_to, place), y las animaciones posteriores del Text actúan sobre él. Es una llamada inmediata de autoría, no un Anim: no la pongas en scene.play. markup=None conserva el modo de marcado actual. Contenido o delimitadores inválidos lanzan ValueError.
Ejemplo
copy = scene.text("Calculando…")
scene.wait(0.5)
copy.become("Resultado: ", part("value", "$42$", color=GOLD))

Documentos Typst y código

Para lo que no cabe en un Text: documentos Typst completos y bloques de código. Para medir cualquier objeto, texto incluido, usa bounds() (ver Drawable).

FÁBRICATypography.typsttypst(source: str | os.PathLike[str], *, width: Optional[str | float | int] = None) -> Drawable
Compila un documento Typst arbitrario (tablas con celdas combinadas, estructuras matemáticas propias, paquetes @preview/…) como un Drawable. Conserva sus proporciones y se escala para que su texto de 11 pt mida lo mismo que el rol body. No ofrece las selecciones de Text. Su texto toma el color del rol body del tema, como scene.text, y también los trazos que Typst dibuja en negro por defecto (tablas, rect, line, formas de CeTZ). El documento empieza con theme, un diccionario con los colores del tema por nombre (theme.accent, theme.foreground, theme.panel, theme.rule…): los paquetes pensados para papel blanco eligen ellos mismos el negro y el blanco, así que pásales estos colores por sus opciones (ver Colores del tema en Typst). Los trazos que continúan uno a otro, como una curva que un paquete de gráficas dibuja muestra a muestra, son una sola pieza para write. Una fuente vacía lanza ValueError y un archivo ilegible, RuntimeError.
Parámetros
NombreTipoPredeterminadoDescripción
sourcestr | os.PathLikeobligatorioMarcado Typst en línea o la ruta de un archivo .typ (relativa a la carpeta de assets).
widthstr | float | int | NoneNoneAncho de página Typst antes de escalar: "16cm", "800pt" o un número en puntos. Sin él, la página se ajusta al contenido: #align(center) centra dentro del contenido y los párrafos solo cortan línea donde la fuente lo indica. Pásalo para que un párrafo se ajuste o para columnas 1fr.
Ejemplo
from gaanim import Scene
scene = Scene(frame=(16, 9), background="#0f172a")
tbl = scene.text.typst('#table(columns: 2, [*Método*], [*Error*], [Base], [0.18], [GPU], [0.04])')
scene.play([tbl.animate.fade_in().duration(0.6)])
scene.render()
animation preview

También carga archivos:

from pathlib import Path
title = scene.text.typst(Path("assets/title.typ"))

Colores del tema en Typst

Cada documento empieza con theme, los colores del tema por nombre, los mismos que scene.canvas.color(...). El texto ya toma el color del tema; úsalos para lo que el documento pinta por su cuenta:

from gaanim import Scene
scene = Scene()
table = scene.text.typst("""
#table(
  columns: 3,
  stroke: 0.6pt + theme.rule,
  fill: (x, y) => if y == 0 { theme.panel },
  [*x*], [*y*], [*x + y*],
  [1], [2], [#text(fill: theme.accent)[3]],
)
""")
scene.play([table.animate.write()])
scene.render()
animation preview

Los paquetes de Typst Universe están pensados para papel blanco y eligen ellos mismos el negro y el blanco de ejes, marcas y fondos de etiqueta. Dales los colores del tema por sus opciones de estilo, como en examples/text/typst_packages.py:

#import "@preview/simple-plot:1.0.0": plot
#plot(
  style: (
    axis: (stroke: theme.rule + 0.8pt, arrow: (symbol: "stealth", fill: theme.rule)),
    ticks: (stroke: theme.rule + 0.6pt, label-fill: theme.muted, label-bg: none),
    labels: (fill: theme.foreground, bg: none),
  ),
  (fn: x => x * x, stroke: theme.accent + 1.2pt),
)
FÁBRICATypography.codecode(source: str, *, language: str = "text", width: float = 7.6, height: float = 3.0, font_size: float = 0.2, background: Optional[ColorLike] = None, color: Optional[ColorLike] = None, accent: Optional[ColorLike] = None, theme: str | os.PathLike[str] | None = None, highlight: bool = True, font: Optional[str] = None) -> Drawable
Panel de código: el nombre del lenguaje sobre una regla y el código debajo, alineado a la izquierda, con sus tokens resaltados. Las palabras clave, las cadenas, los números, los comentarios, las funciones y los tipos toman los colores de código del tema: los temas de editores (dracula, nord, solarized-*, gruvbox-dark, tokyo-night, catppuccin-*) usan su esquema original, y technical, presentation, paper y los temas propios los derivan de la paleta (palabras clave en accent, comentarios en muted). Los tokens code, code_keyword, code_string, code_number, code_comment, code_function y code_type de Theme(colors=...) los sustituyen. Sobre un background en el que los colores del tema no se leen (un panel oscuro en un tema claro, o al revés), el texto y cada token se ajustan hasta leerse; los que ya se leen no cambian. Cada carácter sigue siendo un glifo propio, coloreado o no. Una fuente vacía, un lenguaje con caracteres distintos de letras y dígitos ASCII, - o _, una font vacía o un tamaño no positivo lanzan ValueError; un theme inexistente lanza FileNotFoundError, y uno que no es un tmTheme, ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
sourcestrobligatorioEl código. Los tabuladores y los saltos de línea se conservan.
languagestr"text"Nombre o extensión del lenguaje: "python", "rust", "js", "typst", "json"… Uno desconocido, como "text", queda sin resaltar.
widthfloat7.6Ancho del panel.
heightfloat3.0Alto del panel.
font_sizefloat0.2Tamaño base del texto; el código se dibuja al 80 %.
backgroundColorLike | NoneNoneFondo del panel; por defecto, el panel del tema (o el de theme).
colorColorLike | NoneNoneColor del texto que no se resalta: nombres, operadores y puntuación; por defecto, el foreground del tema (o el de theme), o su background si el foreground no se lee sobre el fondo del bloque.
accentColorLike | NoneNoneColor del borde, la regla y el nombre del lenguaje; por defecto, el accent del tema, aclarado u oscurecido si no se lee sobre un background propio.
themestr | os.PathLike | NoneNoneArchivo .tmTheme (el formato de TextMate, Sublime Text y bat) con los colores de los tokens, resuelto como los demás assets.
highlightboolTrueCon False, todo el código va en color.
fontstr | NoneNoneFamilia monoespaciada. Sin ella, o si no está instalada, JetBrains Mono, incluida en Gaanim, para que el bloque se vea igual en cualquier equipo.
Ejemplo
from gaanim import Scene
scene = Scene(frame=(16, 9))
snippet = scene.text.code(
    "def area(radius):\n"
    "    # circle area\n"
    "    return 3.14159 * radius ** 2",
    language="python",
    width=8,
    height=2.4,
    font_size=0.3,
)
scene.play([snippet.animate.fade_in().duration(0.5)])
scene.render()
animation preview

Con otro esquema, sin resaltar o con otra fuente:

papel = scene.text.code("let x = 42; // respuesta", language="rust", theme="assets/codigo.tmTheme")
plain = scene.text.code("x = 42", language="python", highlight=False)
fira = scene.text.code("x = 42", language="python", font="Fira Code")

Integración con Layout

Un Text libre y el mismo Text dentro de una fila, columna, grid o stack usan el mismo medidor. wrap="auto" lo hace sensible al ancho ofrecido; un ajuste numérico queda además limitado por ese ancho. Un texto que cabe conserva sus líneas naturales, y uno que se ajusta mantiene las líneas con las que se midió: un contenedor que abraza su contenido ("hug") nunca lo reparte en más líneas. Los cambios de métrica, become y animate.transform_to invalidan la medición y piden reflow; los énfasis transitorios (indicate, pulse, wiggle, wave, highlight, focus) no.

Una caja decide dónde descansa un Text hijo: colocarlo de forma inmediata con move_to, shift_by o next_to lanza LayoutOwnershipError; ajusta su text.item(...) o las propiedades de la caja. Todas las animaciones de .animate, de posición incluidas, siguen disponibles y parten de ese sitio. Consulta Layout.

Errores y casos límite

  • TypeError: contenido que no es str ni TextPart, o claves de consulta del tipo equivocado.
  • ValueError: contenido vacío, matemáticas desequilibradas, partes hermanas repetidas, rol, estilo o flujo inválidos, agrupación u orden inválidos, rangos con paso distinto de 1.
  • KeyError: una cadena que no es parte semántica ni texto del Text.
  • IndexError: índice fuera de rango o rango vacío.
  • LayoutOwnershipError: posicionar a mano un texto gestionado, o una transición entre escenas o propietarios incompatibles.