# -*- 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()