aboutsummaryrefslogtreecommitdiffstats
path: root/scenes/v15_mobius.py
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 20:21:47 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 20:21:47 -0300
commit59355909f2de9236af8168a26c70bcf6caa3b285 (patch)
tree686186e2086f81aa22ad25e78eb29fca3cbadc9a /scenes/v15_mobius.py
download100cia-videos-59355909f2de9236af8168a26c70bcf6caa3b285.tar.gz
100cia-videos-59355909f2de9236af8168a26c70bcf6caa3b285.zip
Import video pipeline as it was
Diffstat (limited to 'scenes/v15_mobius.py')
-rw-r--r--scenes/v15_mobius.py241
1 files changed, 241 insertions, 0 deletions
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()