scene.geometry crea las formas vectoriales de la escena. Cada fábrica devuelve un Drawable centrado en el origen, listo para encadenar estilo, posición y animación. Las medidas están en unidades de escena: el marco lógico mide 16 × 9.

from gaanim import BLUE, GOLD, WHITE, Scene

scene = Scene(frame=(16, 9), background="#0f172a")
disk = scene.geometry.circle(1).fill(BLUE).move_to(-3, 0)
card = scene.geometry.rounded_rect(2.4, 1.4, 0.2).fill(GOLD).move_to(0, 0)
star = scene.geometry.star(5, 1, 0.45).no_fill().stroke(WHITE, 0.05).move_to(3, 0)
scene.play([disk.animate.grow_from_center(), card.animate.create(), star.animate.create()], duration=1.2)
scene.render()
animation preview

Primitivas

Círculos, rectángulos, elipses y puntos: la base de casi cualquier diagrama.

FÁBRICAGeometry.circlecircle(radius: float) -> Drawable
Círculo centrado en el origen. Sirve para nodos, marcadores y diagramas radiales; combínalo con annulus para anillos.
Parámetros
NombreTipoPredeterminadoDescripción
radiusfloatobligatorioRadio en unidades de escena.
Ejemplo
from gaanim import BLUE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
node = scene.geometry.circle(0.625).fill(BLUE).move_to(0, 0)
scene.play([node.animate.create().duration(1.0)])
scene.render()
animation preview
FÁBRICAGeometry.rectrect(width: float, height: float) -> Drawable
Rectángulo de width × height centrado en el origen. Úsalo para tarjetas, paneles y barras; rounded_rect da esquinas suaves.
Ejemplo
from gaanim import BLUE, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
card = scene.geometry.rect(2, 1.125).fill(BLUE).stroke(WHITE, 0.025).move_to(0, 0)
scene.play([card.animate.grow_from_center().duration(0.9)])
scene.render()
animation preview
FÁBRICAGeometry.rounded_rectrounded_rect(width: float, height: float, radius: float) -> Drawable
Rectángulo con esquinas redondeadas de radio radius. Útil para botones, etiquetas y tarjetas.
Ejemplo
from gaanim import GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
btn = scene.geometry.rounded_rect(2, 0.625, 0.15).fill(GOLD).move_to(0, 0)
label = scene.text("PULSA").move_to(0, 0)
scene.play([scene.geometry.group([btn, label]).animate.fade_in().duration(0.6)])
scene.render()
animation preview
FÁBRICAGeometry.squaresquare(s: float) -> Drawable
Atajo de rect(s, s). Útil para celdas de rejilla.
Ejemplo
cell = scene.geometry.square(1).fill(BLUE)
FÁBRICAGeometry.ellipseellipse(rx: float, ry: float) -> Drawable
Elipse con radios rx y ry. Sirve para órbitas, resaltados y diagramas de Venn.
Ejemplo
from gaanim import GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
orbit = scene.geometry.ellipse(1.125, 0.625).no_fill().stroke(GOLD, 0.025).move_to(0, 0)
scene.play([orbit.animate.create().duration(1.0)])
scene.render()
animation preview
FÁBRICAGeometry.dotdot(radius: float) -> Drawable
Círculo pequeño relleno para marcadores, viñetas y partículas.
Parámetros
NombreTipoPredeterminadoDescripción
radiusfloatobligatorioRadio en unidades de escena; suele estar entre 0.06 y 0.15.
Ejemplo
marker = scene.geometry.dot(0.1).fill(RED).move_to(1, 0.5)
FÁBRICAGeometry.pointspoints(positions: Sequence[tuple[float, float]], radius: float = 0.06) -> Drawable
Nube de puntos sin Cartesian2D: todos los círculos comparten un camino, así que miles de puntos (una sección de Poincaré, una muestra) siguen siendo un único objeto para fill, opacity, el layout y la animación. Listas vacías, posiciones no finitas o un radio no positivo lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
positionsSequence[tuple[float, float]]obligatorioCentros en coordenadas de escena; al menos uno y todos finitos.
radiusfloat0.06Radio de cada punto, mayor que cero.
Ejemplo
import math
from gaanim import GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
spiral = [(0.02 * i * math.cos(0.2 * i), 0.02 * i * math.sin(0.2 * i)) for i in range(200)]
cloud = scene.geometry.points(spiral, radius=0.04).fill(GOLD)
scene.play([cloud.animate.fade_in().duration(0.5)])
scene.render()
animation preview

Líneas y flechas

Segmentos, flechas rectas y curvas, arcos y conectores. Los extremos reactivos (Endpoint) aceptan tuplas, objetos, AnchorPoint y PointRef. Las líneas, arcos, polilíneas, curvas y trazos seguidos nacen sin relleno: solo dibujan su trazo aunque ganen área.

FÁBRICAGeometry.lineline(p1: Endpoint, p2: Endpoint) | line(x1, y1, x2, y2) | line(*, length, direction=Direction.RIGHT) -> Drawable
Dos tuplas crean una línea normal que se mueve con su grupo. Los extremos que son referencias se resuelven en el espacio del mundo en cada fotograma, así que la línea sigue a los objetos. Con length, next_to coloca la línea sin cambiar su longitud ni su orientación. Mezclar length con extremos lanza TypeError; longitudes o direcciones inválidas lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
p1 / p2EndpointobligatorioExtremos fijos o reactivos.
x1, y1, x2, y2floatobligatorioForma compatible con cuatro coordenadas fijas.
lengthfloatobligatorioLongitud positiva de una línea centrada en el origen.
directionDirectionDirection.RIGHTOrientación con length; admite diagonales y vectores 2D no nulos.
Ejemplo
from gaanim import Anchor, GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
left = scene.geometry.dot(0.11).fill(GOLD).move_to(-1.5, -0.375)
card = scene.geometry.rect(1.5, 0.875).move_to(1.125, 0.44)
connector = scene.geometry.line(
    left.anchor_point(Anchor.RIGHT),
    card.anchor_point(Anchor.LEFT),
).stroke(WHITE, 0.04)
scene.play([left.animate.shift_by(0.375, 0.75).duration(0.7)])
scene.render()
animation preview

Con length, la línea es independiente de las coordenadas:

