stereo-to-zeta: one ball, tipped, halved, shifted; shadows live

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv3n4yABxvJPJHFRhVA
This commit is contained in:
teahubxyzandClaude Fable 5.1 committed 2026-10-27 21:28:16 +00:00
1 parent bc1e1ddbf5
commit 3eaaf2eb1d
1 file changed
+158
+158
View File
@@ -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()