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:
1 parent
bc1e1ddbf5
commit
3eaaf2eb1d
1 file changed
+158
@@ -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()
|
||||
Reference in new issue
Block a user