from gaanim import Direction, GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
title = scene.text("Una línea bajo el título", size=0.5).fill(WHITE)
rule = scene.geometry.line(length=5).stroke(GOLD, 0.035).next_to(
    title, Direction.DOWN, spacing=0.25,
)
scene.play([title.animate.fade_in(), rule.animate.create()], duration=1)
scene.render()
animation preview
FÁBRICAGeometry.arrowarrow(x1: float, y1: float, x2: float, y2: float, *, head_length: Optional[float] = None, head_width: Optional[float] = None, body_width: Optional[float] = None, max_head_ratio: Optional[float] = None) -> Drawable
Flecha sólida de (x1, y1) a (x2, y2). fill la colorea entera: salvo que stroke fije otro, su contorno sigue al relleno (igual en double_arrow, curved_arrow y curved_arrow_arc). Para flechas cortas añade max_head_ratio=0.3, y .no_stroke() para una silueta limpia. Extremos no finitos o medidas no positivas lanzan ValueError; extremos coincidentes dan un camino vacío.
Parámetros
NombreTipoPredeterminadoDescripción
x1, y1floatobligatorioCola.
x2, y2floatobligatorioPunta.
head_length / head_width / body_widthfloat | NoneNoneMedidas en unidades de escena; por defecto 0.18, 0.15 y 0.036.
max_head_ratiofloat | NoneNoneEn (0, 1], limita la punta respecto a la longitud total y escala su ancho en proporción.
Ejemplo
from gaanim import GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
arrow = scene.geometry.arrow(-3, 0, 3, 0).fill(GOLD).no_stroke()
scene.play([arrow.animate.create().duration(0.8)])
scene.render()
animation preview
FÁBRICAGeometry.double_arrowdouble_arrow(x1: float, y1: float, x2: float, y2: float, *, head_length: Optional[float] = None, head_width: Optional[float] = None, body_width: Optional[float] = None) -> Drawable
Flecha con punta en ambos extremos, para rangos y relaciones bidireccionales. Las puntas se acortan para no solaparse; un body_width fino sirve para cotas. Para una cota completa, con líneas de extensión, usa scene.mechanics.dimension. Medidas no positivas lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
x1, y1, x2, y2floatobligatorioExtremos.
head_length / head_width / body_widthfloat | NoneNoneIgual que en arrow: por defecto 0.18, 0.15 y 0.036.
Ejemplo
span = scene.geometry.double_arrow(-3, 0, 3, 0).fill(WHITE).no_stroke()
fine = scene.geometry.double_arrow(-3, -1, 3, -1, body_width=0.012).fill(WHITE).no_stroke()
FÁBRICAGeometry.curved_arrowcurved_arrow(x1: float, y1: float, x2: float, y2: float, angle: float, *, head_length: Optional[float] = None, head_width: Optional[float] = None, body_width: Optional[float] = None, max_head_ratio: Optional[float] = None) -> Drawable
Flecha curva para ciclos y rotaciones. Comparte medidas y validación con arrow, así que flechas rectas y curvas pueden compartir estilo. La punta se acorta en arcos cortos y se estrecha en radios pequeños.
Parámetros
NombreTipoPredeterminadoDescripción
x1, y1, x2, y2floatobligatorioInicio y final.
anglefloatobligatorioDesviación en radianes; el signo elige el lado de la curva.
head_length / head_width / body_width / max_head_ratiofloat | NoneNoneIgual que en arrow; la proporción se mide sobre la longitud del arco.
Ejemplo
from gaanim import GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
loop = scene.geometry.curved_arrow(-3, 0, 3, 0, 0.9).fill(WHITE)
bold = scene.geometry.curved_arrow(
    -3, -1.5, 3, -1.5, -0.9, head_length=0.3, head_width=0.24, body_width=0.06,
).fill(GOLD)
scene.play([loop.animate.create().duration(0.9), bold.animate.create().duration(0.9)])
scene.render()
animation preview
FÁBRICAGeometry.curved_arrow_arccurved_arrow_arc(cx: float, cy: float, radius: float, start_angle: float, sweep_angle: float, *, head_length: Optional[float] = None, head_width: Optional[float] = None, body_width: Optional[float] = None, max_head_ratio: Optional[float] = None) -> Drawable
Flecha a lo largo de un arco circular con centro (cx, cy), radio y ángulos en radianes. Acepta las mismas medidas que curved_arrow; max_head_ratio se mide sobre radius * abs(sweep_angle).
Ejemplo
turn = scene.geometry.curved_arrow_arc(0, 0, 2.5, 0.2, 1.8).fill(GOLD)
FÁBRICAGeometry.arcarc(cx: float, cy: float, radius: float, start_angle: float, sweep_angle: float) -> Drawable
Arco de circunferencia con centro (cx, cy); start_angle y sweep_angle en radianes. Nace sin relleno, como las demás líneas; fill() rellena el segmento entre la cuerda y el arco.
Ejemplo
from gaanim import GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
arc = scene.geometry.arc(0, 0, 0.75, 0.0, 2.0).no_fill().stroke(GOLD, 0.05)
scene.play([arc.animate.create().duration(0.8)])
scene.render()
animation preview
FÁBRICAGeometry.ellipse_arcellipse_arc(cx: float, cy: float, rx: float, ry: float, start_angle: float, sweep_angle: float) -> Drawable
Arco de elipse, para el borde de un cilindro, un cono o una probeta en perspectiva: media elipse es sweep_angle=math.pi. Nace sin relleno y admite tip, recortes y create(), como arc. Radios no positivos o valores no finitos lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
cx, cyfloatobligatorioCentro.
rx, ryfloatobligatorioRadios positivos en x y en y.
start_angle, sweep_anglefloatobligatorioRadianes, en sentido antihorario desde +x, como en arc.
Ejemplo
import math
from gaanim import GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
front = scene.geometry.ellipse_arc(0, -1.2, 1.6, 0.45, math.pi, math.pi).stroke(WHITE, 0.05)
back = scene.geometry.ellipse_arc(0, -1.2, 1.6, 0.45, 0, math.pi).stroke(WHITE, 0.03).opacity(0.4)
top = scene.geometry.ellipse_arc(0, 1.2, 1.6, 0.45, 0, 2 * math.pi).stroke(GOLD, 0.05)
left = scene.geometry.line(-1.6, -1.2, -1.6, 1.2).stroke(WHITE, 0.05)
right = scene.geometry.line(1.6, -1.2, 1.6, 1.2).stroke(WHITE, 0.05)
scene.play([shape.animate.create().duration(0.8) for shape in (front, back, top, left, right)])
scene.render()
animation preview
FÁBRICAGeometry.dashed_linedashed_line(p1: Endpoint, p2: Endpoint, *, dash_length: float = 0.16, gap_length: float = 0.10) -> Drawable
Línea discontinua para guías y aristas ocultas. Acepta los mismos extremos que line: dos puntos, objetos, PointRef o AnchorPoint, o cuatro coordenadas; entre objetos, los sigue en cada cuadro. Los guiones miden dash_length y los separa gap_length (0.16 y 0.10 por defecto). create() los dibuja uno tras otro desde el inicio.
Ejemplo
guide = scene.geometry.dashed_line(-1.75, 0, 1.75, 0, dash_length=0.15, gap_length=0.1).stroke(WHITE, 0.025)
dot = scene.geometry.dot(0.1).move_to(-1, 1.5)
link = scene.geometry.dashed_line(dot, (1.75, 0)).stroke(GOLD, 0.02)
scene.play([guide.animate.create().duration(0.8), dot.animate.shift_by(2, 0)])
FÁBRICAGeometry.connectorconnector(start: Endpoint, end: Endpoint, *, via: Optional[Sequence[Endpoint]] = None, head_length: float = 0.18, head_width: float = 0.15, body_width: float = 0.036, max_head_ratio: Optional[float] = None) -> Drawable
Flecha rellena y reactiva que sigue a sus extremos durante la animación y el seek. Usa via para los codos: no esquiva obstáculos. La punta se limita al último segmento no nulo. Referencias de otra escena lanzan ValueError. Anima el conector completo con create().
Parámetros
NombreTipoPredeterminadoDescripción
start / endEndpointobligatorioCola y punta, fijas o reactivas.
viaSequence[Endpoint] | NoneNonePuntos intermedios en orden.
head_length / head_width / body_widthfloat0.18 / 0.15 / 0.036Medidas positivas en unidades de escena.
Ejemplo
from gaanim import Anchor, Scene
scene = Scene(frame=(16, 9))
left = scene.geometry.rect(2, 1).move_to(-3, 0)
right = scene.geometry.rect(2, 1).move_to(3, 0)
link = scene.geometry.connector(left.anchor_point(Anchor.RIGHT), right.anchor_point(Anchor.LEFT))
scene.play(link.animate.create())
scene.play(right.animate.shift_by(0, 1))
scene.render()

Polígonos y símbolos

Polígonos arbitrarios y regulares, sectores y marcas habituales en diagramas y demostraciones.

