diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 20:21:47 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 20:21:47 -0300 |
| commit | 59355909f2de9236af8168a26c70bcf6caa3b285 (patch) | |
| tree | 686186e2086f81aa22ad25e78eb29fca3cbadc9a /scenes/v15_mobius.py | |
| download | 100cia-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.py | 241 |
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() |