wick: Manim scenes — S³ projected from −1, locked vs one-sided multiplication, the four quarter turns, chart reading, zeta and gamma

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 23:56:09 +00:00
1 parent 5076caa470
commit 266e2b3d14
1 file changed
+181
+181
View File
@@ -0,0 +1,181 @@
from manim import *
import json, numpy as np
D = json.load(open('durations.json'))
BG = "#0b0a12"; GOLD = "#f0b44c"; PINK = "#e2559a"; TEAL = "#4fd1c5"; FG = "#e9e4f2"; MUTED = "#9a93ad"
COOL = "#6f7ad8"; WARM = "#e0a050"; GREY = "#8a8a9a"
config.background_color = BG
S = 1.7 # unit sphere radius on screen
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)
# --- the quaternion picture --------------------------------------------------------------
# p = b i + c j + d k on the unit sphere; project S^3 from -1: (q1,q2,q3)/(1+q0)
def one_sided(b, c, d, th): # q = e^{i th} p, then project from -1
ct, st = np.cos(th), np.sin(th)
q0, q1, q2, q3 = -b*st, b*ct, c*ct - d*st, d*ct + c*st
den = 1 + q0
if abs(den) < 1e-3: den = 1e-3 if den >= 0 else -1e-3
v = np.array([q1, q2, q3]) / den
n = np.linalg.norm(v)
if n > 9: v = v / n * 9
return S*v
def sandwich(b, c, d, th): # q p q^-1 with q = e^{i th/2}: rotation about i by th
ct, st = np.cos(th), np.sin(th)
return S*np.array([b, c*ct - d*st, d*ct + c*st])
# lat-long about the j axis: latitude c = cos(phi), (b, d) = sin(phi)(cos psi, sin psi)
def jpt(phi, psi): return np.sin(phi)*np.cos(psi), np.cos(phi), np.sin(phi)*np.sin(psi)
def grid(f, th_tracker, lats=7, lons=6):
g = VGroup()
for k in range(1, lats+1):
phi = k*PI/(lats+1)
col = COOL if phi < PI/2-1e-6 else (WARM if phi > PI/2+1e-6 else GOLD)
w = 3.0 if abs(phi-PI/2) < 1e-6 else 1.4
g.add(always_redraw(lambda phi=phi, col=col, w=w: ParametricFunction(lambda t: f(*jpt(phi, t), th_tracker.get_value()), t_range=[0, TAU, TAU/110], color=col, stroke_width=w)))
for k in range(lons):
psi = k*PI/lons
g.add(always_redraw(lambda psi=psi: ParametricFunction(lambda p: f(*jpt(p, psi), th_tracker.get_value()), t_range=[0.02, PI-0.02, PI/60], color=GREY, stroke_width=1.0)))
return g
class Q(Timed, ThreeDScene):
def cam(self, phi=66, theta=-38, zoom=0.95):
self.set_camera_orientation(phi=phi*DEGREES, theta=theta*DEGREES, zoom=zoom)
def label(self, text, size=0.5):
t = cap(text, size); self.add_fixed_in_frame_mobjects(t); return t
def axes(self):
a = VGroup(Line3D(-S*1.25*RIGHT, S*1.25*RIGHT, color="#3a3550", thickness=0.01), Line3D(-S*1.25*UP, S*1.25*UP, color="#3a3550", thickness=0.01), Line3D(-S*1.25*OUT, S*1.25*OUT, color="#3a3550", thickness=0.01))
li = Text("i", font="Georgia", slant=ITALIC, color=FG).scale(0.6); lj = Text("j", font="Georgia", slant=ITALIC, color=FG).scale(0.6); lk = Text("k", font="Georgia", slant=ITALIC, color=FG).scale(0.6)
self.add_fixed_orientation_mobjects(li, lj, lk); li.move_to(S*1.4*RIGHT); lj.move_to(S*1.4*UP); lk.move_to(S*1.4*OUT)
one = Dot3D(ORIGIN, radius=0.06, color=FG); l1 = Text("1", font="Georgia", color=MUTED).scale(0.45); self.add_fixed_orientation_mobjects(l1); l1.move_to(0.3*DOWN+0.25*IN)
return VGroup(a, li, lj, lk, one, l1)
def dial(self, tr):
# fixed-in-frame dial: a circle with a needle at theta, quarter marks
c = Circle(radius=0.55, color="#3a3550", stroke_width=2).to_corner(UR, buff=0.5)
marks = VGroup(*[Line(c.get_center()+0.47*np.array([np.cos(a), np.sin(a), 0]), c.get_center()+0.55*np.array([np.cos(a), np.sin(a), 0]), color=MUTED, stroke_width=2) for a in (0, PI/2, PI, 3*PI/2)])
needle = always_redraw(lambda: Line(c.get_center(), c.get_center()+0.5*np.array([np.cos(tr.get_value()), np.sin(tr.get_value()), 0]), color=PINK, stroke_width=3))
lab = always_redraw(lambda: Text("θ = %d°" % round(tr.get_value()*180/PI), font="Georgia", color=MUTED).scale(0.4).next_to(c, DOWN, buff=0.1))
self.add_fixed_in_frame_mobjects(c, marks, needle, lab)
return VGroup(c, marks, needle, lab)
# ---------- W0: the sphere we watch
class W0(Q):
def construct(self):
self.setup_time(0); self.cam()
ax = self.axes(); self.add(ax)
tr = ValueTracker(0.0); g = grid(one_sided, tr)
self.go(Create(g), rt=3.0)
self.label("unit quaternions, projected from −1: the pure imaginaries are this sphere")
self.begin_ambient_camera_rotation(rate=0.06)
self.rest()
# ---------- W1: locked sandwich, the sphere spins
class W1(Q):
def construct(self):
self.setup_time(1); self.cam()
ax = self.axes(); self.add(ax)
tr = ValueTracker(0.0); g = grid(sandwich, tr); self.add(g)
f = Text("p ↦ q p q⁻¹", font="Georgia", slant=ITALIC, color=FG).scale(0.7).to_corner(UL, buff=0.5); self.add_fixed_in_frame_mobjects(f)
self.dial(tr)
self.go(FadeIn(f), rt=0.8)
self.go(tr.animate.set_value(TAU), rt=7.0, rate_func=linear)
self.label("locked: the sphere spins in place and never leaves itself")
self.rest()
# ---------- W2: one-sided, to the plane
class W2(Q):
def construct(self):
self.setup_time(2); self.cam(70, -40, 0.8)
ax = self.axes(); self.add(ax)
tr = ValueTracker(0.0); g = grid(one_sided, tr); self.add(g)
f = Text("p ↦ q p, q = e^{iθ}", font="Georgia", slant=ITALIC, color=FG).scale(0.7).to_corner(UL, buff=0.5); self.add_fixed_in_frame_mobjects(f)
self.dial(tr)
self.go(FadeIn(f), rt=0.8)
self.go(Wait(), rt=3.0)
self.go(tr.animate.set_value(PI/2), rt=7.0, rate_func=smooth)
self.label("quarter turn: the same sphere, one point sent to infinity — a plane")
self.rest()
# ---------- W3: the four quarter turns
class W3(Q):
def construct(self):
self.setup_time(3); self.cam(70, -40, 0.8)
ax = self.axes(); self.add(ax)
tr = ValueTracker(PI/2); g = grid(one_sided, tr); self.add(g)
self.dial(tr)
c = self.label("¼: plane")
self.go(Wait(), rt=1.0)
self.go(tr.animate.set_value(PI), rt=4.0, rate_func=smooth); self.go(FadeOut(c), rt=0.1); c = self.label("½: sphere, inside out")
self.go(Wait(), rt=2.0)
self.go(tr.animate.set_value(3*PI/2), rt=3.5, rate_func=smooth); self.go(FadeOut(c), rt=0.1); c = self.label("¾: plane, facing the other way")
self.go(Wait(), rt=1.5)
self.go(tr.animate.set_value(TAU), rt=3.5, rate_func=smooth); self.go(FadeOut(c), rt=0.1); c = self.label("1: home — sphere, plane, sphere, plane, sphere")
self.rest()
# ---------- W4: chart reading
class W4(Q):
def construct(self):
self.setup_time(4); self.cam(70, -40, 0.8)
ax = self.axes(); self.add(ax)
tr = ValueTracker(PI/2); g = grid(one_sided, tr); self.add(g)
t1 = Text("plane = a chart: the sphere minus one point", font="Georgia", color=FG).scale(0.55).to_edge(UP, buff=0.5); self.add_fixed_in_frame_mobjects(t1)
self.go(FadeIn(t1), rt=1.0)
self.go(Wait(), rt=3.0)
t2 = Text("sphere = the whole thing (projective)", font="Georgia", color=GOLD).scale(0.55).next_to(t1, DOWN, buff=0.15); self.add_fixed_in_frame_mobjects(t2)
self.go(FadeIn(t2), rt=1.0)
self.go(tr.animate.set_value(0.0), rt=5.0, rate_func=smooth)
self.label("putting the missing point back = analytic continuation")
self.begin_ambient_camera_rotation(rate=0.05)
self.rest()
# ---------- W5: the zeta grid, face on
class W5(Q):
def construct(self):
self.setup_time(5)
self.set_camera_orientation(phi=90*DEGREES, theta=0*DEGREES, zoom=0.9) # look along the i axis: the plane phase face on (y = j, z = k)
tr = ValueTracker(0.0); g = grid(one_sided, tr); self.add(g)
self.go(Wait(), rt=1.0)
self.go(tr.animate.set_value(PI/2), rt=4.0, rate_func=smooth)
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(S*(-1)*OUT + 0.35*DOWN*0 + 0.4*LEFT*0 + 0.35*RIGHT*0 + 0.0); m1.move_to(np.array([0, -0.45, -S])); p1.move_to(np.array([0, -0.45, S]))
d0 = Dot3D(np.array([0, 0, -S]), radius=0.07, color=TEAL); d1 = Dot3D(np.array([0, 0, S]), radius=0.07, color=TEAL)
self.go(FadeIn(d0), FadeIn(d1), FadeIn(m1), FadeIn(p1), rt=0.8)
c = self.label("plane phase: latitude and longitude about j become circles around −1 and 1")
self.go(Wait(), rt=3.0)
# freeze a copy of the current grid, then halve and slide so the foci land on 0 and 1 (along k)
frozen = VGroup(*[m.copy().clear_updaters() for m in g]); self.remove(g); self.add(frozen)
grp = VGroup(frozen, d0, d1)
self.go(grp.animate.scale(0.5, about_point=ORIGIN), rt=1.8)
self.go(grp.animate.shift(0.5*S*OUT), rt=1.8)
self.go(FadeOut(c), FadeOut(m1), FadeOut(p1), rt=0.2)
z0 = Text("0", font="Georgia", color=TEAL).scale(0.5); o1 = Text("1", font="Georgia", color=TEAL).scale(0.5); h = Text("½ critical line", font="Georgia", color=GOLD).scale(0.45)
self.add_fixed_orientation_mobjects(z0, o1, h); z0.move_to(np.array([0, -0.45, 0])); o1.move_to(np.array([0, -0.45, S])); h.move_to(np.array([0, 2.6, 0.5*S]))
self.go(FadeIn(z0), FadeIn(o1), FadeIn(h), rt=0.8)
self.label("halve and slide by ½: zeta's grid, poles on 0 and 1, equator on the critical line")
self.rest()
# ---------- W6: the gamma factor
class W6(Q):
def construct(self):
self.setup_time(6); self.cam(66, -38, 0.9)
ax = self.axes(); self.add(ax)
tr = ValueTracker(0.0); g = grid(one_sided, tr); self.add(g)
self.begin_ambient_camera_rotation(rate=0.05)
t1 = Text("ζ: far hemisphere = mirror of the near one × Γ-factor", font="Georgia", color=FG).scale(0.5).to_edge(UP, buff=0.5); self.add_fixed_in_frame_mobjects(t1)
self.go(FadeIn(t1), rt=1.0)
self.go(Wait(), rt=4.0)
t2 = Text("ξ = ζ dressed with the Γ-factor: ξ(s) = ξ(1 − s)", font="Georgia", color=GOLD).scale(0.5).next_to(t1, DOWN, buff=0.15); self.add_fixed_in_frame_mobjects(t2)
self.go(FadeIn(t2), rt=1.0)
self.go(Wait(), rt=3.0)
self.go(tr.animate.set_value(PI), rt=4.0, rate_func=smooth)
self.label("sphere to plane is continuation; the gamma factor is the fare")
self.rest()