FÁBRICAGeometry.polygonpolygon(points: Sequence[tuple[float, float]]) -> Drawable
Polígono cerrado con vértices arbitrarios.
Parámetros
NombreTipoPredeterminadoDescripción
pointsSequence[tuple[float, float]]obligatorioAl menos tres puntos finitos; el polígono se cierra solo.
Ejemplo
from gaanim import BLUE, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
tri = scene.geometry.polygon([(0, 0.875), (-0.81, -0.625), (0.81, -0.625)]).fill(BLUE).stroke(WHITE, 0.025)
scene.play([tri.animate.grow_from_center().duration(0.8)])
scene.render()
animation preview
FÁBRICAGeometry.regular_polygonregular_polygon(sides: int, radius: float) -> Drawable
Polígono regular de sides lados (al menos 3) inscrito en un círculo de radio radius. Con 6 lados obtienes un hexágono.
Ejemplo
from gaanim import BLUE, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
hexa = scene.geometry.regular_polygon(6, 0.75).fill(BLUE).stroke(WHITE, 0.025)
scene.play([hexa.animate.spin_in_from_nothing().duration(0.9)])
scene.render()
animation preview
FÁBRICAGeometry.starstar(points: int, outer_radius: float, inner_radius: float) -> Drawable
Estrella de points puntas (al menos 2) con radios exterior e interior.
Ejemplo
from gaanim import GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
star = scene.geometry.star(5, 0.875, 0.4).fill(GOLD).move_to(0, 0)
scene.play([star.animate.spin_in_from_nothing().duration(1.0)])
scene.render()
animation preview
FÁBRICAGeometry.sectorsector(cx: float, cy: float, radius: float, start_angle: float, sweep_angle: float) -> Drawable
Porción de círculo con centro (cx, cy); ángulos en radianes. Sirve para gráficos de tarta y cuñas de progreso.
Ejemplo
slice_ = scene.geometry.sector(0, 0, 0.875, 0.0, 2.0).fill(GOLD)
FÁBRICAGeometry.annulusannulus(outer_radius: float, inner_radius: float) -> Drawable
Anillo entre inner_radius (mayor que cero) y outer_radius (mayor que el interior). Útil para donuts, halos y dianas.
Ejemplo
ring = scene.geometry.annulus(0.75, 0.425).fill(BLUE).stroke(WHITE, 0.025)
FÁBRICAGeometry.bracebrace(x1: float, y1: float, x2: float, y2: float, height: float) -> Drawable
Llave entre (x1, y1) y (x2, y2) con profundidad height distinta de cero; el signo elige el lado. Se dibuja como una llave tipográfica: brazos rectos, rizos del tamaño de su profundidad y una punta, con un grosor que se afina hacia los extremos (16 % de height en los brazos, al menos 0.02). Es una figura rellena y sin trazo: fill le da color y un tema la pinta como la familia glyph, con el color del texto. Para una llave bajo una parte de un texto, usa selection.animate.brace(...) (ver Texto).
Ejemplo
from gaanim import WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
brace = scene.geometry.brace(-1, -0.25, 1, -0.25, 0.3).fill(WHITE)
label = scene.text("intervalo").move_to(0, -0.69)
scene.play([brace.animate.create().duration(0.7), label.animate.fade_in().duration(0.4)])
scene.render()
animation preview
FÁBRICAGeometry.checkmarkcheckmark(size: float) -> Drawable
Marca de verificación de tamaño size. Combínala con cross para respuestas correctas e incorrectas.
Ejemplo
ok = scene.geometry.checkmark(0.425).fill(GREEN)
FÁBRICAGeometry.crosscross(size: float) -> Drawable
Aspa de tamaño size para errores, rechazos o botones de cierre. No tiene relleno y toma el estilo de línea del tema, como line.
Ejemplo
no = scene.geometry.cross(0.425).stroke(WHITE, 0.05)
FÁBRICAGeometry.right_angleright_angle(arm_length: float) -> Drawable
Marca de ángulo recto con brazos de arm_length. Colócala en la esquina de un triángulo. No tiene relleno y toma el estilo de línea del tema, como line.
Ejemplo
corner = scene.geometry.right_angle(0.3).stroke(WHITE, 0.04).move_to(0, 0)

Trayectorias

Caminos libres: polilíneas, curvas de Bézier y comandos de cursor.

FÁBRICAGeometry.pathpath(definition: Sequence[CurvePoint] | Sequence[CurveCommand]) -> Drawable
Entrada compacta para geometría propia: una lista de puntos crea una polilínea y una lista de comandos, una curva (como curve).
Ejemplo
from gaanim import WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
rail = scene.geometry.path([(-1.75, 0), (0, 0.75), (1.75, 0)]).no_fill().stroke(WHITE, 0.05)
scene.play([rail.animate.create().duration(1.0)])
scene.render()
animation preview
FÁBRICAGeometry.polylinepolyline(points: Sequence[tuple[ScalarSource, ScalarSource]], *, closed: bool = False) -> Drawable
Polilínea que une los puntos en orden. Con coordenadas fijas, abierta o cerrada, points cambia sus vértices. Una coordenada puede ser un Parameter, un Computed o scene.time: entonces la polilínea se redibuja en cada fotograma con sus valores, lo que da forma a cualquier línea o contorno a partir de señales (la traza de un audio, un gráfico en vivo). Un punto cuyo valor no se puede leer queda fuera de ese fotograma.
Parámetros
NombreTipoPredeterminadoDescripción
pointsSequence[tuple[ScalarSource, ScalarSource]]obligatorioPuntos en orden, en coordenadas de la escena.
closedboolFalseUne el último punto con el primero.
Ejemplo
zig = scene.geometry.polyline([(-1.25, -0.375), (0, 0.375), (1.25, -0.375)]).no_fill().stroke(GOLD, 0.04)
wave = scene.geometry.polyline(
    [(x / 2, computed(lambda t, x=x: 0.5 * math.sin(x / 2 + 3 * t), inputs=[scene.time])) for x in range(-8, 9)]
).no_fill().stroke(GOLD, 0.04)
FÁBRICAGeometry.metaballsmetaballs(balls: Sequence[Drawable], *, threshold: float = 1.0, smoothness: float = 0.4) -> Drawable
Contorno de varias bolas fundidas como gotas de líquido (efecto gooey). Se recalcula en cada fotograma a partir de dónde están las bolas y de su tamaño, así que animarlas anima la masa; las bolas dejan de dibujarse (opacidad 0). El resultado es un trazado vectorial normal en coordenadas de la escena: nítido a cualquier tamaño, con relleno, trazo y efectos como cualquier otro. Una lista vacía, una bola de otra escena, un threshold no positivo o un smoothness negativo lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
ballsSequence[Drawable]obligatorioLos objetos que forman la masa; cada uno cuenta como un círculo con el centro de su caja y la mitad de su lado mayor.
thresholdfloat1.0Divide cada radio: por encima de 1 adelgaza las bolas.
smoothnessfloat0.4Distancia en unidades de escena a la que dos bolas se unen con un cuello; 0 es una unión simple.
Ejemplo
from gaanim import BLUE, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
drops = [scene.geometry.circle(r).move_to(x, 0) for x, r in ((-3, 0.9), (0, 1.1), (3, 0.7))]
blob = scene.geometry.metaballs(drops, smoothness=0.9).fill(BLUE)
scene.play([drops[0].animate.move_to(-1.2, 0.3).duration(1.5), drops[2].animate.move_to(1.3, -0.3).duration(1.5)])
scene.render()
animation preview
FÁBRICAGeometry.bezierbezier(start: tuple[float, float], controls: Sequence[tuple[float, float]], end: tuple[float, float]) -> Drawable
Curva de Bézier. Se conserva como Bézier, lo que la hace útil con la geometría reactiva sobre curvas.
Parámetros
NombreTipoPredeterminadoDescripción
start / endtuple[float, float]obligatorioExtremos.
controlsSequence[tuple[float, float]]obligatorioUno para una curva cuadrática, dos para una cúbica.
Ejemplo
from gaanim import WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
curve = scene.geometry.bezier((-1.75, 0), [(-0.625, 1.125), (0.625, -1.125)], (1.75, 0)).no_fill().stroke(WHITE, 0.04)
scene.play([curve.animate.create().duration(1.0)])
scene.render()
animation preview
FÁBRICAGeometry.curvecurve(commands: Sequence[CurveCommand]) -> Drawable
Curva compuesta con comandos de cursor, al estilo de Typst. Los controles de quad y cubic aceptan un punto, None (el control cae sobre su extremo) o "auto": en un quad refleja respecto al cursor el control del quad anterior, y en el primer control de un cubic, el segundo control del cubic anterior. Tras otro tipo de tramo, o como segundo control de un cubic, "auto" equivale a None. close y close_smooth llevan una lista vacía.
Parámetros
NombreTipoPredeterminadoDescripción
commandsSequence[CurveCommand]obligatorioPares (nombre, puntos): move, line, quad, cubic, close y close_smooth. Añade _rel a un comando de dibujo para usar puntos relativos al cursor.
Ejemplo
shape = scene.geometry.curve([
    ("move", [(0, 0)]),
    ("cubic", [(0.625, 0.75), (1.375, -0.75), (2, 0)]),
    ("line_rel", [(0, -1)]),
    ("close", []),
]).no_fill().stroke(WHITE, 0.04)

Grupos y marcos

Agrupa objetos para moverlos como uno o dibuja un marco vivo a su alrededor.

