From 59355909f2de9236af8168a26c70bcf6caa3b285 Mon Sep 17 00:00:00 2001 From: Elvis Claros Castro Date: Sat, 26 Sep 2026 20:21:47 -0300 Subject: Import video pipeline as it was --- scenes/overlay.py | 43 ++++++++ scenes/tiktok.py | 156 ++++++++++++++++++++++++++++ scenes/v10_buffon.py | 150 +++++++++++++++++++++++++++ scenes/v11_collatz.py | 171 ++++++++++++++++++++++++++++++ scenes/v12_simpson.py | 188 +++++++++++++++++++++++++++++++++ scenes/v13_cuerda.py | 161 +++++++++++++++++++++++++++++ scenes/v14_gabriel.py | 172 +++++++++++++++++++++++++++++++ scenes/v15_mobius.py | 241 +++++++++++++++++++++++++++++++++++++++++++ scenes/v16_bayes.py | 160 ++++++++++++++++++++++++++++ scenes/v17_cicloide.py | 275 +++++++++++++++++++++++++++++++++++++++++++++++++ scenes/v18_dados.py | 189 +++++++++++++++++++++++++++++++++ scenes/v1_montyhall.py | 166 +++++++++++++++++++++++++++++ scenes/v2_nueves.py | 121 ++++++++++++++++++++++ scenes/v3_cumple.py | 143 +++++++++++++++++++++++++ scenes/v4_luna.py | 178 ++++++++++++++++++++++++++++++++ scenes/v5_japones.py | 160 ++++++++++++++++++++++++++++ scenes/v6_benford.py | 131 +++++++++++++++++++++++ scenes/v7_hilbert.py | 135 ++++++++++++++++++++++++ scenes/v8_reuleaux.py | 158 ++++++++++++++++++++++++++++ scenes/v9_regla72.py | 124 ++++++++++++++++++++++ 20 files changed, 3222 insertions(+) create mode 100644 scenes/overlay.py create mode 100644 scenes/tiktok.py create mode 100644 scenes/v10_buffon.py create mode 100644 scenes/v11_collatz.py create mode 100644 scenes/v12_simpson.py create mode 100644 scenes/v13_cuerda.py create mode 100644 scenes/v14_gabriel.py create mode 100644 scenes/v15_mobius.py create mode 100644 scenes/v16_bayes.py create mode 100644 scenes/v17_cicloide.py create mode 100644 scenes/v18_dados.py create mode 100644 scenes/v1_montyhall.py create mode 100644 scenes/v2_nueves.py create mode 100644 scenes/v3_cumple.py create mode 100644 scenes/v4_luna.py create mode 100644 scenes/v5_japones.py create mode 100644 scenes/v6_benford.py create mode 100644 scenes/v7_hilbert.py create mode 100644 scenes/v8_reuleaux.py create mode 100644 scenes/v9_regla72.py (limited to 'scenes') diff --git a/scenes/overlay.py b/scenes/overlay.py new file mode 100644 index 0000000..0a3eb71 --- /dev/null +++ b/scenes/overlay.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Capa de interfaz del canal (chip, barra de progreso y subtitulos) para los +videos renderizados en Blender. Se renderiza con --transparent y se compone +encima del 3D, asi el estilo sale del mismo codigo que los videos de Manim. + + NAME=bolapeluda CHIP=Topología CHIPCOL=celeste manim --transparent ... + +Tambien escribe out/_timeline.json, que es lo que despues leen el script +de Blender (para animar) y build_video.py (para colocar el audio). +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +COLORES = {"ambar": AMBAR, "verde": VERDE, "rosa": ROSA, "celeste": CELESTE} + + +class Overlay(TikTok): + NAME = os.environ.get("NAME", "") + + def backdrop(self): + return VGroup() # el fondo punteado va aparte, debajo del 3D + + def construct(self): + self.prepare() + chip = self.chip(os.environ.get("CHIP", ""), + COLORES[os.environ.get("CHIPCOL", "ambar")]) + self.add(chip) + for i in range(len(self.segs)): + with self.beat(i) as b: + b.wait(b.floor) + self.finish() + + +class Fondo(TikTok): + """Un solo frame con la trama de puntos, para usar de fondo.""" + NAME = "fondo" + + def construct(self): + self._sub = None + TikTok.backdrop(self) + self.wait(0.1) diff --git a/scenes/tiktok.py b/scenes/tiktok.py new file mode 100644 index 0000000..7e2ad2d --- /dev/null +++ b/scenes/tiktok.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +"""Base comun para los videos verticales: formato 9:16, subtitulos, +barra de progreso y un sistema de 'beats' sincronizados con el audio.""" +import json, os +from contextlib import contextmanager +from manim import * + +# --- formato TikTok 1080x1920 ------------------------------------------------- +if os.environ.get("DRAFT"): + config.pixel_width, config.pixel_height = 540, 960 +else: + config.pixel_width, config.pixel_height = 1080, 1920 +config.frame_width = 9.0 +config.frame_height = 16.0 +config.frame_rate = 30 +config.background_color = "#0B0F1A" + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +FONT = "Roboto" +AMBAR = "#FFD166" +VERDE = "#06D6A0" +ROSA = "#EF476F" +CELESTE = "#4CC9F0" +GRIS = "#8D99AE" + +GAP = 0.30 # silencio entre segmentos +SUB_Y = -4.25 # los subtitulos van arriba de la UI de TikTok +TOP_Y = 6.45 + + +class _Beat: + """Acumula el tiempo usado dentro de un segmento para poder rellenar el resto.""" + def __init__(self, scene, floor): + self.s, self.floor, self.used = scene, floor, 0.0 + + def play(self, *anims, run_time=1.0, **kw): + self.s.play(*anims, run_time=run_time, **kw) + self.used += run_time + + def wait(self, t): + if t > 0.02: + self.s.wait(t) + self.used += t + + def add(self, *m): + self.s.add(*m) + + def remove(self, *m): + self.s.remove(*m) + + def close(self): + rest = self.floor - self.used + if rest > 0.02: + self.s.wait(rest) + self.used += rest + if GAP > 0.02: + self.s.wait(GAP) + self.used += GAP + return self.used + + +class TikTok(Scene): + NAME = "" + + # -- infraestructura ------------------------------------------------------ + def prepare(self): + with open(os.path.join(ROOT, "audio", self.NAME, "segments.json"), encoding="utf-8") as f: + self.data = json.load(f) + self.segs = self.data["segments"] + self.timeline, self.clock = [], 0.0 + self._sub = None + self._expected = sum(s["dur"] + GAP for s in self.segs) + self.backdrop() + self._progress_bar() + + def backdrop(self): + dots = VGroup(*[ + Dot(radius=0.035, color="#243050", fill_opacity=0.55).move_to([x, y, 0]) + for x in np.arange(-4.2, 4.3, 1.05) + for y in np.arange(-7.5, 7.6, 1.05) + ]) + self.add(dots) + return dots + + def _progress_bar(self): + w = config.frame_width + track = Rectangle(width=w, height=0.075, stroke_width=0, + fill_color="#1B2233", fill_opacity=1).move_to(UP * (TOP_Y + 1.15)) + bar = Rectangle(width=0.002, height=0.075, stroke_width=0, + fill_color=AMBAR, fill_opacity=1).move_to(track.get_left(), LEFT) + self.elapsed = 0.0 + total, left, y = self._expected, -w / 2, track.get_y() + + def upd(m, dt): + self.elapsed += dt + frac = min(self.elapsed / total, 1.0) + nw = max(frac * w, 0.002) + m.stretch_to_fit_width(nw) + m.move_to([left + nw / 2, y, 0]) + + bar.add_updater(upd) + self.add(track, bar) + + def subtitle(self, text): + """Cambia el subtitulo sin consumir tiempo de animacion.""" + if self._sub: + self.remove(self._sub) + t = Text(text, font=FONT, font_size=44, weight=BOLD, color=WHITE, + line_spacing=0.8).move_to(UP * SUB_Y) + if t.width > 8.0: + t.scale(8.0 / t.width) + box = RoundedRectangle(corner_radius=0.18, stroke_width=0, + fill_color="#000000", fill_opacity=0.55, + width=t.width + 0.55, height=t.height + 0.42).move_to(t) + self._sub = VGroup(box, t) + self.add(self._sub) + + @contextmanager + def beat(self, i, sub=True): + """Bloque sincronizado con el segmento i de audio.""" + if sub: + self.subtitle(self.segs[i]["sub"]) + b = _Beat(self, self.segs[i]["dur"]) + yield b + used = b.close() + self.timeline.append({"i": i, "start": round(self.clock, 3), + "dur": round(used, 3), "audio": self.segs[i]["dur"]}) + self.clock += used + + def finish(self): + if self._sub: + self.play(FadeOut(self._sub), run_time=0.4) + self.clock += 0.4 + self.wait(0.5) + self.clock += 0.5 + os.makedirs(os.path.join(ROOT, "out"), exist_ok=True) + with open(os.path.join(ROOT, "out", f"{self.NAME}_timeline.json"), "w") as f: + json.dump({"name": self.NAME, "gap": GAP, "total": round(self.clock, 3), + "beats": self.timeline}, f, indent=2) + + # -- helpers visuales ----------------------------------------------------- + def chip(self, text, color=AMBAR): + t = Text(text, font=FONT, font_size=34, weight=BOLD, color="#0B0F1A") + box = RoundedRectangle(corner_radius=0.3, stroke_width=0, fill_color=color, + fill_opacity=1, width=t.width + 0.7, height=t.height + 0.45) + g = VGroup(box, t).move_to(UP * TOP_Y) + return g + + def fit(self, m, w=8.3): + if m.width > w: + m.scale(w / m.width) + return m + + def big(self, text, size=76, color=WHITE): + return Text(text, font=FONT, font_size=size, weight=BOLD, color=color) diff --git a/scenes/v10_buffon.py b/scenes/v10_buffon.py new file mode 100644 index 0000000..35ee4e0 --- /dev/null +++ b/scenes/v10_buffon.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +import sys, os, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +D = 1.1 # separacion entre rayas = largo de la aguja +Y0 = 0.35 # raya mas baja +NLIN = 5 +SEED, N1, N2 = 36, 60, 200 + + +def tirar(n): + rng = np.random.default_rng(SEED) + x = rng.uniform(-3.9, 3.9, n) + y = rng.uniform(Y0 + 0.1, Y0 + (NLIN - 1) * D - 0.1, n) + th = rng.uniform(0, PI, n) + dx, dy = D / 2 * np.cos(th), D / 2 * np.sin(th) + p1 = np.stack([x - dx, y - dy, np.zeros(n)], axis=1) + p2 = np.stack([x + dx, y + dy, np.zeros(n)], axis=1) + cruza = np.floor((p1[:, 1] - Y0) / D) != np.floor((p2[:, 1] - Y0) / D) + return p1, p2, cruza + + +class Buffon(TikTok): + NAME = "buffon" + + def construct(self): + self.prepare() + chip = self.chip("π sin círculos", ROSA) + p1, p2, cruza = tirar(N2) + + rayas = VGroup(*[Line([-4.2, Y0 + k * D, 0], [4.2, Y0 + k * D, 0], + stroke_width=4, color="#33405C") for k in range(NLIN)]) + + with self.beat(0) as b: + hook = VGroup( + self.fit(Text("tiro agujas al piso", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("y sale π", font=FONT, font_size=64, + weight=BOLD, color=AMBAR), 8.2), + ).arrange(DOWN, buff=0.32).move_to(UP * 2.4) + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.45) + b.play(FadeIn(hook[0], shift=UP * 0.15), run_time=0.6) + b.play(Write(hook[1]), run_time=0.8) + + with self.beat(1) as b: + b.play(FadeOut(hook), run_time=0.35) + b.play(LaggedStart(*[Create(r) for r in rayas], lag_ratio=0.12), run_time=0.9) + demo = Line([-1.0, Y0 + D, 0], [-1.0, Y0 + 2 * D, 0], + stroke_width=9, color=AMBAR) + med = DoubleArrow([0.2, Y0 + D, 0], [0.2, Y0 + 2 * D, 0], buff=0, + color=VERDE, stroke_width=6, tip_length=0.2) + lab = Text("mismo largo", font=FONT, font_size=32, weight=BOLD, color=VERDE) + lab.next_to(med, RIGHT, buff=0.25) + b.play(Create(demo), run_time=0.5) + b.play(GrowArrow(med), FadeIn(lab), run_time=0.6) + + agujas = VGroup(*[Line(p1[i], p2[i], stroke_width=4.5, color="#7C8AA8") + for i in range(N2)]) + + with self.beat(2) as b: + b.play(FadeOut(VGroup(demo, med, lab)), run_time=0.3) + b.play(LaggedStart(*[FadeIn(agujas[i]) for i in range(N1)], + lag_ratio=0.012), run_time=max(1.1, b.floor - 0.5)) + + with self.beat(3) as b: + hit = [agujas[i] for i in range(N1) if cruza[i]] + miss = [agujas[i] for i in range(N1) if not cruza[i]] + b.play(*[a.animate.set_stroke(AMBAR, 6) for a in hit], + *[a.animate.set_stroke("#46526E", 3.5) for a in miss], run_time=1.0) + + c1 = int(cruza[:N1].sum()) + + def marcador(n, c, y=-0.75): + g = VGroup( + VGroup(Text("tiradas", font=FONT, font_size=30, color=GRIS), + Text(str(n), font=FONT, font_size=62, weight=BOLD, color=WHITE) + ).arrange(DOWN, buff=0.1), + VGroup(Text("cruzan", font=FONT, font_size=30, color=GRIS), + Text(str(c), font=FONT, font_size=62, weight=BOLD, color=AMBAR) + ).arrange(DOWN, buff=0.1), + ).arrange(RIGHT, buff=1.5).move_to([0, y, 0]) + return g + + marc = marcador(N1, c1) + with self.beat(4) as b: + b.play(FadeIn(marc, shift=UP * 0.15), run_time=0.8) + + with self.beat(5) as b: + f = MathTex(r"\frac{2 \times \text{tiradas}}{\text{cruces}}", + color=CELESTE).scale(1.25).move_to(DOWN * 2.45) + b.play(Write(f), run_time=1.1) + + with self.beat(6) as b: + est = 2 * N1 / c1 + res = MathTex(r"= %s" % f"{est:.4f}".replace(".", "{,}"), + color=VERDE).scale(1.45) + res.next_to(f, RIGHT, buff=0.4) + grp = VGroup(f, res) + b.play(Write(res), run_time=0.8) + b.play(grp.animate.move_to(DOWN * 2.45), run_time=0.4) + b.play(Flash(res, color=VERDE, line_length=0.5, num_lines=16, + flash_radius=1.4), run_time=0.6) + + c2 = int(cruza.sum()) + with self.beat(7) as b: + hit = [agujas[i] for i in range(N1, N2) if cruza[i]] + miss = [agujas[i] for i in range(N1, N2) if not cruza[i]] + for a in hit: + a.set_stroke(AMBAR, 6) + for a in miss: + a.set_stroke("#46526E", 3.5) + b.play(LaggedStart(*[FadeIn(agujas[i]) for i in range(N1, N2)], + lag_ratio=0.004), run_time=max(1.0, b.floor - 1.3)) + est2 = 2 * N2 / c2 + nm = marcador(N2, c2) + nres = MathTex(r"= %s" % f"{est2:.4f}".replace(".", "{,}"), + color=VERDE).scale(1.45).next_to(f, RIGHT, buff=0.4) + b.play(Transform(marc, nm), Transform(res, nres), run_time=0.9) + + with self.beat(8) as b: + pi = MathTex(r"\pi = 3{,}14159\ldots", color=AMBAR).scale(1.5) + pi.move_to(UP * 5.5) + b.play(Write(pi), run_time=1.0) + b.play(Circumscribe(pi, color=AMBAR, buff=0.25, stroke_width=6), run_time=0.8) + + with self.beat(9) as b: + b.play(FadeOut(VGroup(marc, pi, f, res)), run_time=0.45) + t = VGroup( + self.fit(Text("no hay ningún círculo", font=FONT, font_size=48, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("y π aparece igual", font=FONT, font_size=48, + weight=BOLD, color=AMBAR), 8.2), + ).arrange(DOWN, buff=0.35).move_to(DOWN * 1.5) + b.play(FadeIn(t[0], shift=UP * 0.15), run_time=0.6) + b.play(FadeIn(t[1], shift=UP * 0.15), run_time=0.7) + + with self.beat(10) as b: + b.play(FadeOut(t), run_time=0.35) + cta = VGroup( + self.fit(Text("aguja de Buffon", font=FONT, font_size=58, + weight=BOLD, color=AMBAR), 8.0), + self.fit(Text("hacelo con fósforos", font=FONT, font_size=44, + weight=BOLD, color=WHITE), 8.0), + ).arrange(DOWN, buff=0.35).move_to(DOWN * 1.5) + b.play(Write(cta[0]), run_time=0.8) + b.play(FadeIn(cta[1], shift=UP * 0.15), run_time=0.6) + + self.finish() diff --git a/scenes/v11_collatz.py b/scenes/v11_collatz.py new file mode 100644 index 0000000..49436b8 --- /dev/null +++ b/scenes/v11_collatz.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +import sys, os, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +X0, X1 = -3.75, 3.75 +Y0, Y1 = -1.30, 3.30 + + +def seq(n): + s = [n] + while n != 1: + n = n // 2 if n % 2 == 0 else 3 * n + 1 + s.append(n) + return s + + +def curva(sq, color=AMBAR, sw=5, mx=None, nx=None, log=False): + """Trayectoria como poligonal. mx/nx permiten compartir escala entre curvas.""" + mx = mx or max(sq) + nx = nx or len(sq) + if log: + f = lambda v: math.log(v) / math.log(mx) + else: + f = lambda v: v / mx + pts = [[X0 + (X1 - X0) * i / (nx - 1), Y0 + (Y1 - Y0) * f(sq[i]), 0] + for i in range(len(sq))] + m = VMobject(stroke_color=color, stroke_width=sw) + m.set_points_as_corners(pts) + return m, pts + + +def tile(v, col): + t = Text(str(v), font=FONT, font_size=44, weight=BOLD, color="#0B0F1A") + box = RoundedRectangle(corner_radius=0.16, width=max(0.95, t.width + 0.45), + height=0.88, stroke_width=0, fill_color=col, fill_opacity=1) + return VGroup(box, t) + + +class Collatz(TikTok): + NAME = "collatz" + + def construct(self): + self.prepare() + chip = self.chip("Abierto desde 1937", ROSA) + + with self.beat(0) as b: + n = self.big("27", 150, AMBAR).move_to(UP * 1.9) + cap = self.fit(Text("agarrá cualquier número", font=FONT, font_size=50, + weight=BOLD, color=WHITE), 8.2).move_to(DOWN * 0.3) + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.45) + b.play(FadeIn(n, scale=0.6), run_time=0.45) + for v in ("6", "19", "41"): + b.play(Transform(n, self.big(v, 150, AMBAR).move_to(UP * 1.9)), run_time=0.26) + b.play(FadeIn(cap, shift=UP * 0.15), run_time=0.5) + + def regla(txt, op, col): + a = Text(txt, font=FONT, font_size=46, weight=BOLD, color=col) + fl = Arrow(ORIGIN, RIGHT * 0.85, buff=0, color=GRIS, stroke_width=6, + max_tip_length_to_length_ratio=0.35) + c = Text(op, font=FONT, font_size=52, weight=BOLD, color=WHITE) + return VGroup(a, fl, c).arrange(RIGHT, buff=0.35) + + with self.beat(1) as b: + b.play(FadeOut(VGroup(n, cap)), run_time=0.3) + r1 = regla("es PAR", "÷ 2", CELESTE) + r2 = regla("es IMPAR", "× 3 + 1", AMBAR) + rr = VGroup(r1, r2).arrange(DOWN, buff=0.7).move_to(UP * 1.6) + self.fit(rr, 8.2) + b.play(FadeIn(r1, shift=UP * 0.2), run_time=0.7) + b.play(FadeIn(r2, shift=UP * 0.2), run_time=0.7) + + s6 = seq(6) + with self.beat(2) as b: + b.play(rr.animate.scale(0.72).move_to(UP * 5.2), run_time=0.5) + seis = self.big("6", 140, VERDE).move_to(UP * 1.0) + b.play(FadeIn(seis, scale=0.6), run_time=0.6) + + with self.beat(3) as b: + b.play(FadeOut(seis), run_time=0.25) + tiles = VGroup(*[tile(v, VERDE if v == 1 else (CELESTE if v % 2 == 0 else AMBAR)) + for v in s6]) + fila1 = VGroup(*tiles[:5]).arrange(RIGHT, buff=0.28) + fila2 = VGroup(*tiles[5:]).arrange(RIGHT, buff=0.28) + grid = VGroup(fila1, fila2).arrange(DOWN, buff=0.45).move_to(UP * 1.4) + self.fit(grid, 8.2) + b.play(LaggedStart(*[FadeIn(t, scale=0.6) for t in tiles], + lag_ratio=0.5), run_time=max(1.4, b.floor - 0.4)) + + with self.beat(4) as b: + b.play(Circumscribe(tiles[-1], color=VERDE, buff=0.12, stroke_width=6), + run_time=0.7) + b.play(FadeOut(grid), run_time=0.35) + siete = self.big("7", 140, AMBAR).move_to(UP * 1.0) + b.play(FadeIn(siete, scale=0.6), run_time=0.5) + + base = Line([X0 - 0.25, Y0, 0], [X1 + 0.25, Y0, 0], stroke_width=4, color="#33405C") + uno = Text("1", font=FONT, font_size=32, weight=BOLD, color=VERDE) + uno.next_to(base.get_start(), LEFT, buff=0.12) + + s7 = seq(7) + with self.beat(5) as b: + b.play(FadeOut(siete), run_time=0.25) + c7, p7 = curva(s7, AMBAR, 6) + pico = Text("52", font=FONT, font_size=34, weight=BOLD, color=AMBAR) + pico.next_to([p7[s7.index(max(s7))][0], Y1, 0], UP, buff=0.12) + b.play(FadeIn(base), FadeIn(uno), run_time=0.35) + b.play(Create(c7), run_time=max(1.2, b.floor - 1.1)) + b.play(FadeIn(pico), run_time=0.4) + + s27 = seq(27) + with self.beat(6) as b: + b.play(FadeOut(VGroup(c7, pico)), run_time=0.3) + c27, p27 = curva(s27, ROSA, 5) + k = s27.index(max(s27)) + top = Text("9.232", font=FONT, font_size=40, weight=BOLD, color=ROSA) + top.next_to([p27[k][0], Y1, 0], UP, buff=0.12) + lab = Text("27", font=FONT, font_size=40, weight=BOLD, color=ROSA) + lab.move_to([X0 - 0.05, Y1 + 0.45, 0]) + b.play(FadeIn(lab), run_time=0.3) + b.play(Create(c27), run_time=max(1.3, b.floor - 1.0)) + b.play(FadeIn(top), run_time=0.4) + + with self.beat(7) as b: + fin = Dot([p27[-1][0], Y0, 0], radius=0.14, color=VERDE) + pasos = self.fit(Text("111 pasos y cae al 1", font=FONT, font_size=48, + weight=BOLD, color=VERDE), 8.0).move_to(DOWN * 2.6) + b.play(FadeIn(fin, scale=0.4), run_time=0.4) + b.play(Flash(fin, color=VERDE, line_length=0.35, num_lines=14, + flash_radius=0.8), run_time=0.6) + b.play(FadeIn(pasos, shift=UP * 0.15), run_time=0.6) + + with self.beat(8) as b: + b.play(FadeOut(VGroup(c27, top, lab, fin, pasos)), run_time=0.4) + todos = [seq(k) for k in range(2, 80)] + MX = max(max(s) for s in todos) + NX = max(len(s) for s in todos) + cols = [AMBAR, CELESTE, ROSA, VERDE] + gr = VGroup(*[curva(s, cols[i % 4], 2.4, mx=MX, nx=NX, log=True)[0] + for i, s in enumerate(todos)]) + gr.set_stroke(opacity=0.65) + b.play(LaggedStart(*[Create(c) for c in gr], lag_ratio=0.02), + run_time=max(1.6, b.floor - 0.9)) + marca = self.fit(Text("todos terminan en 1", font=FONT, font_size=46, + weight=BOLD, color=VERDE), 8.0).move_to(DOWN * 2.6) + b.play(FadeIn(marca, shift=UP * 0.15), run_time=0.6) + + with self.beat(9) as b: + b.play(FadeOut(VGroup(gr, marca, base, uno, rr)), run_time=0.45) + t = VGroup( + self.fit(Text("nadie pudo demostrar", font=FONT, font_size=50, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("que pasa SIEMPRE", font=FONT, font_size=54, + weight=BOLD, color=ROSA), 8.2), + ).arrange(DOWN, buff=0.35).move_to(UP * 1.2) + b.play(FadeIn(t[0], shift=UP * 0.15), run_time=0.6) + b.play(FadeIn(t[1], scale=0.8), run_time=0.7) + + with self.beat(10) as b: + b.play(FadeOut(t), run_time=0.35) + cta = VGroup( + self.fit(Text("conjetura de Collatz", font=FONT, font_size=56, + weight=BOLD, color=AMBAR), 8.0), + self.fit(Text("dejá tu número abajo", font=FONT, font_size=46, + weight=BOLD, color=WHITE), 8.0), + ).arrange(DOWN, buff=0.38).move_to(UP * 0.8) + b.play(Write(cta[0]), run_time=0.9) + b.play(FadeIn(cta[1], shift=UP * 0.15), run_time=0.6) + + self.finish() diff --git a/scenes/v12_simpson.py b/scenes/v12_simpson.py new file mode 100644 index 0000000..6092924 --- /dev/null +++ b/scenes/v12_simpson.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +# datos reales (Charig et al., 1986): tratamiento A vs B para calculos renales +LEV = (("A", 81, 87), ("B", 234, 270)) +GRA = (("A", 192, 263), ("B", 55, 80)) +TOT = (("A", 81 + 192, 87 + 263), ("B", 234 + 55, 270 + 80)) + +BARW = 4.05 # ancho de barra al 100% +XL = -1.55 # borde izquierdo de las barras + + +class Simpson(TikTok): + NAME = "simpson" + + def barra(self, letra, num, den, y, gana): + pct = 100.0 * num / den + col = VERDE if gana else GRIS + lab = Text(letra, font=FONT, font_size=50, weight=BOLD, + color=CELESTE if letra == "A" else ROSA) + lab.move_to([-3.65, y, 0]) + track = RoundedRectangle(corner_radius=0.12, width=BARW, height=0.62, + stroke_width=0, fill_color="#1B2233", fill_opacity=1) + track.move_to([XL, y, 0], LEFT) + fill = RoundedRectangle(corner_radius=0.12, width=max(pct / 100 * BARW, 0.2), + height=0.62, stroke_width=0, fill_color=col, fill_opacity=1) + fill.move_to([XL, y, 0], LEFT) + val = Text(f"{pct:.0f}%", font=FONT, font_size=42, weight=BOLD, color=col) + val.next_to(track, RIGHT, buff=0.22) + return VGroup(lab, track, fill, val) + + def bloque(self, titulo, datos, ytop, ganador): + t = Text(titulo, font=FONT, font_size=40, weight=BOLD, color=WHITE) + t.move_to([-3.85, ytop, 0], LEFT) + b1 = self.barra(datos[0][0], datos[0][1], datos[0][2], ytop - 0.85, + datos[0][0] == ganador) + b2 = self.barra(datos[1][0], datos[1][1], datos[1][2], ytop - 1.70, + datos[1][0] == ganador) + return VGroup(t, b1, b2) + + def construct(self): + self.prepare() + chip = self.chip("Estadística que engaña", ROSA) + + with self.beat(0) as b: + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.45) + cab = VGroup( + self.fit(Text("dos tratamientos", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.2), + VGroup( + Text("A", font=FONT, font_size=90, weight=BOLD, color=CELESTE), + Text("vs", font=FONT, font_size=44, color=GRIS), + Text("B", font=FONT, font_size=90, weight=BOLD, color=ROSA), + ).arrange(RIGHT, buff=0.7), + ).arrange(DOWN, buff=0.45).move_to(UP * 3.0) + b.play(FadeIn(cab[0], shift=UP * 0.15), run_time=0.6) + b.play(FadeIn(cab[1][0], scale=0.6), run_time=0.35) + b.play(FadeIn(cab[1][1]), run_time=0.2) + b.play(FadeIn(cab[1][2], scale=0.6), run_time=0.35) + + lev = self.bloque("casos LEVES", LEV, 2.55, "A") + with self.beat(1) as b: + b.play(cab.animate.scale(0.5).move_to(UP * 5.4), run_time=0.5) + b.play(FadeIn(lev[0], shift=RIGHT * 0.2), run_time=0.4) + for bb in (lev[1], lev[2]): + b.play(FadeIn(bb[0]), FadeIn(bb[1]), + GrowFromEdge(bb[2], LEFT), FadeIn(bb[3]), run_time=0.55) + + gra = self.bloque("casos GRAVES", GRA, -0.35, "A") + with self.beat(2) as b: + b.play(FadeIn(gra[0], shift=RIGHT * 0.2), run_time=0.4) + for bb in (gra[1], gra[2]): + b.play(FadeIn(bb[0]), FadeIn(bb[1]), + GrowFromEdge(bb[2], LEFT), FadeIn(bb[3]), run_time=0.55) + + def tilde(col=VERDE): + m = VMobject(stroke_color=col, stroke_width=9) + m.set_points_as_corners([[-0.20, 0.02, 0], [-0.05, -0.18, 0], [0.24, 0.26, 0]]) + return m + + with self.beat(3) as b: + tick = VGroup(*[tilde() for _ in range(2)]) + tick[0].next_to(lev[1][3], RIGHT, buff=0.25) + tick[1].next_to(gra[1][3], RIGHT, buff=0.25) + b.play(FadeIn(tick[0], scale=0.5), run_time=0.4) + b.play(FadeIn(tick[1], scale=0.5), run_time=0.4) + pre = self.fit(Text("A gana 2 de 2", font=FONT, font_size=48, + weight=BOLD, color=CELESTE), 8.0).move_to(DOWN * 2.80) + b.play(FadeIn(pre, shift=UP * 0.15), run_time=0.5) + + tot = self.bloque("TODOS juntos", TOT, 2.55, "B") + with self.beat(4) as b: + b.play(FadeOut(VGroup(lev, gra, tick, pre)), run_time=0.45) + b.play(FadeIn(tot[0], shift=RIGHT * 0.2), run_time=0.4) + for bb in (tot[1], tot[2]): + b.play(FadeIn(bb[0]), FadeIn(bb[1]), + GrowFromEdge(bb[2], LEFT), FadeIn(bb[3]), run_time=0.6) + + with self.beat(5) as b: + b.play(Circumscribe(tot[2], color=VERDE, buff=0.18, stroke_width=6), + run_time=0.8) + g = self.fit(Text("gana B", font=FONT, font_size=70, weight=BOLD, + color=ROSA), 8.0).move_to(DOWN * 0.75) + ne = self.fit(Text("y no hay ningún error", font=FONT, font_size=44, + weight=BOLD, color=WHITE), 8.0).next_to(g, DOWN, buff=0.4) + b.play(FadeIn(g, scale=0.7), run_time=0.6) + b.play(FadeIn(ne, shift=UP * 0.15), run_time=0.5) + + with self.beat(6) as b: + b.play(FadeOut(VGroup(tot, g, ne, cab)), run_time=0.4) + nom = VGroup( + self.fit(Text("PARADOJA", font=FONT, font_size=72, weight=BOLD, + color=AMBAR), 8.2), + self.fit(Text("DE SIMPSON", font=FONT, font_size=72, weight=BOLD, + color=AMBAR), 8.2), + ).arrange(DOWN, buff=0.2).move_to(UP * 2.6) + b.play(FadeIn(nom, scale=0.75), run_time=0.8) + b.play(Flash(nom, color=AMBAR, line_length=0.6, num_lines=20, + flash_radius=3.0), run_time=0.7) + + def reparto(letra, col, leves, graves, y): + lab = Text(letra, font=FONT, font_size=50, weight=BOLD, color=col) + lab.move_to([-3.65, y, 0]) + n = leves + graves + wl = leves / n * BARW + a = Rectangle(width=max(wl, 0.05), height=0.7, stroke_width=0, + fill_color=VERDE, fill_opacity=1).move_to([XL, y, 0], LEFT) + c = Rectangle(width=max(BARW - wl, 0.05), height=0.7, stroke_width=0, + fill_color=ROSA, fill_opacity=1).next_to(a, RIGHT, buff=0) + nl = Text(str(leves), font=FONT, font_size=30, weight=BOLD, color="#0B0F1A").move_to(a) + ng = Text(str(graves), font=FONT, font_size=30, weight=BOLD, color="#0B0F1A").move_to(c) + return VGroup(lab, a, c, nl, ng) + + leyenda = VGroup( + VGroup(Square(0.34, stroke_width=0, fill_color=VERDE, fill_opacity=1), + Text("leves", font=FONT, font_size=32, color=WHITE)).arrange(RIGHT, buff=0.18), + VGroup(Square(0.34, stroke_width=0, fill_color=ROSA, fill_opacity=1), + Text("graves", font=FONT, font_size=32, color=WHITE)).arrange(RIGHT, buff=0.18), + ).arrange(RIGHT, buff=0.8).move_to(UP * 0.55) + + rA = reparto("A", CELESTE, 87, 263, -0.65) + rB = reparto("B", ROSA, 270, 80, -1.75) + + with self.beat(7) as b: + b.play(nom.animate.scale(0.55).move_to(UP * 5.3), run_time=0.5) + b.play(FadeIn(leyenda), run_time=0.4) + b.play(FadeIn(rA[0]), GrowFromEdge(VGroup(rA[1], rA[2]), LEFT), + FadeIn(VGroup(rA[3], rA[4])), run_time=0.8) + b.play(Circumscribe(rA[2], color=ROSA, buff=0.1, stroke_width=5), run_time=0.7) + + with self.beat(8) as b: + b.play(FadeIn(rB[0]), GrowFromEdge(VGroup(rB[1], rB[2]), LEFT), + FadeIn(VGroup(rB[3], rB[4])), run_time=0.8) + b.play(Circumscribe(rB[1], color=VERDE, buff=0.1, stroke_width=5), run_time=0.7) + + with self.beat(9) as b: + b.play(FadeOut(VGroup(rA, rB, leyenda)), run_time=0.4) + t = VGroup( + self.fit(Text("el promedio mezcla", font=FONT, font_size=48, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("dos grupos distintos", font=FONT, font_size=48, + weight=BOLD, color=AMBAR), 8.2), + self.fit(Text("(datos de un estudio real)", font=FONT, font_size=34, + color=GRIS), 8.2), + ).arrange(DOWN, buff=0.35).move_to(UP * 1.6) + b.play(FadeIn(t[0], shift=UP * 0.15), run_time=0.55) + b.play(FadeIn(t[1], shift=UP * 0.15), run_time=0.55) + b.play(FadeIn(t[2]), run_time=0.45) + + with self.beat(10) as b: + b.play(FadeOut(t), run_time=0.35) + cta = VGroup( + self.fit(Text("cuando te muestren", font=FONT, font_size=44, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("un promedio, preguntá:", font=FONT, font_size=44, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("¿de quiénes?", font=FONT, font_size=64, + weight=BOLD, color=AMBAR), 8.0), + ).arrange(DOWN, buff=0.3).move_to(UP * 1.4) + b.play(FadeIn(cta[0], shift=UP * 0.12), run_time=0.45) + b.play(FadeIn(cta[1], shift=UP * 0.12), run_time=0.45) + b.play(Write(cta[2]), run_time=0.8) + b.play(Circumscribe(cta[2], color=AMBAR, buff=0.25, stroke_width=6), run_time=0.8) + + self.finish() diff --git a/scenes/v13_cuerda.py b/scenes/v13_cuerda.py new file mode 100644 index 0000000..5ca224d --- /dev/null +++ b/scenes/v13_cuerda.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +import sys, os, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +CEN = UP * 1.75 +RT = 2.05 # radio de la Tierra en pantalla +HUECO = 0.72 # separacion dibujada (exagerada) + + +def gato(h=0.55, color="#E8EDF7"): + """Silueta simple de gato de perfil, alto total = h.""" + body = Ellipse(width=0.90, height=0.34, stroke_width=0, fill_color=color, + fill_opacity=1).move_to([0, 0.12, 0]) + head = Circle(radius=0.17, stroke_width=0, fill_color=color, + fill_opacity=1).move_to([0.46, 0.26, 0]) + e1 = Triangle(stroke_width=0, fill_color=color, fill_opacity=1).scale(0.09) + e1.move_to([0.39, 0.42, 0]) + e2 = e1.copy().shift(RIGHT * 0.15) + tail = ArcBetweenPoints([-0.44, 0.16, 0], [-0.62, 0.52, 0], angle=-1.6, + stroke_width=5, color=color) + legs = VGroup(*[Line([x, 0.02, 0], [x, -0.16, 0], stroke_width=6, color=color) + for x in (-0.28, -0.08, 0.18, 0.36)]) + g = VGroup(tail, body, legs, head, e1, e2) + g.scale(h / g.height) + return g + + +class Cuerda(TikTok): + NAME = "cuerda" + + def planeta(self, r, center, col="#1D3B6E"): + p = Circle(radius=r, stroke_width=4, stroke_color=CELESTE, + fill_color=col, fill_opacity=1).move_to(center) + return p + + def construct(self): + self.prepare() + chip = self.chip("Intuición rota", CELESTE) + + tierra = self.planeta(RT, CEN) + cuerda = Circle(radius=RT + 0.03, stroke_width=7, color=AMBAR).move_to(CEN) + + with self.beat(0) as b: + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.45) + b.play(GrowFromCenter(tierra), run_time=0.7) + b.play(Create(cuerda), run_time=max(0.8, b.floor - 1.6)) + + with self.beat(1) as b: + km = Text("40.000 km", font=FONT, font_size=50, weight=BOLD, color=AMBAR) + km.move_to(CEN + DOWN * 0.0) + box = RoundedRectangle(corner_radius=0.16, stroke_width=0, fill_color="#0B0F1A", + fill_opacity=0.78, width=km.width + 0.5, + height=km.height + 0.35).move_to(km) + b.play(FadeIn(VGroup(box, km), scale=0.7), run_time=0.8) + + with self.beat(2) as b: + b.play(FadeOut(VGroup(box, km)), run_time=0.3) + seg = Line(LEFT * 0.6, RIGHT * 0.6, stroke_width=9, color=VERDE) + tip = VGroup(*[Line(UP * 0.16, DOWN * 0.16, stroke_width=6, color=VERDE) + .move_to(p) for p in (seg.get_start(), seg.get_end())]) + lab = Text("+ 1 metro", font=FONT, font_size=48, weight=BOLD, color=VERDE) + g = VGroup(VGroup(seg, tip), lab).arrange(DOWN, buff=0.3).move_to(DOWN * 2.15) + b.play(GrowFromCenter(VGroup(seg, tip)), run_time=0.5) + b.play(FadeIn(lab, shift=UP * 0.15), run_time=0.6) + + with self.beat(3) as b: + alta = Circle(radius=RT + HUECO, stroke_width=7, color=AMBAR).move_to(CEN) + radios = VGroup(*[ + DashedLine(CEN + RT * np.array([math.cos(a), math.sin(a), 0]), + CEN + (RT + HUECO) * np.array([math.cos(a), math.sin(a), 0]), + stroke_width=4, color="#7C8AA8", dash_length=0.07) + for a in np.linspace(0, TAU, 16, endpoint=False)]) + b.play(FadeOut(g), run_time=0.25) + b.play(Transform(cuerda, alta), run_time=1.0) + b.play(FadeIn(radios), run_time=0.5) + esc = Text("(dibujo exagerado)", font=FONT, font_size=28, color=GRIS) + esc.move_to(DOWN * 2.6) + b.play(FadeIn(esc), run_time=0.35) + + with self.beat(4) as b: + q = VGroup( + self.fit(Text("¿cuánto se separa?", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.0), + VGroup( + Text("«nada»", font=FONT, font_size=40, color=GRIS), + Text("«un pelo»", font=FONT, font_size=40, color=GRIS), + ).arrange(RIGHT, buff=0.7), + ).arrange(DOWN, buff=0.35).move_to(CEN) + bx = RoundedRectangle(corner_radius=0.2, stroke_width=0, fill_color="#0B0F1A", + fill_opacity=0.82, width=q.width + 0.6, + height=q.height + 0.5).move_to(q) + b.play(FadeIn(VGroup(bx, q), scale=0.85), run_time=0.8) + + with self.beat(5) as b: + b.play(FadeOut(VGroup(bx, q)), run_time=0.3) + gt = gato(HUECO * 0.92).move_to(CEN + UP * (RT + HUECO / 2)) + med = DoubleArrow(CEN + LEFT * RT, CEN + LEFT * (RT + HUECO), buff=0, + color=VERDE, stroke_width=6, tip_length=0.18) + val = Text("15,9 cm", font=FONT, font_size=42, weight=BOLD, color=VERDE) + val.next_to(med, UP, buff=0.22) + b.play(GrowArrow(med), FadeIn(val), run_time=0.7) + b.play(FadeIn(gt, shift=RIGHT * 0.3), run_time=0.7) + + with self.beat(6) as b: + b.play(FadeOut(VGroup(radios, gt, med, val, esc)), run_time=0.4) + b.play(VGroup(tierra, cuerda).animate.scale(0.52).move_to(UP * 4.6), + run_time=0.6) + f1 = MathTex(r"r = \frac{P}{2\pi}", color=WHITE).scale(1.6).move_to(UP * 1.5) + b.play(Write(f1), run_time=1.0) + + with self.beat(7) as b: + f2 = MathTex(r"\Delta r = \frac{1\,\text{m}}{2\pi}", color=AMBAR) + f2.scale(1.6).move_to(DOWN * 0.5) + b.play(Write(f2), run_time=1.1) + + with self.beat(8) as b: + f3 = MathTex(r"= 0{,}159\ \text{m} = 15{,}9\ \text{cm}", color=VERDE) + f3.scale(1.25).move_to(DOWN * 2.15) + b.play(Write(f3), run_time=0.9) + nota = self.fit(Text("el tamaño del planeta no aparece", font=FONT, + font_size=40, weight=BOLD, color=ROSA), 8.2) + nota.move_to(DOWN * 3.05) + b.play(FadeIn(nota, shift=UP * 0.15), run_time=0.6) + + with self.beat(9) as b: + b.play(FadeOut(VGroup(f1, f2, f3, nota, tierra, cuerda)), run_time=0.45) + def par(r, etiqueta, x, col): + c0 = [x, 1.75, 0] + p = self.planeta(r, c0, col) + c = Circle(radius=r + 0.30, stroke_width=6, color=AMBAR).move_to(c0) + lab = Text(etiqueta, font=FONT, font_size=36, weight=BOLD, color=WHITE) + lab.move_to([x, -0.55, 0]) + v = Text("15,9 cm", font=FONT, font_size=38, weight=BOLD, color=VERDE) + v.move_to([x, -1.25, 0]) + return VGroup(p, c, lab, v) + iz = par(1.25, "la Tierra", -2.05, "#1D3B6E") + de = par(0.55, "una pelota de tenis", 2.05, "#2F7A3C") + self.fit(de[2], 3.9) + b.play(FadeIn(iz), run_time=0.6) + b.play(FadeIn(de), run_time=0.6) + ig = Text("=", font=FONT, font_size=64, weight=BOLD, color=AMBAR) + ig.move_to([0, 1.75, 0]) + b.play(FadeIn(ig, scale=0.6), run_time=0.5) + esc2 = Text("(no está a escala)", font=FONT, font_size=28, color=GRIS) + esc2.move_to(DOWN * 2.35) + b.play(FadeIn(esc2), run_time=0.35) + + with self.beat(10) as b: + b.play(FadeOut(VGroup(iz, de, ig, esc2)), run_time=0.4) + cta = VGroup( + self.fit(Text("el tamaño no importa", font=FONT, font_size=56, + weight=BOLD, color=AMBAR), 8.0), + self.fit(Text("mandáselo al que dudó", font=FONT, font_size=44, + weight=BOLD, color=WHITE), 8.0), + ).arrange(DOWN, buff=0.38).move_to(UP * 1.4) + b.play(Write(cta[0]), run_time=0.9) + b.play(FadeIn(cta[1], shift=UP * 0.15), run_time=0.6) + + self.finish() diff --git a/scenes/v14_gabriel.py b/scenes/v14_gabriel.py new file mode 100644 index 0000000..24a9621 --- /dev/null +++ b/scenes/v14_gabriel.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +import sys, os, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +XA, XB = 1.0, 7.4 # dominio de 1/x +KX, KY = 1.00, 2.00 # escala a pantalla +X0S, CY = -3.75, 1.70 + + +def sx(x): + return X0S + (x - XA) * KX + + +def sy(x): + return KY / x + + +def rama(sig, n=160, **kw): + xs = np.linspace(XA, XB, n) + pts = [[sx(x), CY + sig * sy(x), 0] for x in xs] + m = VMobject(**kw) + m.set_points_as_corners(pts) + return m + + +def cuerpo(fill="#1E4E8C", op=0.55): + xs = np.linspace(XA, XB, 160) + up = [[sx(x), CY + sy(x), 0] for x in xs] + dn = [[sx(x), CY - sy(x), 0] for x in reversed(xs)] + m = VMobject(stroke_width=0, fill_color=fill, fill_opacity=op) + m.set_points_as_corners(up + dn + [up[0]]) + return m + + +def aros(n=9, col=CELESTE, op=0.55): + xs = np.linspace(XA + 0.35, XB, n) + return VGroup(*[ + Ellipse(width=0.34, height=2 * sy(x), stroke_width=3, stroke_opacity=op, + color=col).move_to([sx(x), CY, 0]) for x in xs]) + + +class Gabriel(TikTok): + NAME = "gabriel" + + def construct(self): + self.prepare() + chip = self.chip("Paradoja del infinito", CELESTE) + + with self.beat(0) as b: + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.45) + t = VGroup( + self.fit(Text("el cuerno de", font=FONT, font_size=50, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("GABRIEL", font=FONT, font_size=86, + weight=BOLD, color=AMBAR), 8.0), + ).arrange(DOWN, buff=0.25).move_to(UP * 2.6) + b.play(FadeIn(t[0], shift=UP * 0.15), run_time=0.5) + b.play(FadeIn(t[1], scale=0.7), run_time=0.8) + + eje = Line([sx(XA) - 0.5, CY, 0], [sx(XB) + 0.35, CY, 0], + stroke_width=3, color="#33405C") + + with self.beat(1) as b: + b.play(t.animate.scale(0.5).move_to(UP * 5.3), run_time=0.5) + f = MathTex(r"y = \frac{1}{x}", color=AMBAR).scale(1.35).move_to(DOWN * 2.4) + up = rama(+1, stroke_color=AMBAR, stroke_width=6) + b.play(FadeIn(eje), run_time=0.3) + b.play(Create(up), run_time=0.9) + b.play(Write(f), run_time=0.7) + + body = cuerpo() + low = rama(-1, stroke_color=AMBAR, stroke_width=6) + ar = aros() + boca = Ellipse(width=0.42, height=2 * sy(XA), stroke_width=6, + color=AMBAR).move_to([sx(XA), CY, 0]) + + with self.beat(2) as b: + b.play(Create(low), run_time=0.6) + b.play(FadeIn(body), FadeIn(boca), run_time=0.5) + b.play(LaggedStart(*[Create(a) for a in ar], lag_ratio=0.15), run_time=0.9) + inf = MathTex(r"\cdots \infty", color=GRIS).scale(1.0) + inf.next_to([sx(XB), CY, 0], RIGHT, buff=0.2) + b.play(FadeIn(inf), run_time=0.4) + + horn = VGroup(eje, body, up, low, ar, boca, inf) + + with self.beat(3) as b: + b.play(FadeOut(f), run_time=0.25) + q = self.fit(Text("¿cuánta pintura entra?", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.2).move_to(DOWN * 2.3) + b.play(FadeIn(q, shift=UP * 0.15), run_time=0.7) + + with self.beat(4) as b: + b.play(FadeOut(q), run_time=0.25) + lleno = cuerpo(fill=VERDE, op=0.85) + b.play(FadeIn(lleno), run_time=0.7) + v = MathTex(r"V = \pi\int_1^{\infty}\frac{dx}{x^2} = \pi", + color=VERDE).scale(1.15).move_to(DOWN * 2.05) + b.play(Write(v), run_time=1.1) + + with self.beat(5) as b: + lt = self.fit(Text("≈ 3,14 litros y te sobra", font=FONT, font_size=48, + weight=BOLD, color=VERDE), 8.2).move_to(DOWN * 2.90) + b.play(FadeIn(lt, shift=UP * 0.15), run_time=0.7) + + with self.beat(6) as b: + b.play(FadeOut(VGroup(v, lt, lleno)), run_time=0.4) + q2 = self.fit(Text("¿y pintarlo por fuera?", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.2).move_to(DOWN * 2.3) + b.play(up.animate.set_stroke(ROSA, 8), low.animate.set_stroke(ROSA, 8), + run_time=0.6) + b.play(FadeIn(q2, shift=UP * 0.15), run_time=0.6) + + with self.beat(7) as b: + b.play(FadeOut(q2), run_time=0.25) + a = MathTex(r"A > 2\pi\int_1^{\infty}\frac{dx}{x} = \infty", + color=ROSA).scale(1.15).move_to(DOWN * 2.15) + b.play(Write(a), run_time=1.1) + b.play(Flash(a, color=ROSA, line_length=0.5, num_lines=18, + flash_radius=2.4), run_time=0.7) + + with self.beat(8) as b: + b.play(FadeOut(VGroup(a, horn)), run_time=0.45) + def fila(txt, val, col): + t1 = Text(txt, font=FONT, font_size=44, weight=BOLD, color=WHITE) + t2 = Text(val, font=FONT, font_size=56, weight=BOLD, color=col) + return VGroup(t1, t2).arrange(DOWN, buff=0.18) + cmp = VGroup(fila("volumen", "π (finito)", VERDE), + fila("superficie", "∞ (infinita)", ROSA) + ).arrange(DOWN, buff=0.75).move_to(UP * 2.4) + self.fit(cmp, 8.2) + b.play(FadeIn(cmp[0], shift=UP * 0.15), run_time=0.55) + b.play(FadeIn(cmp[1], shift=UP * 0.15), run_time=0.55) + golpe = VGroup( + self.fit(Text("lo llenás de pintura", font=FONT, font_size=48, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("pero no lo podés pintar", font=FONT, font_size=48, + weight=BOLD, color=AMBAR), 8.2), + ).arrange(DOWN, buff=0.3).move_to(DOWN * 0.6) + b.play(FadeIn(golpe, scale=0.85), run_time=0.8) + + with self.beat(9) as b: + b.play(FadeOut(VGroup(cmp, golpe)), run_time=0.4) + tr = VGroup( + self.fit(Text("la trampa:", font=FONT, font_size=48, + weight=BOLD, color=ROSA), 8.2), + self.fit(Text("la pintura real tiene", font=FONT, font_size=46, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("espesor. El cuerno", font=FONT, font_size=46, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("se hace más fino que eso.", font=FONT, font_size=46, + weight=BOLD, color=WHITE), 8.2), + ).arrange(DOWN, buff=0.3).move_to(UP * 1.8) + for k in range(4): + b.play(FadeIn(tr[k], shift=UP * 0.12), run_time=0.45) + + with self.beat(10) as b: + b.play(FadeOut(tr), run_time=0.35) + cta = VGroup( + self.fit(Text("con el infinito", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("la intuición se rompe", font=FONT, font_size=56, + weight=BOLD, color=AMBAR), 8.0), + ).arrange(DOWN, buff=0.35).move_to(UP * 1.4) + b.play(FadeIn(cta[0], shift=UP * 0.15), run_time=0.55) + b.play(Write(cta[1]), run_time=0.9) + b.play(Circumscribe(cta[1], color=AMBAR, buff=0.25, stroke_width=6), + run_time=0.8) + + self.finish() diff --git a/scenes/v15_mobius.py b/scenes/v15_mobius.py new file mode 100644 index 0000000..a921c6b --- /dev/null +++ b/scenes/v15_mobius.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +"""Cinta de Mobius renderizada como superficie reglada: se generan quads en 3D, +se proyectan con una camara fija y se dibujan de atras hacia adelante.""" +import sys, os, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +TILT, YAW = 1.02, 0.0 +CARA_A, CARA_B = "#FFD166", "#2F6FD0" + + +def geom(tau, beta, L=9.4, w=0.62, nu=170, smax=1.0): + """Punto central y direccion transversal de la cinta para s en [0, smax].""" + s = np.linspace(0.0, smax, max(int(nu * smax), 2) + 1) + th = max(beta * TAU, 1e-3) + Rr = L / th + ang = th * (s - 0.5) + p = np.stack([Rr * np.sin(ang), Rr * np.cos(ang) - Rr, np.zeros_like(ang)], axis=1) + N = np.stack([np.sin(ang), np.cos(ang), np.zeros_like(ang)], axis=1) + B = np.zeros_like(N) + B[:, 2] = 1.0 + phi = tau * PI * s + d = np.cos(phi)[:, None] * N + np.sin(phi)[:, None] * B + return p, d, s + + +def proj(P): + x, y, z = P[..., 0], P[..., 1], P[..., 2] + c, sn = math.cos(YAW), math.sin(YAW) + x2, y2 = x * c - y * sn, x * sn + y * c + ct, st = math.cos(TILT), math.sin(TILT) + return x2, y2 * ct - z * st, y2 * st + z * ct # X, Y, profundidad + + +def quads(tau, beta, L=9.4, w=0.62, nu=170, scale=1.0, center=ORIGIN): + """Lista de (poligono_screen, profundidad, color) sin ordenar.""" + p, d, _ = geom(tau, beta, L, w, nu) + A = p - w * d + Bv = p + w * d + XA, YA, DA = proj(A) + XB, YB, DB = proj(Bv) + out = [] + for i in range(len(p) - 1): + n = np.cross(Bv[i] - A[i], A[i + 1] - A[i]) + ln = np.linalg.norm(n) + 1e-12 + _, _, nd = proj(n / ln) + front = nd < 0 + bright = 0.42 + 0.58 * abs(nd) + col = interpolate_color(BLACK, ManimColor(CARA_A if front else CARA_B), bright) + pts = [[XA[i], YA[i], 0], [XB[i], YB[i], 0], + [XB[i + 1], YB[i + 1], 0], [XA[i + 1], YA[i + 1], 0]] + pts = [list(np.array(q) * scale + center) for q in pts] + out.append((pts, (DA[i] + DB[i] + DA[i + 1] + DB[i + 1]) / 4.0, col)) + return out + + +def draw(items): + """Painter's algorithm: lo mas lejos (profundidad mayor) se dibuja primero.""" + items = sorted(items, key=lambda t: -t[1]) + return VGroup(*[Polygon(*pts, stroke_width=0, fill_color=col, fill_opacity=1) + for pts, _, col in items]) + + +def cinta(tau, beta, **kw): + return draw(quads(tau, beta, **kw)) + + +def camino(tau, beta, off, smax=1.0, L=9.4, w=0.62, nu=170, scale=1.0, + center=ORIGIN, **kw): + """Poligonal sobre la cinta a distancia 'off' del centro (off en [-w, w]).""" + p, d, _ = geom(tau, beta, L, w, nu, smax=smax) + P = p + off * d + X, Y, _ = proj(P) + m = VMobject(**kw) + m.set_points_as_corners([list(np.array([X[i], Y[i], 0]) * scale + center) + for i in range(len(X))]) + return m + + +class Mobius(TikTok): + NAME = "mobius" + + def construct(self): + self.prepare() + chip = self.chip("Hacelo en casa", VERDE) + C = UP * 1.85 + SC = 1.30 + P = dict(L=9.4, w=0.62, nu=130, scale=SC, center=C) + + tau = ValueTracker(0.0) + beta = ValueTracker(0.0) + sc = ValueTracker(0.60) # la tira plana mide L*scale: tiene que entrar + + def actual(): + return cinta(tau.get_value(), beta.get_value(), + **{**P, "scale": sc.get_value()}) + + banda = actual() + + with self.beat(0) as b: + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.45) + b.play(FadeIn(banda, shift=UP * 0.2), run_time=0.7) + + with self.beat(1) as b: + banda.add_updater(lambda m: m.become(actual())) + b.play(tau.animate.set_value(1.0), run_time=max(1.0, b.floor * 0.45), + rate_func=smooth) + b.play(beta.animate.set_value(1.0), sc.animate.set_value(SC), + run_time=max(1.0, b.floor * 0.45), rate_func=smooth) + banda.clear_updaters() + banda.become(cinta(1.0, 1.0, **P)) + + with self.beat(2) as b: + nom = VGroup( + self.fit(Text("cinta de M\u00f6bius", font=FONT, font_size=58, + weight=BOLD, color=AMBAR), 8.0), + self.fit(Text("una sola cara", font=FONT, font_size=50, + weight=BOLD, color=WHITE), 8.0), + ).arrange(DOWN, buff=0.3).move_to(DOWN * 2.6) + b.play(Write(nom[0]), run_time=0.8) + b.play(FadeIn(nom[1], shift=UP * 0.15), run_time=0.6) + + def recorrido(off, color, prog): + m = camino(1.0, 1.0, off, smax=max(prog.get_value(), 0.004), + stroke_color=color, stroke_width=9, **P) + return m + + with self.beat(3) as b: + b.play(FadeOut(nom), run_time=0.25) + prog = ValueTracker(0.004) + traza = recorrido(0.72 * P["w"], ROSA, prog) + traza.add_updater(lambda m: m.become(recorrido(0.72 * P["w"], ROSA, prog))) + punta = Dot(radius=0.16, color=ROSA) + punta.add_updater(lambda m: m.move_to(traza.get_end())) + b.add(traza, punta) + b.play(prog.animate.set_value(2.0), run_time=max(1.6, b.floor - 0.5), + rate_func=linear) + traza.clear_updaters(); punta.clear_updaters() + + with self.beat(4) as b: + b.play(FadeOut(VGroup(traza, punta)), run_time=0.3) + prog2 = ValueTracker(0.004) + borde = recorrido(P["w"], VERDE, prog2) + borde.add_updater(lambda m: m.become(recorrido(P["w"], VERDE, prog2))) + b.add(borde) + b.play(prog2.animate.set_value(2.0), run_time=max(1.4, b.floor - 0.9), + rate_func=linear) + borde.clear_updaters() + uno = self.fit(Text("un solo borde", font=FONT, font_size=50, + weight=BOLD, color=VERDE), 8.0).move_to(DOWN * 2.6) + b.play(FadeIn(uno, shift=UP * 0.15), run_time=0.6) + + with self.beat(5) as b: + b.play(FadeOut(VGroup(borde, uno)), run_time=0.3) + corte = camino(1.0, 1.0, 0.0, smax=1.0, stroke_color=ROSA, + stroke_width=8, **P) + corte = DashedVMobject(corte, num_dashes=90, color=ROSA) + b.play(Create(corte), run_time=max(1.2, b.floor - 0.8)) + tij = self.fit(Text("cortala al medio", font=FONT, font_size=50, + weight=BOLD, color=ROSA), 8.0).move_to(DOWN * 2.6) + b.play(FadeIn(tij, shift=UP * 0.15), run_time=0.6) + + with self.beat(6) as b: + b.play(FadeOut(VGroup(corte, tij)), run_time=0.3) + b.remove(banda) + larga = cinta(4.0, 1.0, L=18.0, w=0.30, nu=260, scale=0.92, center=C) + b.play(FadeIn(larga), run_time=0.7) + t = VGroup( + self.fit(Text("no salen dos", font=FONT, font_size=50, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("sale UNA, el doble de larga", font=FONT, font_size=46, + weight=BOLD, color=AMBAR), 8.2), + ).arrange(DOWN, buff=0.3).move_to(DOWN * 2.6) + b.play(FadeIn(t[0], shift=UP * 0.15), run_time=0.5) + b.play(FadeIn(t[1], shift=UP * 0.15), run_time=0.6) + + with self.beat(7) as b: + b.play(FadeOut(t), run_time=0.3) + dc = VGroup( + self.fit(Text("y esa ya tiene", font=FONT, font_size=48, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("DOS caras", font=FONT, font_size=62, + weight=BOLD, color=CELESTE), 8.0), + ).arrange(DOWN, buff=0.28).move_to(DOWN * 2.7) + b.play(FadeIn(dc[0], shift=UP * 0.15), run_time=0.5) + b.play(FadeIn(dc[1], scale=0.75), run_time=0.7) + + with self.beat(8) as b: + b.play(FadeOut(VGroup(larga, dc)), run_time=0.35) + c1 = DashedVMobject(camino(1.0, 1.0, P["w"] / 3, stroke_color=VERDE, + stroke_width=7, **P), + num_dashes=80, color=VERDE) + c2 = DashedVMobject(camino(1.0, 1.0, -P["w"] / 3, stroke_color=VERDE, + stroke_width=7, **P), + num_dashes=80, color=VERDE) + b.play(FadeIn(banda), run_time=0.35) + b.play(Create(c1), Create(c2), run_time=max(1.1, b.floor - 1.1)) + tt = self.fit(Text("ahora cortá a 1/3 del borde", font=FONT, font_size=44, + weight=BOLD, color=VERDE), 8.2).move_to(DOWN * 2.7) + b.play(FadeIn(tt, shift=UP * 0.15), run_time=0.55) + + with self.beat(9) as b: + b.play(FadeOut(VGroup(c1, c2, tt)), run_time=0.3) + b.remove(banda) + q1 = quads(1.0, 1.0, L=9.4, w=0.22, nu=170, scale=0.95, + center=C + LEFT * 1.15) + q2 = quads(4.0, 1.0, L=18.0, w=0.22, nu=260, scale=0.72, + center=C + RIGHT * 1.15) + r1, r2 = draw(q1), draw(q2) + dep = np.array([q[1] for q in q1]) + med = float(np.median(dep)) + frente = draw([q for q in q1 # eslabon que pasa encima + if np.mean([pt[0] for pt in q[0]]) > C[0] - 1.15 + and q[1] < med]) + cadena = VGroup(r1, r2, frente) + b.play(FadeIn(cadena), run_time=0.8) + t2 = VGroup( + self.fit(Text("dos cintas distintas", font=FONT, font_size=48, + weight=BOLD, color=WHITE), 8.2), + self.fit(Text("y quedan enganchadas", font=FONT, font_size=48, + weight=BOLD, color=AMBAR), 8.2), + ).arrange(DOWN, buff=0.3).move_to(DOWN * 2.7) + b.play(FadeIn(t2[0], shift=UP * 0.15), run_time=0.5) + b.play(FadeIn(t2[1], shift=UP * 0.15), run_time=0.6) + + with self.beat(10) as b: + b.play(FadeOut(VGroup(cadena, t2)), run_time=0.4) + cta = VGroup( + self.fit(Text("una hoja y cinta", font=FONT, font_size=52, + weight=BOLD, color=WHITE), 8.0), + self.fit(Text("hacelo ahora", font=FONT, font_size=66, + weight=BOLD, color=VERDE), 8.0), + self.fit(Text("contame cómo te fue", font=FONT, font_size=42, + weight=BOLD, color=WHITE), 8.0), + ).arrange(DOWN, buff=0.32).move_to(UP * 1.4) + b.play(FadeIn(cta[0], shift=UP * 0.12), run_time=0.5) + b.play(Write(cta[1]), run_time=0.8) + b.play(FadeIn(cta[2], shift=UP * 0.12), run_time=0.5) + + self.finish() diff --git a/scenes/v16_bayes.py b/scenes/v16_bayes.py new file mode 100644 index 0000000..311dd01 --- /dev/null +++ b/scenes/v16_bayes.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from manim import * +from tiktok import * + +COLS, ROWS = 25, 40 +PX, PY = 0.27, 0.175 +GY = 1.65 # centro vertical de la grilla +RAD = 0.062 +APAGADO = "#2B3A5C" +ENFERMA = 437 # fila 17, columna 12 (queda centrada) +FALSOS = [63, 147, 232, 318, 389, 476, 551, 668, 742, 861] + + +def pos(i): + r, c = divmod(i, COLS) + return np.array([(c - (COLS - 1) / 2) * PX, + GY + ((ROWS - 1) / 2 - r) * PY, 0.0]) + + +class Bayes(TikTok): + NAME = "bayes" + + def construct(self): + self.prepare() + chip = self.chip("Probabilidad", CELESTE) + + # --- la tarjeta del test --------------------------------------------- + marco = RoundedRectangle(corner_radius=0.32, width=6.4, height=3.4, stroke_width=5, + stroke_color=CELESTE, fill_color="#121C33", + fill_opacity=1).move_to(UP * 3.5) + et = Text("TEST", font=FONT, font_size=40, weight=BOLD, color=GRIS) + num = Text("99%", font=FONT, font_size=118, weight=BOLD, color=AMBAR) + pie = Text("de precisión", font=FONT, font_size=38, weight=BOLD, color=WHITE) + VGroup(et, num, pie).arrange(DOWN, buff=0.18).move_to(marco) + tarjeta = VGroup(marco, et, num, pie) + + with self.beat(0) as b: + b.play(FadeIn(chip, shift=DOWN * 0.3), run_time=0.4) + b.play(GrowFromCenter(tarjeta), run_time=0.8) + b.play(Flash(num, color=AMBAR, line_length=0.35, num_lines=14, + flash_radius=1.7), run_time=0.9) + + sello = VGroup( + RoundedRectangle(corner_radius=0.18, width=4.7, height=1.25, stroke_width=8, + stroke_color=ROSA, fill_color="#2A0E18", fill_opacity=0.94), + Text("POSITIVO", font=FONT, font_size=60, weight=BOLD, color=ROSA)) + sello.rotate(-0.09).move_to(marco.get_bottom() + DOWN * 0.05) + + with self.beat(1) as b: + b.play(FadeIn(sello, scale=2.4), run_time=0.55) + b.play(Wiggle(sello, scale_value=1.07), run_time=1.0) + + preg = Text("La mayoría contesta:", font=FONT, font_size=40, + weight=BOLD, color=GRIS).move_to(UP * 0.45) + mal = Text("99%", font=FONT, font_size=106, weight=BOLD, color=GRIS) + mal.next_to(preg, DOWN, buff=0.28) + + with self.beat(2) as b: + b.play(FadeIn(preg, shift=UP * 0.2), run_time=0.5) + b.play(FadeIn(mal, scale=0.7), run_time=0.7) + + tacha = Line(mal.get_left() + LEFT * 0.28, mal.get_right() + RIGHT * 0.28, + stroke_width=11, color=ROSA) + bien = Text("9%", font=FONT, font_size=150, weight=BOLD, + color=VERDE).move_to(DOWN * 2.05) + + with self.beat(3) as b: + b.play(Create(tacha), run_time=0.45) + b.play(FadeIn(bien, scale=0.6), run_time=0.7) + b.play(Flash(bien, color=VERDE, flash_radius=1.7, num_lines=16), run_time=0.9) + + # --- las mil personas -------------------------------------------------