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:
1 parent
5076caa470
commit
266e2b3d14
1 file changed
+181
@@ -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()
|
||||
Reference in new issue
Block a user