FÁBRICAGeometry.groupgroup(members: Sequence[Drawable]) -> Drawable
Agrupa objetos para moverlos, rotarlos y escalarlos como uno, conservando las coordenadas locales de cada miembro (también las de sus updaters). Los miembros ya visibles no se ocultan al unirse a un grupo que contiene un trazo o una fuerza diferidos. write, create y los fundidos del grupo son entradas explícitas de todo el subárbol. Para distribuir miembros usa Layout.
Ejemplo
from gaanim import BLUE, Direction, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
row = scene.geometry.group([scene.geometry.dot(0.125).fill(BLUE), scene.text("Etiqueta").move_to(0.9, 0)]).move_to(0, 0)
scene.play([row.animate.fade_in_from(Direction.DOWN, distance=0.3).duration(0.6)])
scene.render()
animation preview
FÁBRICAGeometry.surrounding_rectsurrounding_rect(targets: Drawable | TextSelection | Sequence[Drawable | TextSelection], *, padding: Padding = 0.12, corner_radius: float = 0.08) -> SurroundingRect
Marco vivo alrededor de los límites de sus objetivos en el mundo: los sigue al moverse, escalarse, rotar o reordenarse en un layout. Usa el trazo del tema y ningún relleno. Su posición y geometría pertenecen al vínculo: anima los objetivos o cambia de objetivo con retarget. Objetivos vacíos, de otra escena o medidas inválidas lanzan TypeError o ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
targetsDrawable | TextSelection | SequenceobligatorioUno o varios objetos, partes de texto o partes de ecuación.
paddingfloat | (v, h) | (t, r, b, l)0.12Margen no negativo alrededor de la unión.
corner_radiusfloat0.08Radio no negativo, limitado al tamaño del marco.
Ejemplo
from gaanim import GOLD, Scene, part
scene = Scene(frame=(16, 9), background="#0f172a")
eq = scene.text.equation("E =", part("mass", "m"), part("light", "c^2"))
frame = scene.geometry.surrounding_rect(eq["mass"]).stroke(GOLD, 0.04)
scene.play([eq.animate.fade_in(), frame.animate.create()])
scene.play([frame.retarget(eq["light"]).duration(0.8)])
scene.render()
animation preview
MÉTODOSurroundingRect.retargetretarget(targets: Drawable | TextSelection | Sequence[Drawable | TextSelection]) -> Anim
Devuelve un Anim que interpola los cuatro bordes del marco hacia nuevos objetivos y después los sigue. Origen y destino pueden seguir moviéndose durante la transición; los seeks reproducen la misma geometría. Configura el tiempo con duration y easing.
Ejemplo
equation = scene.text("$", part("lhs", "x + 3"), " = ", part("result", "7"), "$")
frame = scene.geometry.surrounding_rect(equation["lhs"])
scene.play([frame.retarget(equation["result"]).duration(0.9).easing(Easing.spring(stiffness=90, damping=12))])

Repetidores y distribuciones

Copias de una figura colocadas por una transformación acumulada o por una distribución, como el Repeater de After Effects o el Duplicator de Cavalry. El resultado es un grupo cuyos miembros son las copias, en orden: connect las recorre y el estilo del grupo llega a todas. La figura original es la copia 0; los grupos y textos se copian con sus miembros. count y animate.count muestran solo las primeras copias y funden la siguiente con la parte fraccionaria, para construir el patrón copia a copia.

FÁBRICAGeometry.repeatrepeat(shape, count, *, rotate=0.0, scale=1.0, offset=(0.0, 0.0), opacity=(1.0, 1.0), about=None) -> Drawable
count copias (1 a 10 000): la copia i se desplaza i * offset, gira i * rotate radianes y escala scale ** i, alrededor de about si se da (así los pétalos orbitan el centro de la flor) o de su propio pivote. Su opacidad va de opacity[0] en la primera a opacity[1] en la última. Valores no finitos, una escala no positiva, opacidades fuera de [0, 1] o una figura de otra escena lanzan ValueError.
Ejemplo
import math
from gaanim import CYAN, PINK, Scene
scene = Scene(frame=(16, 9), background="#0d1322")
petal = scene.geometry.ellipse(0.28, 1.0).fill(PINK).move_to(-3.5, 1.3)
flower = scene.geometry.repeat(petal, 12, rotate=math.tau / 12, about=(-3.5, 0), opacity=(1.0, 0.35))
square = scene.geometry.square(5.0).no_fill().stroke(CYAN, 0.05).move_to(3.5, 0)
tunnel = scene.geometry.repeat(square, 8, rotate=0.12, scale=0.82, opacity=(1.0, 0.2))
scene.play([flower.animate.rotate_by(1.0).duration(1.5), tunnel.animate.rotate_by(-0.6).duration(1.5)])
scene.render()
animation preview
FÁBRICAGeometry.duplicateduplicate(shape, distribution) -> Drawable
Una copia de shape centrada en cada punto de distribution; la figura original va al primero. Con una distribución orientada, cada copia gira para seguirla. Una figura de otra escena lanza ValueError.
Ejemplo
from gaanim import GOLD, Distribution, Scene
scene = Scene(frame=(16, 9), background="#0d1322")
arrow = scene.geometry.polygon([(-0.3, -0.2), (0.35, 0.0), (-0.3, 0.2)]).fill(GOLD)
ring = scene.geometry.duplicate(arrow, Distribution.circle(16, 2.6, orient=True))
scene.play([ring.animate.rotate_by(1.2).duration(1.5)])
scene.render()
animation preview
CLASEDistributionDistribution.grid(columns, rows, spacing=0.5, *, center=(0, 0)) · circle(count, radius=2.0, *, center=(0, 0), start=0.0, orient=False) · along(points, count, *, orient=False) · random(count, bounds=(-6, -3, 6, 3), *, seed=0) · phyllotaxis(count, spacing=0.2, *, center=(0, 0))
Dónde van las copias de Geometry.duplicate. grid: columns × rows puntos separados spacing (un número o (x, y)), fila a fila desde arriba a la izquierda. circle: puntos en una circunferencia desde start radianes en sentido antihorario; con orient cada copia mira hacia fuera. along: puntos repartidos por longitud a lo largo de una polilínea, extremos incluidos; con orient siguen su dirección. random: puntos uniformes dentro de (xmin, ymin, xmax, ymax), iguales para la misma seed. phyllotaxis: espiral de girasol, el punto i a spacing * sqrt(i) girado por el ángulo áureo. points devuelve las posiciones. Menos de 1 o más de 10 000 puntos, o valores no finitos, lanzan ValueError.
Ejemplo
grid = Distribution.grid(12, 7, 0.5)
seeds = scene.geometry.duplicate(scene.geometry.dot(0.04), Distribution.phyllotaxis(300, 0.15))
print(len(grid), grid.points[0])
84 (-2.75, 1.5)
FÁBRICAGeometry.connectconnect(points, max_distance=1.5, *, mode="range", neighbors=2, fade_by_distance=True) -> Drawable
Líneas vivas entre puntos, como el Connect Shape de Cavalry (efecto plexus). points es un grupo (sus miembros) o una lista de objetos; cada punto es el centro de sus límites y las líneas los siguen al moverse, en cada fotograma y búsqueda. mode elige los pares no más largos que max_distance: "range" une todos, "nearest" cada punto con sus neighbors más cercanos y "sequential" cada punto con el siguiente. Los puntos ocultos se saltan y los pares salen en un orden fijo. Con fade_by_distance las conexiones largas son más tenues. Por defecto son líneas blancas finas (o el estilo de línea del tema); cámbialas con stroke. Menos de dos puntos, un max_distance no positivo o neighbors menor que 1 lanzan ValueError.
Ejemplo
from gaanim import CYAN, Distribution, Scene
scene = Scene(frame=(16, 9), background="#060b16")
cloud = scene.geometry.duplicate(scene.geometry.dot(0.06).fill(CYAN), Distribution.random(40, (-6.5, -3.0, 6.5, 3.0), seed=7))
links = scene.geometry.connect(cloud, max_distance=2.2).stroke(CYAN, 0.025)
scene.play([cloud.animate.rotate_by(0.6).duration(2.0)])
scene.render()
animation preview

Operaciones booleanas

Unión, intersección, diferencia y diferencia simétrica de áreas vectoriales cerradas.

Los operandos se conservan como objetos independientes y el resultado copia el estilo que tiene el primero al crearlo (relleno, trazo, opacidad y efectos); los cambios de estilo posteriores de los operandos no lo cambian, ni siquiera con live=True. Con live=True, el resultado se reconstruye cuando cambian los caminos o las transformaciones de sus fuentes. tolerance es el error máximo, en unidades de escena, al aproximar las curvas; el valor predeterminado (0.0025, unos 0.3 px a 1080p) mantiene suaves los círculos. Súbelo solo para acelerar resultados live=True muy complejos. Debe ser finita y positiva. rule acepta "nonzero" o "evenodd"; operandos de otra escena o valores inválidos lanzan ValueError.

