From 3eaaf2eb1d8e51bbc6a759e5a90283addda60fa7 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Tue, 27 Oct 2026 21:28:16 +0000 Subject: [PATCH] stereo-to-zeta: one ball, tipped, halved, shifted; shadows live Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FAqjv3n4yABxvJPJHFRhVA --- src/zeta-ball/zb2.py | 158 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/zeta-ball/zb2.py diff --git a/src/zeta-ball/zb2.py b/src/zeta-ball/zb2.py new file mode 100644 index 0000000..3326912 --- /dev/null +++ b/src/zeta-ball/zb2.py @@ -0,0 +1,158 @@ +from manim import * +import json, numpy as np, mpmath + +D = json.load(open('durations2.json')) +BG = "#0b0a12"; GOLD = "#f0b44c"; PINK = "#e2559a"; TEAL = "#4fd1c5"; FG = "#e9e4f2"; MUTED = "#9a93ad" +COOL = "#6f7ad8"; WARM = "#e0a050"; GREY = "#8a8a9a" +config.background_color = BG +mpmath.mp.dps = 15 + +class Timed: + def setup_time(self, i): + self.budget = D[i] + 0.6; self.used = 0.0 + def go(self, *anims, rt=1.0): + self.play(*anims, run_time=rt); self.used += rt + def rest(self, extra=0.0): + self.wait(max(0.1, self.budget - self.used + extra)) + +def cap(text, size=0.5): + return Text(text, font="Georgia", color=MUTED).scale(size).to_edge(DOWN, buff=0.45) + +R = 1.6 # starting radius; s-unit = 2R plane units (foci at ±2R <-> s = ±1) + +class Ball: + """One ball, described by three trackers: tilt angle a (0 -> pi/2), scale k (1 -> 1/2), shift h (0 -> R).""" + def __init__(self): + self.a = ValueTracker(0.0); self.k = ValueTracker(1.0); self.h = ValueTracker(0.0) + def rad(self): return R*self.k.get_value() + def centre(self): return np.array([self.h.get_value(), 0, self.rad()]) + def lamp(self): return np.array([self.h.get_value(), 0, 2*self.rad()]) + def q(self, phi, th): + x, y, z = np.sin(phi)*np.cos(th), np.sin(phi)*np.sin(th), -np.cos(phi) # axis vertical: pole phi=0 at the bottom + a = self.a.get_value(); ca, sa = np.cos(a), np.sin(a) + return np.array([x*ca + z*sa, y, -x*sa + z*ca]) + def pt(self, phi, th): return self.centre() + self.rad()*self.q(phi, th) + def shadow(self, b): + L = self.lamp(); d = b - L; t = -L[2] / min(d[2], -1e-3) + return np.clip(L + t*d, -12, 12) + def lat(self, phi, col, w, n=120): + return always_redraw(lambda: ParametricFunction(lambda t: self.pt(phi, t), t_range=[0, TAU, TAU/n], color=col, stroke_width=w)) + def lon(self, th, col, w): + return always_redraw(lambda: ParametricFunction(lambda p: self.pt(p, th), t_range=[0.02, PI-0.02, PI/60], color=col, stroke_width=w)) + def lat_sh(self, phi, col, w): + return always_redraw(lambda: ParametricFunction(lambda t: self.shadow(self.pt(phi, t)), t_range=[0, TAU, TAU/160], color=col, stroke_width=w)) + def lon_sh(self, th, col, w): + return always_redraw(lambda: ParametricFunction(lambda p: self.shadow(self.pt(p, th)), t_range=[0.03, PI-0.03, PI/90], color=col, stroke_width=w)) + def mesh(self): + return always_redraw(lambda: Sphere(center=self.centre(), radius=self.rad()*0.99, resolution=(18, 36)).set_fill("#1d1a2b", opacity=0.42).set_stroke("#3a3550", width=0.3, opacity=0.35)) + def lamp_dot(self): return always_redraw(lambda: Dot3D(self.lamp(), radius=0.07, color=FG)) + def pole_dots(self): + return (always_redraw(lambda: Dot3D(self.pt(0, 0), radius=0.07, color=TEAL)), always_redraw(lambda: Dot3D(self.pt(PI, 0), radius=0.07, color=TEAL)), + always_redraw(lambda: Dot3D(self.shadow(self.pt(0, 0)), radius=0.07, color=TEAL)), always_redraw(lambda: Dot3D(self.shadow(self.pt(PI, 0)), radius=0.07, color=TEAL))) + def grid(self): + g = VGroup() + for phi in np.linspace(0.3, PI-0.3, 8): + col = COOL if phi < PI/2-1e-6 else (WARM if phi > PI/2+1e-6 else GOLD) + g.add(self.lat(phi, col, 1.4)) + for th in np.linspace(0, PI, 7, endpoint=False): + g.add(self.lon(th, GREY, 1.0)) + return g + def grid_sh(self): + g = VGroup() + for phi in np.linspace(0.3, PI-0.3, 8): + col = COOL if phi < PI/2-1e-6 else (WARM if phi > PI/2+1e-6 else GOLD) + g.add(self.lat_sh(phi, col, 1.4)) + for th in np.linspace(0, PI, 7, endpoint=False): + g.add(self.lon_sh(th, GREY, 1.0)) + return g + +def floor(): + return NumberPlane(x_range=[-8, 8, 1], y_range=[-5, 5, 1], background_line_style={"stroke_color": "#2a2638", "stroke_width": 1, "stroke_opacity": 0.5}, axis_config={"stroke_color": "#3a3550"}) + +class Base(Timed, ThreeDScene): + def build(self, a=0.0, k=1.0, h=0.0): + self.B = Ball(); self.B.a.set_value(a); self.B.k.set_value(k); self.B.h.set_value(h) + self.add(floor()) + self.sh = self.B.grid_sh(); self.add(self.sh) + self.mesh = self.B.mesh(); self.add(self.mesh) + self.gr = self.B.grid(); self.add(self.gr) + self.ld = self.B.lamp_dot(); self.add(self.ld) + self.pd = self.B.pole_dots(); self.add(*self.pd) + def label(self, text): + t = cap(text); self.add_fixed_in_frame_mobjects(t); return t + +class T0(Base): + def construct(self): + self.setup_time(0) + self.set_camera_orientation(phi=60*DEGREES, theta=-40*DEGREES, zoom=0.85) + self.build() + self.go(Wait(), rt=1.0) + # a ray sweeping to show the projection + tr = ValueTracker(0.0) + bp = lambda: self.B.pt(0.9 + 0.5*np.sin(1.3*tr.get_value()), tr.get_value()) + ray = always_redraw(lambda: Line(self.B.lamp(), self.B.shadow(bp()), color=GOLD, stroke_width=1.6, stroke_opacity=0.8)) + d1 = always_redraw(lambda: Dot3D(bp(), radius=0.05, color=GOLD)); d2 = always_redraw(lambda: Dot3D(self.B.shadow(bp()), radius=0.05, color=GOLD)) + self.go(FadeIn(ray), FadeIn(d1), FadeIn(d2), rt=0.6) + self.go(tr.animate.set_value(TAU), rt=5.0) + self.go(FadeOut(ray), FadeOut(d1), FadeOut(d2), rt=0.5) + self.label("the usual picture: circles and rays") + self.rest() + +class T1(Base): + def construct(self): + self.setup_time(1) + self.set_camera_orientation(phi=60*DEGREES, theta=-40*DEGREES, zoom=0.85) + self.build() + self.go(Wait(), rt=2.0) + self.go(self.B.a.animate.set_value(PI/2), rt=5.0, ) + self.go(Wait(), rt=1.0) + m1 = Text("−1", font="Georgia", color=TEAL).scale(0.5); p1 = Text("1", font="Georgia", color=TEAL).scale(0.5) + self.add_fixed_orientation_mobjects(m1, p1); m1.move_to(np.array([-2*R, -0.45, 0])); p1.move_to(np.array([2*R, -0.45, 0])) + self.go(FadeIn(m1), FadeIn(p1), rt=0.8) + self.label("tip it a quarter turn: the poles land on −1 and 1") + self.rest() + +class T2(Base): + def construct(self): + self.setup_time(2) + self.set_camera_orientation(phi=60*DEGREES, theta=-40*DEGREES, zoom=0.85) + self.build(a=PI/2) + self.go(Wait(), rt=1.5) + self.go(self.B.k.animate.set_value(0.5), rt=2.6) + self.go(self.B.h.animate.set_value(R), rt=2.6) + z0 = Text("0", font="Georgia", color=TEAL).scale(0.5); o1 = Text("1", font="Georgia", color=TEAL).scale(0.5); hf = Text("½", font="Georgia", color=FG).scale(0.5) + self.add_fixed_orientation_mobjects(z0, o1, hf); z0.move_to(np.array([0, -0.45, 0])); o1.move_to(np.array([2*R, -0.45, 0])); hf.move_to(np.array([R, -0.45, 0])) + self.go(FadeIn(z0), FadeIn(o1), FadeIn(hf), rt=0.8) + eq = always_redraw(lambda: ParametricFunction(lambda t: self.B.pt(PI/2, t), t_range=[0, TAU, TAU/120], color=GOLD, stroke_width=4)) + crit = Line(np.array([R, -4.5, 0]), np.array([R, 4.5, 0]), color=GOLD, stroke_width=3) + self.go(Create(eq), rt=1.2); self.go(Create(crit), rt=1.4) + self.label("halve it, slide it by ½: poles on 0 and 1, equator on the critical line") + self.rest() + +def zeta(s): + return complex(mpmath.zeta(mpmath.mpc(s.real, s.imag))) + +class T3(Base): + def construct(self): + self.setup_time(3) + self.set_camera_orientation(phi=60*DEGREES, theta=-40*DEGREES, zoom=0.85) + self.build(a=PI/2, k=0.5, h=R) + eq = ParametricFunction(lambda t: self.B.pt(PI/2, t), t_range=[0, TAU, TAU/120], color=GOLD, stroke_width=4) + crit = Line(np.array([R, -4.5, 0]), np.array([R, 4.5, 0]), color=GOLD, stroke_width=2, stroke_opacity=0.6) + self.add(eq, crit) + self.go(Wait(), rt=1.5) + # the real zeta, in the output plane: w=0 at the plane origin, w=1 at 2R + out = VGroup() + def W(w): return np.clip(np.array([2*R*w.real, 2*R*w.imag, 0.0]), -12, 12) + for sig in list(np.linspace(-3.0, -0.3, 6)) + list(np.linspace(1.2, 4.0, 7)): + col = COOL if sig < 0.5 else WARM + out.add(ParametricFunction(lambda t, sig=sig: W(zeta(complex(sig, t))), t_range=[-4.9, 4.9, 0.02], color=col, stroke_width=2.2)) + for t0 in [t for t in (0.45, 0.95, 1.55, 2.25, 3.05, 3.95) for t in (t, -t)]: + out.add(ParametricFunction(lambda x, t0=t0: W(zeta(complex(x, t0))), t_range=[-3.0, 4.0, 0.02], color=FG, stroke_width=1.2, stroke_opacity=0.8)) + self.go(Create(out), rt=4.0) + self.go(Wait(), rt=0.8) + z = Text("ζ", font="Georgia", color=GOLD).scale(1.2).to_corner(UL, buff=0.5); self.add_fixed_in_frame_mobjects(z) + self.go(FadeIn(z), rt=0.6) + self.label("the real ζ laid on the ball's shadow: tipped, halved, shifted by ½") + self.begin_ambient_camera_rotation(rate=0.04) + self.rest()