Recetario de estilos
Escenas completas para copiar: fichas, encuestas, sala de espera, carreras, batallas y podios
Cada receta es una escena completa que puedes guardar como main.py, presentar tal cual y cambiar a tu gusto. Todas usan el ensayo para que la vista previa muestre un juego en marcha; al presentar, los mismos dibujos siguen al público real.
Las recetas se combinan: una trivia completa puede empezar con la sala de espera, seguir con fichas de colores en cada pregunta, pasar por una batalla de equipos y terminar en el podio. Usa un segmento por diapositiva (scene.segment) y una pausa donde el presentador hable.
Fichas de colores
El estilo clásico: cuatro fichas con el color y la forma de cada respuesta en el teléfono, una cuenta atrás y una pausa que avanza sola cuando todos respondieron. Al revelar, las fichas se encogen a una fila con magic_move, aparecen los votos y la correcta brilla.
from gaanim import Scene, magic_move
INK, WHITE, GOLD = "#1c0b3f", "#fff8ff", "#ffd23f"
scene = Scene(frame=(16, 9), background=INK)
scene.rehearsal(16, seed=4)
quiz = scene.quiz("¿Qué planeta es el más caliente?", ["Mercurio", "Venus", "Marte", "Júpiter"],
correct=1, time=20, rehearse=0.45)
def tile(i, width, height, size):
"""Una respuesta con su color y su forma, con nombre para magic_move."""
box = scene.geometry.rounded_rect(width, height, height / 4).fill(quiz.color(i)).no_stroke()
x = -width / 2 + height / 2
badge = scene.geometry.circle(height * 0.32).fill(WHITE).no_stroke().move_to(x, 0)
icon = quiz.icon(i, height * 0.36).move_to(x, 0)
label = scene.text(quiz.options[i], size=size, color=WHITE, weight=700, text_box="cap")
label.move_to(height * 0.35, 0)
return scene.geometry.group([box, badge, icon, label]).named(f"respuesta{i}")
title = scene.text(quiz.question, size=0.6, color=WHITE, weight=700).move_to(0, 3.4)
clock = scene.viz.readout(quiz.remaining(), format=".0f", color=GOLD, font_size=0.7).move_to(7, 3.4)
tiles = [tile(i, 7.4, 1.8, 0.55).move_to(-3.9 + i % 2 * 7.8, 0.9 - i // 2 * 2.2) for i in range(4)]
scene.play([title.animate.fade_in(), clock.animate.fade_in()]
+ [t.animate.grow_from_center().duration(0.4).delay(0.08 * i).settle()
for i, t in enumerate(tiles)])
scene.stop("respondiendo", until=quiz.answered(share=1.0) | quiz.time_up())
quiz.reveal()
# Las fichas se encogen a una fila y aparecen los votos.
COLUMNS = [-5.7, -1.9, 1.9, 5.7]
row = [tile(i, 3.6, 0.9, 0.3).move_to(COLUMNS[i], 0.6) for i in range(4)]
before, after = scene.geometry.group(tiles), scene.geometry.group(row)
scene.play([magic_move(before, after).duration(0.8), clock.animate.fade_out()])
reveal = []
for i, x in enumerate(COLUMNS):
bar = quiz.bar(i, length=2.8, thickness=0.16, radius=0.08, scale="total")
bar.fill(WHITE).no_stroke().move_to(x - 0.3, -0.2)
votes = scene.viz.readout(quiz.votes(i), format=".0f", color=WHITE, font_size=0.3)
reveal += [bar.animate.create(), votes.move_to(x + 1.45, -0.2).animate.fade_in()]
if i == quiz.correct:
reveal.append(row[i].animate.glow(quiz.color(i), radius=0.4, intensity=1.3))
else:
reveal.append(row[i].animate.opacity(0.3))
scene.play(reveal)
scene.wait(2)
scene.stop("respuesta")
scene.render()
Los teléfonos ordenan y colorean las respuestas de un cuestionario a su manera para que nadie copie al vecino, así que en un cuestionario la ficha de la pantalla no coincide con la del teléfono; en una encuesta sí.
Columnas de una encuesta
Una encuesta sin respuesta correcta: columnas que crecen con el porcentaje de cada respuesta y el total de teléfonos que votaron.
from gaanim import Scene
INK, WHITE, MUTED = "#0f172a", "#f8fafc", "#94a3b8"
scene = Scene(frame=(16, 9), background=INK)
scene.rehearsal(30, seed=2)
poll = scene.poll("¿Cómo prefieres estudiar?", ["Solo", "En grupo", "Con música", "En silencio"],
rehearse=[2, 5, 4, 1])
scene.text(poll.question, size=0.6, color=WHITE, weight=700).move_to(0, 3.5)
for i, answer in enumerate(poll.options):
x = -5.4 + 3.6 * i
poll.bar(i, length=5, thickness=2.4, direction="up", radius=0.2) \
.fill(poll.color(i)).no_stroke().move_to(x, -0.4)
scene.viz.readout(poll.percent(i), format=".0f", suffix=" %", color=WHITE,
font_size=0.45).move_to(x - 0.5, 2.6)
poll.icon(i, 0.4).move_to(x - 0.9, -3.3)
scene.text(answer, size=0.36, color=WHITE, text_box="cap").move_to(x + 0.3, -3.3)
scene.viz.readout(poll.total(), format=".0f", prefix="votaron ", color=MUTED,
font_size=0.3).move_to(0, -4)
scene.wait(4)
scene.stop("votando")
scene.render()
Con el valor por defecto de scale, "leader", la respuesta que va ganando llena su columna y las demás miden en proporción, lo que se lee bien aunque voten pocos; con scale="total" cada columna mide su porcentaje. Una lectura (readout) se ancla por la izquierda para no saltar cuando cambian los dígitos, así que para centrarla bajo algo córrela un poco a la izquierda. poll.percent(i) sigue en vivo también al presentar un .gaanim.
Sala de espera con personajes
Mientras el público escanea el QR, cada jugador cae desde arriba como el personaje que armó en su teléfono y rebota al aterrizar. La pausa avanza sola cuando hay al menos 8.
from gaanim import Scene
from gaanim.live import ease_in, impact, lerp, pose
INK, WHITE, GOLD = "#1c0b3f", "#fff8ff", "#ffd23f"
scene = Scene(frame=(16, 9), background=INK)
scene.rehearsal(14, seed=5, arrive=5)
audience = scene.audience()
scene.geometry.rounded_rect(3.6, 3.6, 0.3).fill(WHITE).no_stroke().move_to(-5.6, 0.6)
audience.qr(3.1).fill(INK).move_to(-5.6, 0.6)
scene.text(audience.code, size=0.7, color=GOLD, weight=800).move_to(-5.6, -1.8)
scene.viz.readout(audience.count(), format=".0f", suffix=" en la sala", color=WHITE,
font_size=0.35).move_to(-6.8, -2.6)
def llegar(p):
x = -1.8 + p.index % 6 * 1.7
y = 1.4 - p.index // 6 * 2.0
caida = ease_in(min(p.t / 0.5, 1)) # cae medio segundo
sx, sy = impact(p.t - 0.5) # y se aplasta al tocar el suelo
return pose(x, lerp(6, y, caida), sx=sx, sy=sy, express="happy", since=0.5)
scene.live_zone(audience, llegar, size=1.1, names=True, name_size=0.22)
scene.wait(7)
scene.stop("sala", until=audience.at_least(8))
scene.render()
p.index es el orden de llegada y p.t los segundos desde que llegó, así que cada uno hace su entrada a su tiempo. Con names=True la zona dibuja el apodo bajo los pies. Para más movimientos, mira Zonas vivas.
Cada uno corre a su respuesta
Después de revelar, cada personaje corre bajo la respuesta que eligió: los que acertaron saltan, los demás se lamentan y quienes no respondieron se quedan en una esquina.
import math
from gaanim import Scene
from gaanim.live import ease_out_back, impact, lerp, pose, progress, smoothstep
INK, WHITE = "#1c0b3f", "#fff8ff"
COLUMNS = (-4.8, 0, 4.8)
scene = Scene(frame=(16, 9), background=INK)
scene.rehearsal(18, seed=3)
audience = scene.audience()
quiz = scene.quiz("¿Cuántos huesos tiene un adulto?", ["186", "206", "306"], correct=1,
rehearse=0.55)
for i, x in enumerate(COLUMNS):
scene.geometry.rounded_rect(4.2, 1.1, 0.25).fill(quiz.color(i)).no_stroke().move_to(x, 2.9)
scene.text(quiz.options[i], size=0.55, color=WHITE, weight=700, text_box="cap").move_to(x, 2.9)
scene.wait(1)
scene.stop("respondiendo")
quiz.reveal()
def correr(p):
if p.answer < 0: # no respondió
return pose(-7.2 + 0.4 * p.index, -4, scale=0.6, express="sad", loop=True)
x = COLUMNS[0] + (COLUMNS[1] - COLUMNS[0]) * p.answer + 3 * (p.random(1) - 0.5)
y = 0.8 - 3.8 * p.random(2)
salida = 0.4 * p.random(3)
carrera = progress(p.t, salida, 0.8)
if carrera < 1: # corre desde abajo
return pose(lerp(16 * p.random(4) - 8, x, ease_out_back(carrera)),
lerp(-6, y, smoothstep(carrera)) + math.sin(math.pi * carrera),
express="surprised", since=0)
llego = p.t - salida - 0.8
if p.answer_points > 0: # acertó: salta sin parar
beat = (llego + p.random(5)) % 0.8 / 0.8
return pose(x, y + 1.6 * beat * (1 - beat), express="winner", loop=True)
sx, sy = impact(llego, amount=0.4)
return pose(x, y, sx=sx, sy=sy, express="sad", since=salida + 0.8)
scene.live_zone(audience, correr, size=0.95)
scene.wait(4)
scene.stop("respuesta")
scene.render()
p.answer es la respuesta que eligió (-1 si ninguna) y p.answer_points lo que ganó con ella; en selección múltiple, p.answer_mask tiene un bit por respuesta elegida. p.random(k) da a cada jugador un número propio y fijo, para que no corran todos igual.
Batalla de equipos
Dos equipos tiran de una cuerda: el que va ganando la arrastra hacia su lado. Las barras de arriba comparan el promedio de puntos por jugador, más justo si los equipos quedaron desparejos.
import math
from gaanim import Scene
from gaanim.live import ease_out_back, pose
INK, MUTED = "#1a1033", "#cbb8ff"
scene = Scene(frame=(16, 9), background=INK)
teams = scene.teams(["Rojo", "Azul"], colors=["#ff4f8b", "#2fb8ff"])
scene.rehearsal(16, seed=2, skill=[0.85, 0.4])
audience = scene.audience()
# Aquí van tus preguntas; el ensayo las responde y suma los puntos.
for question, options, correct in [("¿7 × 8?", ["54", "56", "64"], 1),
("¿Capital de Perú?", ["Lima", "Cusco", "Puno"], 0)]:
quiz = scene.quiz(question, options, correct=correct)
scene.wait(0.5)
scene.stop()
quiz.reveal()
scene.segment("Batalla")
def side(p):
return -1 if p.team == 0 else 1
def tirar(p):
x = side(p) * (1.6 + p.team_index % 5 * 0.95)
y = -0.6 - p.team_index // 5 * 1.25
adelante = p.team_rank == 0
arrastre = 1.4 * ease_out_back(min(p.time / 1.2, 1)) * (1 if adelante else -1)
tiron = 0.15 * math.sin(2 * math.pi * (p.t * 1.4 + p.team_index * 0.13))
return pose(x + side(p) * (arrastre + tiron), y, lean=side(p) * 0.25, flip=side(p) > 0,
express="winner" if adelante else "hurt", loop=True)
scene.geometry.rect(15, 0.12).fill("#c89b6d").no_stroke().move_to(0, -0.2)
for team, x in [(0, -4.5), (1, 4.5)]:
scene.text(teams.names[team], size=0.7, color=teams.colors[team], weight=800).move_to(x, 3.4)
teams.bar(team, length=5.5, thickness=0.35, radius=0.17,
direction="left" if team == 0 else "right") \
.fill(teams.colors[team]).no_stroke().move_to(x, 2.5)
scene.viz.readout(teams.average(team), format=".0f", suffix=" pts por jugador", color=MUTED,
font_size=0.28).move_to(x - 1.3, 1.9)
scene.live_zone(audience, tirar, size=0.9, lean=0.0)
scene.wait(5)
scene.stop("batalla")
scene.render()
p.team_rank es 0 para el equipo que va ganando y p.team_index el orden de llegada dentro de su equipo, que sirve para formar filas en cada lado. Con choose=True en scene.teams, cada uno elige su equipo en el teléfono.
Carrera de puntajes
Los cinco primeros corren por carriles: avanzan hasta su puntaje, cambian de carril cuando alguien los adelanta y celebran una racha de aciertos. El puntaje que muestran sube poco a poco gracias al estado entre cuadros.
import math
from gaanim import Scene
from gaanim.live import STEP, ease_out_back, lerp, pose, spring, state
INK, SEA, MUTED, GOLD = "#1c0b3f", "#4a2a8c", "#cbb8ff", "#ffd23f"
scene = Scene(frame=(16, 9), background=INK)
scene.rehearsal(12, seed=8)
audience = scene.audience()
board = scene.leaderboard()
for question, options, correct in [("¿Símbolo del oro?", ["Ag", "Au", "Or"], 1),
("¿Año de la llegada a la Luna?", ["1965", "1969", "1972"], 1),
("¿Qué significa PDF?", ["Portable Document Format",
"Printed Data File"], 0)]:
quiz = scene.quiz(question, options, correct=correct)
scene.wait(0.5)
scene.stop()
quiz.reveal()
scene.segment("Carrera")
LANES, START, LENGTH = 5, -5.2, 10.4
def lane(rank):
return 2.3 - 1.3 * rank
def contar(p): # el puntaje mostrado persigue al real
return state(mostrado=p.state.mostrado + (p.score - p.state.mostrado) * min(1, 2.5 * STEP))
def correr(p):
y = lerp(lane(p.previous_rank), lane(p.rank), ease_out_back(p.rank_since / 0.6))
meta = START + LENGTH * p.state.mostrado / max(p.leader, 1)
x = lerp(START - 1.2, meta, spring(p.t, frequency=0.9, damping=0.45))
paso = abs(math.sin(2 * math.pi * 1.6 * p.t))
animo = "winner" if p.streak >= 3 else ("happy" if p.rank == 0 else None)
return pose(x, y + 0.16 * paso, express=animo, loop=True, visible=p.rank < LANES)
scene.text("Así va la carrera", size=0.6, color="#fff8ff", weight=700).move_to(0, 3.7)
for rank in range(LANES):
y = lane(rank)
scene.geometry.rect(LENGTH + 0.6, 0.07).fill(SEA).no_stroke().move_to(START + LENGTH / 2, y - 0.04)
board.name(rank, size=0.28, weight=700).fill(MUTED).move_to(-6.9, y + 0.35)
scene.viz.readout(board.points(rank), format=".0f", color=GOLD, font_size=0.28).move_to(6.2, y + 0.35)
scene.live_zone(audience, correr, size=0.85, state={"mostrado": 0.0}, update=contar)
scene.wait(5)
scene.stop("carrera")
scene.render()
p.rank, p.previous_rank y p.rank_since (segundos desde el último cambio de puesto) animan los adelantamientos; p.leader es el puntaje del primero, para medir la pista. state y update guardan un número por jugador entre cuadros (ver Estado entre cuadros).
Podio
Los tres primeros suben a su escalón y el ganador salta de alegría mientras llueve confeti; el resto aplaude desde abajo.
import math
from gaanim import Scene
from gaanim.live import anticipate, impact, pose
INK, WHITE, GOLD = "#1c0b3f", "#fff8ff", "#ffd23f"
scene = Scene(frame=(16, 9), background=INK)
scene.rehearsal(10, seed=6)
audience = scene.audience()
board = scene.leaderboard()
for question, options, correct in [("¿Planeta con anillos?", ["Marte", "Saturno"], 1),
("¿Autor de «La noche estrellada»?", ["Van Gogh", "Monet"], 0)]:
quiz = scene.quiz(question, options, correct=correct)
scene.wait(0.5)
scene.stop()
quiz.reveal()
scene.segment("Podio")
PLACES = ((0.0, 0.2, 3.6), (-3.6, -0.8, 2.6), (3.6, -1.4, 2.0)) # x, arriba, alto
MEDALS = (GOLD, "#d7dbe8", "#e39b5f")
def celebrar(p):
if p.rank < 3:
x, y, _ = PLACES[p.rank]
if p.rank > 0:
return pose(x, y, scale=1.3, lean=0.08 * math.sin(2 * math.pi * p.t / 1.3), look_y=0.6)
beat = p.t % 1.3 # agacharse, saltar, aterrizar
sx, sy = anticipate(beat, at=0.35, amount=0.3, duration=0.35)
alto = 0.0
if 0.35 <= beat < 0.9:
u = (beat - 0.35) / 0.55
alto = 4.4 * u * (1 - u)
elif beat >= 0.9:
sx, sy = impact(beat - 0.9, amount=0.35)
return pose(x, y + alto, sx=sx, sy=sy, scale=1.4, express="winner", loop=True)
x = -6.3 + (p.rank - 3) % 8 * 1.8
return pose(x, -4.0, scale=0.7, look_x=-x / 4, look_y=1)
heading = scene.text("¡Campeones!", size=0.8, color=WHITE, weight=800).move_to(0, 3.5)
steps = []
for rank, (x, top, height) in enumerate(PLACES):
block = scene.geometry.rect(2.8, height).fill(MEDALS[rank]).no_stroke().move_to(x, top - height / 2)
name = board.name(rank, size=0.36, weight=700, align="center").fill(INK).move_to(x, top - 0.5)
points = scene.viz.readout(board.points(rank), format=".0f", suffix=" pts", color=INK,
font_size=0.3).move_to(x - 0.6, top - 1.0)
steps.append(scene.geometry.group([block, name, points]).shift_by(0, -8))
scene.play([step.animate.shift_by(0, 8).duration(0.6).delay(0.2 * (2 - rank)).settle(overshoot=0.1)
for rank, step in enumerate(steps)])
scene.live_zone(audience, celebrar, size=1.1)
confetti = scene.fx.confetti((0, -4.6), count=0, seed=21, spread=1.2, speed=(8, 12))
scene.play([confetti.animate.burst(110).duration(1.2), heading.animate.indicate().duration(0.6)])
scene.wait(2)
scene.stop("podio")
scene.render()
Al terminar la presentación, los teléfonos también muestran el podio y una despedida.
Más ideas
- Sonidos: cualquier animación lleva un sonido con
.sound(ruta), yTransition.magic_move(..., sound=)suena al cambiar de diapositiva. Los paquetes de efectos de Kenney son de dominio público (CC0). - Un dato entre preguntas: después de revelar, una
scene.viz.bar_racecuenta la historia detrás de la respuesta. - Un anfitrión:
scene.character(...)pone en la escena un personaje como los del público, que celebra conexpress("winner")cuando quieras. - Transiciones que continúan: nombra las piezas que se repiten en cada diapositiva (el logo, el marcador de equipos) con
.named()y usaTransition.magic_moveenscene.segmentpara que se muevan en lugar de aparecer de nuevo. - Partidas de verdad: el ejemplo
examples/showcase/trivia/del repositorio junta todas estas recetas en una trivia completa con preguntas en Markdown, sonidos e imágenes.