FÁBRICAGeometry.unionunion(*operands: Drawable, live: bool = False, tolerance: float = 0.0025, rule: Literal["nonzero", "evenodd"] = "nonzero") -> Drawable
Unión de al menos dos áreas.
Ejemplo
from gaanim import BLUE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
left = scene.geometry.circle(1).move_to(-0.5, 0)
right = scene.geometry.circle(1).move_to(0.5, 0)
merged = scene.geometry.union(left, right).fill(BLUE)
left.opacity(0)
right.opacity(0)
scene.play([merged.animate.create().duration(1.0)])
scene.render()
animation preview
FÁBRICAGeometry.intersectionintersection(*operands: Drawable, live: bool = False, tolerance: float = 0.0025, rule: Literal["nonzero", "evenodd"] = "nonzero") -> Drawable
Área común de al menos dos objetos.
Ejemplo
lens = scene.geometry.intersection(left, right).fill(GOLD)
FÁBRICAGeometry.differencedifference(subject: Drawable, *clips: Drawable, live: bool = False, tolerance: float = 0.0025, rule: Literal["nonzero", "evenodd"] = "nonzero") -> Drawable
Resta cada recorte del sujeto, de izquierda a derecha.
Ejemplo
from gaanim import GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
plate = scene.geometry.rect(3, 2).fill(GOLD)
hole = scene.geometry.circle(0.6).fill(WHITE).move_to(-0.8, 0)
cut = scene.geometry.difference(plate, hole, live=True)
plate.opacity(0)
hole.opacity(0.2)
scene.play([hole.animate.shift_by(1.6, 0).duration(1.2)])
scene.render()
animation preview
FÁBRICAGeometry.xorxor(*operands: Drawable, live: bool = False, tolerance: float = 0.0025, rule: Literal["nonzero", "evenodd"] = "nonzero") -> Drawable
Diferencia simétrica: las áreas cubiertas por un número impar de operandos.
Ejemplo
either = scene.geometry.xor(left, right, rule="evenodd")

Relleno por nivel

Llena una silueta hasta un porcentaje, como un depósito de agua o una barra de progreso con forma.

FÁBRICAGeometry.fill_levelfill_level(mask: Drawable, paint: Paint, level: Optional[ScalarSource] = None, *, direction: Literal["up", "down", "left", "right"] = "up", keep_outline: bool = True) -> Drawable
Genera un interior vectorial que se intersecta con la silueta en cada fotograma. Oculta la máscara original si su relleno no debe tapar el nivel. Con una fuente reactiva, las muestras finitas se limitan a [0, 1]; una muestra no finita genera un diagnóstico y usa el valor de respaldo. Anima el nivel con animate.fill_level(...); los saltos en la línea de tiempo restauran vínculos, valores y cortes.
Parámetros
NombreTipoPredeterminadoDescripción
maskDrawableobligatorioSilueta que recorta el relleno; conserva su visibilidad propia.
paintPaintobligatorioColor o pincel del interior.
levelfloat | Parameter | Variable | Computed | TimeInput | NoneNoneNivel en [0, 1]; None empieza vacío.
directionstr"up"Hacia dónde crece el relleno: up (desde abajo), down, left (desde la derecha) o right (desde la izquierda).
keep_outlineboolTrueAñade una copia reactiva que solo usa el trazo de la máscara.
Ejemplo
drop = scene.media.svg("drop.svg").scale_to(2.5).no_fill().stroke("#dbeafe", 0.05).opacity(0)
water = scene.geometry.fill_level(drop, "#38bdf8", 0.0)
scene.play([water.animate.fill_level(0.72).duration(1.4)])

Con una fuente reactiva, el nivel sigue a un parámetro que también puede mostrarse:

amount = scene.viz.parameter(0)
fraction = computed(lambda value: value / 100, inputs=[amount])
water = scene.geometry.fill_level(drop, "#38bdf8", fraction)
label = scene.viz.readout(amount, format=".1f", suffix="%")
scene.play(amount.animate.set(72), duration=1.4)
MÉTODODrawable.set_fill_levelset_fill_level(level: ScalarSource) -> Drawable
Cambia el nivel de un objeto creado con fill_level desde el cursor. Otra fuente reactiva reemplaza el vínculo; un número en [0, 1] lo termina con un corte reversible. Mientras esté vinculado, anima la fuente: animate.fill_level(...) se rechaza. Valores fijos inválidos, fuentes de otra escena u otros tipos de objeto lanzan ValueError.
Ejemplo
water.set_fill_level(0.4)

Geometría reactiva

Relaciones que se recalculan en el mismo fotograma: «este extremo es la esquina del rectángulo» o «este punto está al 35 % de la curva».

En lugar de recalcular una línea desde Python, declaras la relación y el motor regenera solo la geometría afectada después de aplicar las transformaciones de sus fuentes. La relación se evalúa para cualquier instante, así que el seek es exacto. Los objetos reactivos suelen empezar ocultos: anima su entrada (fade_in, create o write) junto con lo que los conduce. Las magnitudes reactivas (Parameter, Variable, Computed) están en Visualización y la guía Reactividad enseña a combinarlas.

Puntos de referencia

Puntos no dibujados que se aceptan en cualquier lugar donde se admite un Endpoint. Sus coordenadas aceptan números o fuentes reactivas.

FÁBRICAGeometry.point_refpoint_ref(x: _ReactiveScalar, y: _ReactiveScalar) -> PointRef
Punto de escena cuyas coordenadas pueden ser fuentes reactivas.
Ejemplo
x = scene.viz.parameter(-3)
probe = scene.geometry.point_ref(x, 1)
marker = scene.geometry.dot(0.1).fill(GOLD).follow(probe)
scene.play([marker.animate.fade_in(), x.animate.set(3).duration(1.5)])
FÁBRICAGeometry.offset_pointoffset_point(origin: Endpoint, dx: _ReactiveScalar, dy: _ReactiveScalar) -> PointRef
Punto desplazado (dx, dy) desde un origen móvil, en el espacio de la escena.
Ejemplo
body = scene.geometry.circle(0.4).move_to(-2, 0)
above = scene.geometry.offset_point(body, 0, 0.8)
tag = scene.text("m", role="label").follow(above)
FÁBRICAGeometry.point_betweenpoint_between(from_: Endpoint, to: Endpoint, *, alpha: _ReactiveScalar = 0.5, offset: tuple[float, float] = (0.0, 0.0)) -> PointRef
Punto afín entre dos extremos (alpha=0 en el primero, 1 en el segundo) más un desplazamiento en el mundo. alpha puede ser un número, un Parameter, una Variable o un Computed.
Ejemplo
a = scene.geometry.dot(0.1).move_to(-3, 0)
b = scene.geometry.dot(0.1).move_to(3, 1)
middle = scene.geometry.point_between(a, b, alpha=0.5, offset=(0, 0.3))
label = scene.text("M", role="label").follow(middle)
FÁBRICAGeometry.polar_pointpolar_point(origin: Endpoint, radius: _ReactiveScalar, angle: _ReactiveScalar) -> PointRef
Punto a distancia radius y ángulo angle (radianes) de un origen; ambos pueden ser reactivos.
Ejemplo
theta = scene.viz.parameter(0.0)
tip = scene.geometry.polar_point((0, 0), 1.5, theta)
hand = scene.geometry.tracking_line((0, 0), tip).stroke(WHITE, 0.05)
scene.play([hand.animate.create(), theta.animate.set(3.14).duration(1.5)])

Líneas y marcas que siguen a otros objetos

FÁBRICAGeometry.tracking_linetracking_line(from_: Endpoint, to: Endpoint) -> Drawable
Línea oculta cuyos extremos siguen a sus fuentes en el mismo fotograma y que conserva el progreso de create o write activos. Revélala con una animación de entrada.
Ejemplo
from gaanim import GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
tip = scene.geometry.dot(0.11).fill(GOLD).move_to(1.25, 0.375)
rod = scene.geometry.tracking_line((-1.25, -0.375), tip).no_fill().stroke(WHITE, 0.05)
scene.play([rod.animate.create().duration(0.8), tip.animate.shift_by(-0.5, 1).duration(0.8)])
scene.play([rod.animate.write().duration(0.8), tip.animate.shift_by(1, -0.625).duration(0.8)])
scene.render()
animation preview
FÁBRICAGeometry.point_on_curvepoint_on_curve(curve: Drawable, tracker: _ReactiveScalar) -> Drawable
Punto oculto que recorre la curva por longitud de arco, sin callbacks de Python durante la reproducción. Lee la curva tal como se dibuja: si se mueve, gira, escala o anima, o está dentro de un grupo como unos ejes, el punto sigue sobre ella. Lo mismo vale para tangent_on_curve, normal_on_curve y curvature_on_curve.
Parámetros
NombreTipoPredeterminadoDescripción
curveDrawableobligatorioPolilínea, Bézier, arco, círculo o polígono.
trackerfloat | Parameter | Variable | ComputedobligatorioPosición de 0 a 1 por longitud de arco; se limita a ese rango.
Ejemplo
from gaanim import GOLD, WHITE, Scene
from math import cos, sin, pi
scene = Scene(frame=(16, 9), background="#0f172a")
t = scene.viz.parameter(0.0)
curve = scene.geometry.polyline([(1.375*cos(u), 0.75*sin(2*u)) for u in (2*pi*i/240 for i in range(241))]).no_fill().stroke(WHITE, 0.025)
dot = scene.geometry.point_on_curve(curve, t).fill(GOLD)
scene.play([dot.animate.fade_in().duration(0.3), t.animate.set(1.0).duration(1.6)])
scene.render()
animation preview
FÁBRICAGeometry.tangent_on_curvetangent_on_curve(curve: Drawable, tracker: _ReactiveScalar, length: float = 0.8) -> Drawable
Segmento oculto de longitud length, centrado en el punto de la curva y orientado según la tangente. Usa el mismo muestreo por longitud de arco que point_on_curve.
Ejemplo
from gaanim import GOLD, WHITE, Scene
from math import cos, sin, pi
scene = Scene(frame=(16, 9), background="#0f172a")
t = scene.viz.parameter(0.35)
curve = scene.geometry.polyline([(1.375*cos(u), 0.75*sin(u)) for u in (2*pi*i/240 for i in range(241))]).no_fill().stroke(WHITE, 0.025)
tangent = scene.geometry.tangent_on_curve(curve, t, length=0.875).stroke(GOLD, 0.04)
scene.play([tangent.animate.fade_in().duration(0.3), t.animate.set(0.9).duration(1.4)])
scene.render()
animation preview
FÁBRICAGeometry.normal_on_curvenormal_on_curve(curve: Drawable, tracker: _ReactiveScalar, length: float = 0.8) -> Drawable
Como tangent_on_curve, girado 90° en sentido antihorario.
Ejemplo
t = scene.viz.parameter(0.2)
curve = scene.geometry.polyline([(math.cos(u / 40), math.sin(u / 40)) for u in range(252)])
normal = scene.geometry.normal_on_curve(curve, t, length=0.6).stroke(RED, 0.04)
FÁBRICAGeometry.curvature_on_curvecurvature_on_curve(curve: Drawable, tracker: _ReactiveScalar, window: float = 0.02) -> Drawable
Círculo osculador oculto, estimado a partir de tres muestras separadas window (una fracción de la longitud de arco). En los extremos de una curva abierta las muestras se desplazan hacia dentro, y en una curva cerrada pasan por la costura, así que el círculo existe también en 0 y en 1. En un tramo recto no se dibuja. Dale estilo con no_fill().stroke(...).
Ejemplo
from gaanim import RED, WHITE, Scene
from math import cos, sin, pi
scene = Scene(frame=(16, 9), background="#0f172a")
t = scene.viz.parameter(0.25)
curve = scene.geometry.polyline([(1.375*cos(u), 0.75*sin(u)) for u in (2*pi*i/240 for i in range(241))]).no_fill().stroke(WHITE, 0.025)
circle = scene.geometry.curvature_on_curve(curve, t).no_fill().stroke(RED, 0.025)
scene.play([circle.animate.fade_in().duration(0.3), t.animate.set(0.7).duration(1.4)])
scene.render()
animation preview
FÁBRICAGeometry.always_redraw_arcalways_redraw_arc(tracker: _ReactiveScalar, cx: float, cy: float, radius: float, start_angle: float, sweep_scale: float = 1.0, sweep_offset: float = 0.0) -> Drawable
Flecha en arco oculta que se regenera en cada fotograma a partir de un escalar, sin callbacks de Python. Ideal para marcar un ángulo que cambia. Como toda flecha sólida, fill la colorea entera; stroke fija su contorno.
Parámetros
NombreTipoPredeterminadoDescripción
trackerfloat | Parameter | Variable | ComputedobligatorioConduce el ángulo de barrido.
cx, cy, radius, start_anglefloatobligatorioCentro, radio y ángulo inicial en radianes.
sweep_scale / sweep_offsetfloat1.0 / 0.0El barrido es tracker * sweep_scale + sweep_offset.
Ejemplo
from gaanim import WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
theta = scene.viz.parameter(0.3)
rot = scene.geometry.always_redraw_arc(theta, 0, 0, 0.69, 0.0).fill(WHITE)
scene.play([rot.animate.fade_in().duration(0.3), theta.animate.set(5.0).duration(1.6)])
scene.render()
animation preview

Trazos

FÁBRICAGeometry.traced_pathtraced_path(source: Drawable, *, dissipating_time: Optional[float] = None, max_points: Optional[int] = None, min_distance: float = 0.01) -> Drawable
Estela 2D que dibuja el recorrido de un objeto. El muestreo empieza en el cursor donde se declara, así que los segmentos anteriores y los seeks no la rellenan de antemano. Revélala con trail.animate.fade_in(). Con dissipating_time las muestras caducan por la cola en la vista previa, los seeks, las capturas y la exportación. Tras un seek, la estela se reconstruye si la fuente la mueven updaters, drive_from_samples, vínculos reactivos o follow; una fuente movida solo con .animate empieza una estela nueva.
Parámetros
NombreTipoPredeterminadoDescripción
sourceDrawableobligatorioObjeto cuya posición se muestrea.
dissipating_timefloat | NoneNoneSegundos que dura cada muestra en la estela; positivo.
max_pointsint | NoneNoneLímite positivo de muestras retenidas.
min_distancefloat0.01Distancia mínima entre muestras.
Ejemplo
from gaanim import GOLD, RED, Scene, Updater
scene = Scene(frame=(16, 9), background="#0f172a")
dot = scene.geometry.dot(0.09).fill(GOLD).move_to(1.5, 0)
dot.add_updater(Updater.orbit(0, 0, 1.5, 2.0))
trail = scene.geometry.traced_path(dot, dissipating_time=1.0).stroke(RED, 0.04).no_fill()
scene.play([trail.animate.fade_in().duration(0.2)])
scene.wait(1.6)
scene.render()
animation preview

Dibujos axonométricos

Para planos técnicos en isométrica o dimétrica, con el estilo plano del resto de la escena. scene.geometry.axonometric(...) devuelve una proyección que dibuja puntos de un modelo 3D como polígonos y polilíneas 2D normales: se rellenan, se trazan y se animan como cualquier otro Drawable. No usa el 3D experimental de la sección siguiente.

El modelo tiene z hacia arriba y su planta en x/y, como un modelo estructural. Un metro del eje menos acortado mide scale unidades de escena, así que la isométrica conserva la longitud de los tres ejes, como un dibujo isométrico, y el origen del modelo se dibuja en origin. Los trazos de estos dibujos van centrados en el contorno, de modo que una cara vista de canto (un muro en planta) se sigue viendo como línea.

depth_sort ordena las caras como un pintor: donde dos dibujos se solapan en pantalla, el más cercano queda encima. Llámalo antes de mostrarlos, como z_index, con la vista en la que importa el orden (view= toma la de otra proyección, como la vista final de una animación): el orden no se recalcula al cambiar de vista. Tres caras que se tapan en ciclo, o dos que se atraviesan, no tienen un orden exacto; parte esas caras en piezas. animate_to lleva todos los dibujos de la proyección a otra vista, vértice a vértice, y la proyección dibuja desde entonces con esa vista.

from gaanim import Scene

scene = Scene(frame=(16, 9), background="#f5f1ea")
plan = scene.geometry.axonometric("plan", origin=(-2.5, -2), scale=0.9)
iso = scene.geometry.axonometric("isometric", origin=(-2.5, -2), scale=0.9)
H = 2.5
faces = [
    plan.polygon([(0, 0, 0), (5, 0, 0), (5, 4, 0), (0, 4, 0)]).fill("#d8d2c6"),
    plan.polygon([(0, 4, 0), (5, 4, 0), (5, 4, H), (0, 4, H)]).fill("#c46a45"),
    plan.polygon([(0, 0, 0), (0, 4, 0), (0, 4, H), (0, 0, H)]).fill("#9c4f32"),
    plan.polygon([(0, 0, H), (5, 0, H), (5, 2, H), (0, 2, H)]).fill("#9aa3ad"),
]
for face in faces:
    face.stroke("#2b2b2b", 0.03)
columns = [plan.line((x, y, 0), (x, y, H)).stroke("#4e535c", 0.06) for x, y in [(5, 2), (5, 4)]]
plan.depth_sort(faces + columns, z_index=10, view=iso)
scene.wait(0.4)
scene.play(plan.animate_to(iso).defaults(duration=1.6))
scene.wait(0.6)
scene.render()
animation preview
FÁBRICAGeometry.axonometricaxonometric(view: Literal["isometric", "dimetric", "plan", "front", "side"] = "isometric", *, azimuth: Optional[float] = None, elevation: Optional[float] = None, origin: tuple[float, float] = (0.0, 0.0), scale: float = 1.0) -> Axonometric
Un ángulo u origen no finito, un scale que no sea positivo u otro nombre de vista lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
viewstr"isometric""isometric", "dimetric" (la 2:1, cuyos ejes que se alejan suben una unidad cada dos), "plan", "front" (mirando hacia +y) o "side" (desde +x).
azimuthfloat | NoneNoneRadianes que la vista gira alrededor de z desde la vista frontal; sustituye al de view.
elevationfloat | NoneNoneRadianes que la vista sube sobre el horizonte; negativa, se ve desde abajo. Sustituye a la de view.
origintuple[float, float](0, 0)Punto de la escena donde se dibuja el origen del modelo.
scalefloat1Unidades de escena por unidad del modelo en el eje menos acortado.
Devuelve: Axonometric — Proyección que dibuja y anima puntos del modelo.
Ejemplo
iso = scene.geometry.axonometric("isometric", origin=(0, -1), scale=0.5)
below = scene.geometry.axonometric("isometric", elevation=-0.5)
MÉTODOAxonometric.polygonpolygon(points: Sequence[tuple[float, float, float]]) -> Drawable
Menos de tres puntos o una coordenada no finita lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
pointsSequence[tuple[float, float, float]]obligatorioVértices en coordenadas del modelo, como una cara.
Devuelve: Drawable — Un polygon en coordenadas de escena.
Ejemplo
wall = iso.polygon([(0, 0, 0), (5, 0, 0), (5, 0, 3), (0, 0, 3)]).fill(GRAY)
MÉTODOAxonometric.polylinepolyline(points: Sequence[tuple[float, float, float]], *, closed: bool = False) -> Drawable
Menos de dos puntos o una coordenada no finita lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
pointsSequence[tuple[float, float, float]]obligatorioPuntos del modelo, en orden.
closedboolFalseUne el último punto con el primero.
Devuelve: Drawable — Una polyline en coordenadas de escena.
MÉTODOAxonometric.lineline(start: tuple[float, float, float], end: tuple[float, float, float]) -> Drawable
Parámetros
NombreTipoPredeterminadoDescripción
starttuple[float, float, float]obligatorioUn extremo, en el modelo.
endtuple[float, float, float]obligatorioEl otro extremo.
Devuelve: Drawable — Una polilínea de dos puntos, que animate.points también mueve.
MÉTODOAxonometric.pointpoint(x: float, y: float, z: float) -> tuple[float, float]
Sirve para colocar etiquetas y cotas junto al dibujo; un texto colocado así no sigue a animate_to.
Parámetros
NombreTipoPredeterminadoDescripción
xfloatobligatorioCoordenadas del punto del modelo.
yfloatobligatorio
zfloatobligatorio
Devuelve: tuple[float, float] — Dónde se dibuja, en unidades de escena, con la vista actual.
Ejemplo
label = scene.text("A").move_to(*iso.point(0, 0, 3))
MÉTODOAxonometric.depthdepth(x: float, y: float, z: float) -> float
Parámetros
NombreTipoPredeterminadoDescripción
xfloatobligatorioCoordenadas del punto del modelo.
yfloatobligatorio
zfloatobligatorio
Devuelve: float — Cuán cerca del observador está el punto: mayor es más cerca.
MÉTODOAxonometric.depth_sortdepth_sort(drawables: Sequence[Drawable], *, z_index: int = 0, view: Optional[Axonometric] = None) -> None
Asigna z_index de atrás hacia delante en la vista actual o en la de view. Donde dos dibujos se solapan en pantalla, el más cercano allí queda encima; el resto sigue su profundidad media, aunque solo se toquen en un vértice o una arista. Un dibujo de otra proyección, uno repetido o un z_index sin sitio para todos lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
drawablesSequence[Drawable]obligatorioDibujos hechos con esta proyección.
z_indexint0z_index del dibujo más lejano; cada siguiente suma uno.
viewAxonometric | NoneNoneOrdena según la vista de esta proyección en lugar de la actual.
Devuelve: None
Ejemplo
floor = iso.polygon([(0, 0, 0), (4, 0, 0), (4, 4, 0), (0, 4, 0)])
slab = iso.polygon([(0, 0, 3), (4, 0, 3), (4, 4, 3), (0, 4, 3)])
column = iso.line((0, 4, 0), (0, 4, 3))
iso.depth_sort([slab, column, floor], z_index=10)
MÉTODOAxonometric.animate_toanimate_to(other: Axonometric) -> Composition
Lleva cada vértice en línea recta a donde lo dibuja la vista de other; fija la duración con defaults(duration=...). Desde entonces esta proyección dibuja con esa vista, y other no se lleva los dibujos. Una proyección que no ha dibujado nada lanza ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
otherAxonometricobligatorioProyección cuya vista se adopta.
Devuelve: Composition — Un animate.points por dibujo, en paralelo.
Ejemplo
plan = scene.geometry.axonometric("plan")
scene.play(iso.animate_to(plan).defaults(duration=1.5))
PROPIEDADAxonometric.azimuthazimuth: float
Devuelve: float — Giro de la vista actual alrededor de z, en radianes. elevation, origin y scale dan el resto de la vista.

Geometría 3D

Primitivas iluminadas con Material3D, líneas en el espacio y estelas 3D. Gaanim las dibuja con Vello: proyecta cada triángulo con la cámara, los ordena de atrás hacia delante y calcula la luz en la CPU. No hay sombras ni búfer de profundidad, así que las superficies que se cruzan pueden mostrar errores de orden, y todo el 3D queda debajo del contenido 2D. Gaanim usa un mundo dextrógiro con Y hacia arriba: cilindros y conos crecen a lo largo de Y y los planos yacen en XZ. Los puntos 3D son tuplas (x, y, z). La guía Cámara y 3D explica la cámara en perspectiva.

Primitive3D extiende Drawable, así que posición, rotación, escala, opacidad y material se animan y son deterministas al hacer seek. Los parámetros de geometría quedan fijos tras la construcción. create() hace crecer la malla desde su centro mientras aparece; write(), que es solo vectorial, se rechaza.

from gaanim import BLUE, GOLD, Material3D, Scene

scene = Scene(frame=(16, 9))
cube = scene.geometry.cube(2, material=Material3D.matte(BLUE))
sphere = scene.geometry.sphere(1, segments=32, rings=16,
                      material=Material3D.metal(GOLD)).move_to_3d(3, 0, 0)
floor = scene.geometry.plane(10, 8, subdivisions=(4, 4)).move_to_3d(0, -2, 0)
scene.geometry.lighting_3d("studio", intensity=1.0)
scene.camera.look_at(eye=(6, 4, 8), target=(1, 0, 0))
scene.play([cube.animate.create(), sphere.animate.create(), floor.animate.fade_in()])
scene.play([cube.animate.material(Material3D.metal(GOLD)).duration(1.0)])
scene.render()
FÁBRICAGeometry.cubecube(size: float = 2.0, *, material: Optional[Material3D] = None) -> Primitive3D
Cubo centrado de caras planas.
FÁBRICAGeometry.spheresphere(radius: float = 1.0, *, segments: int = 32, rings: int = 16, material: Optional[Material3D] = None) -> Primitive3D
Esfera UV con sombreado suave; segments y rings controlan la resolución.
FÁBRICAGeometry.cylindercylinder(radius: float = 1.0, height: float = 2.0, *, segments: int = 32, caps: bool = True, material: Optional[Material3D] = None) -> Primitive3D
Cilindro a lo largo de Y; caps=False lo deja abierto.
FÁBRICAGeometry.conecone(radius: float = 1.0, height: float = 2.0, *, segments: int = 32, cap: bool = True, material: Optional[Material3D] = None) -> Primitive3D
Cono a lo largo de Y; cap=False quita la base.
FÁBRICAGeometry.planeplane(width: float = 2.0, height: float = 2.0, *, subdivisions: tuple[int, int] = (1, 1), material: Optional[Material3D] = None) -> Primitive3D
Plano en XZ con normales hacia arriba, subdividido en subdivisions celdas.
MÉTODOGeometry.lighting_3dlighting_3d(preset: Literal["studio", "none"] = "studio", intensity: float = 1.0, shadows: bool = True) -> None
Configura el único equipo automático de luces 3D de la escena. "studio" ilumina las primitivas con material al estilo de Gaanim (luz del cielo y del suelo, una luz principal suave desde arriba a la izquierda y un brillo leve en los bordes), escalada por intensity; "none" deja solo la emisión. shadows se acepta por compatibilidad, pero el contenido 3D no proyecta sombras. Las superficies, los gráficos 3D y las líneas no reciben luz. Las luces de light_3d sustituyen a la luz principal.
FÁBRICAGeometry.light_3dlight_3d(kind="point", *, color=None, intensity=1.0) -> Drawable
Una luz para las primitivas con material, en el origen. No dibuja nada; es un dibujable, así que move_to_3d la coloca y animate, los grupos y follow la mueven. Una luz "point" alumbra desde su posición en todas direcciones; una "directional", con rayos paralelos desde la dirección de su posición vista desde el origen, como el sol. Mientras la escena muestra luces, sustituyen a la luz principal fija de lighting_3d, cuya luz del cielo y del suelo se mantiene, y cada una alumbra en su color (blanco por defecto) con su intensity, atenuada por su opacidad: fade_in y fade_out la encienden y la apagan. Con lighting_3d("none") solo alumbran las luces. Las superficies, los gráficos 3D y las líneas no reciben luz. Una intensity negativa o no finita, u otro kind, lanzan ValueError.
Ejemplo
lamp = scene.geometry.light_3d(color=GOLD, intensity=1.5).move_to_3d(3, 2, 4)
scene.play([lamp.animate.move_to_3d(-3, 2, 4).duration(2.0)])
CLASEMaterial3D.matte / metal / emissive / color / roughness / metallic / emissive_color / emissive_strengthMaterial3D(color=WHITE, roughness=0.55, metallic=0.0, emissive=None, emissive_strength=0.0)
Material de una primitiva 3D iluminada, sombreado por Gaanim: la rugosidad y la metalicidad dan forma al brillo y la emisión se suma a la luz. Sus propiedades numéricas se interpolan en espacio lineal y cuyos rangos se validan. Los atajos Material3D.matte(color), Material3D.metal(color) y Material3D.emissive(color, strength=1.0) cubren los aspectos habituales. Las propiedades color, roughness, metallic, emissive_color y emissive_strength son de solo lectura.
Ejemplo
glass = Material3D(color="#93c5fd", roughness=0.1, metallic=0.0)
lamp = Material3D.emissive(GOLD, strength=2.0)
FÁBRICAGeometry.extrudeextrude(source: Drawable, depth: float = 0.2, *, bevel: float = 0.0, material: Optional[Material3D] = None, tolerance: float = 0.01, keep_source: bool = False) -> Primitive3D
El contorno se lee al compilar la escena, así que funcionan textos, ecuaciones, grupos y SVG importados, y se rellena como se dibuja (regla nonzero): las letras conservan sus huecos. La malla queda centrada donde está el origen en el plano z = 0, con la tapa frontal hacia la cámara predeterminada. Los trazos de menos del doble del chaflán conservan aristas rectas. El origen sigue siendo un drawable propio, oculto desde el cursor (el 2D se dibuja encima del 3D) salvo con keep_source; cambiarlo después no altera la malla. Gira la cámara para ver la profundidad. Un origen de otra escena, 3D, imagen, video o Lottie, una depth o tolerance no positiva o un bevel negativo o de al menos la mitad de depth lanzan ValueError; un contorno sin área avisa y deja la malla vacía.
Parámetros
NombreTipoPredeterminadoDescripción
sourceDrawableobligatorioDrawable 2D cuyo relleno se extruye: forma, texto, ecuación, grupo o SVG.
depthfloat0.2Profundidad de la malla, centrada en z = 0.
bevelfloat0.0Ancho del chaflán a 45° en ambas tapas; menor que la mitad de depth.
materialMaterial3D | NoneNoneMaterial; sin él, mate del color de relleno del origen.
tolerancefloat0.01Tolerancia al aplanar las curvas, en unidades de escena; menor es más suave y pesado.
keep_sourceboolFalseMantiene visible el drawable de origen.
Devuelve: Primitive3D — Malla cerrada con tapas, paredes y chaflán.
Ejemplo
from gaanim import *

scene = Scene(frame=(16, 9))
word = scene.geometry.extrude(scene.text("HOLA", size=1.6), depth=0.4, bevel=0.04,
                              material=Material3D.metal(GOLD))
star = scene.geometry.extrude(scene.geometry.star(5, 1.2, 0.5).fill(BLUE).move_to(4.5, 0), depth=0.3)
scene.camera.perspective(fov_y=0.785)
scene.camera.look_at(eye=(2, 3, 12), target=(1, 0, 0))
scene.play([word.animate.create(), star.animate.create()])
scene.play([scene.camera.animate.orbit(delta_yaw=0.6, delta_pitch=0.0).duration(2)])
scene.render()
animation preview
MÉTODOPrimitive3D.materialmaterial(material: Material3D) -> Self
Cambia el material al instante; .animate.material(...) lo interpola.
FÁBRICAGeometry.polyline_3dpolyline_3d(points: Sequence[tuple[float, float, float]], color: Optional[ColorLike] = None, *, colors: Optional[Sequence[Color]] = None, colormap: Optional[str] = None) -> Drawable
Tira de líneas 3D en el espacio del mundo. La lista colors debe tener la misma longitud que points.
Parámetros
NombreTipoPredeterminadoDescripción
pointsSequence[tuple[float, float, float]]obligatorioAl menos dos puntos finitos del mundo.
colorColor | NoneNoneColor uniforme.
colorsSequence[Color] | NoneNoneUn color por punto; tiene prioridad sobre colormap.
colormapstr | NoneNoneDegradado ordenado a lo largo de la línea, por ejemplo "inferno", "viridis" o "plasma".
Ejemplo
points = [(math.cos(t / 8), math.sin(t / 8), t / 40 - 1) for t in range(81)]
helix = scene.geometry.polyline_3d(points, colormap="inferno")
FÁBRICAGeometry.traced_path_3dtraced_path_3d(source: Drawable, *, colormap: Optional[str] = None, dissipating_time: Optional[float] = None, max_points: Optional[int] = None, min_distance: float = 0.1) -> Drawable
Estela 3D de la posición de un objeto en el mundo. Funciona con Updater y add_updater_fn; el muestreo empieza donde se declara. Revélala con fade_in o create. dissipating_time hace caducar la cola, max_points limita la memoria y min_distance (0.1 por defecto) filtra muestras casi iguales. Admite los mapas "inferno", "viridis" y "plasma".
Ejemplo
dot = scene.geometry.dot(0.09).move_to_3d(1, 0, 0)
dot.add_updater(Updater.orbit(0, 0, 1, 1.5))
trail = scene.geometry.traced_path_3d(
    dot, colormap="viridis", dissipating_time=2.0, max_points=600
)
scene.play([trail.animate.fade_in()])

Este ejemplo combina ejes 3D, una polilínea, un punto que orbita con su estela y la cámara en perspectiva:

from gaanim import Axis, BLACK, GOLD, RED, WHITE, Scene, Updater
scene = Scene(frame=(16, 9), background=BLACK)

axes = scene.viz.cartesian_3d(
    Axis.linear(-3, 3).ticks(1).label("x").style(color=WHITE),
    Axis.linear(-3, 3).ticks(1).label("y").style(color=WHITE),
    Axis.linear(-2, 2).ticks(1).label("z").style(color=WHITE),
    size=(6, 6, 4),
)
path = scene.geometry.polyline_3d([(-2, -1, -1), (0, 1, 0), (2, -1, 1)], color=RED)
dot = scene.geometry.dot(0.1).fill(GOLD).move_to_3d(1, 0, 0).billboard()
dot.add_updater(Updater.orbit(0, 0, 1, 1.2))
trail = scene.geometry.traced_path_3d(dot, colormap="viridis", max_points=120)

scene.camera.perspective(fov_y=0.785, near=0.1, far=100.0)
scene.camera.look_at(eye=(7, 5, 6), target=(0, 0, 0))
scene.play([
    axes.animate.create().duration(0.8),
    path.animate.create().duration(0.8),
    dot.animate.fade_in().duration(0.4),
    trail.animate.fade_in().duration(0.4),
])
scene.play([scene.camera.animate.orbit(delta_yaw=0.5, delta_pitch=0.1).duration(0.8)])
scene.play([scene.camera.animate.dolly(factor=0.85).duration(0.5)])
scene.wait(0.5)
scene.render()