135 lines
5.3 KiB
Python
135 lines
5.3 KiB
Python
|
|
"""Manifold explainer 2 — XY pad projects to two numbers.
|
||
|
|
|
||
|
|
A 2-D pad is two independent 0-1 readings. Moving the dot moves A (horizontal)
|
||
|
|
and B (vertical) independently, each shown as a slider.
|
||
|
|
|
||
|
|
Render:
|
||
|
|
manim -qh s2_xy_pad.py XYPad
|
||
|
|
"""
|
||
|
|
|
||
|
|
from manim import *
|
||
|
|
|
||
|
|
BG = "#1C1C1C"
|
||
|
|
PRIMARY = "#58C4DD" # pad / control
|
||
|
|
A_COL = "#FFFF00" # A axis (horizontal)
|
||
|
|
B_COL = "#83C167" # B axis (vertical)
|
||
|
|
DIM = 0.4
|
||
|
|
MONO = "Monospace"
|
||
|
|
|
||
|
|
|
||
|
|
def slider(value_tracker, length, colour, horizontal=True):
|
||
|
|
"""A track + a knob that tracks value in [0,1] along `length`."""
|
||
|
|
if horizontal:
|
||
|
|
track = Line(LEFT * length / 2, RIGHT * length / 2, color=colour,
|
||
|
|
stroke_width=5).set_opacity(DIM)
|
||
|
|
knob = always_redraw(lambda: Dot(
|
||
|
|
track.get_start() + RIGHT * value_tracker.get_value() * length,
|
||
|
|
radius=0.13, color=colour))
|
||
|
|
fill = always_redraw(lambda: Line(
|
||
|
|
track.get_start(),
|
||
|
|
track.get_start() + RIGHT * value_tracker.get_value() * length,
|
||
|
|
color=colour, stroke_width=5))
|
||
|
|
else:
|
||
|
|
track = Line(DOWN * length / 2, UP * length / 2, color=colour,
|
||
|
|
stroke_width=5).set_opacity(DIM)
|
||
|
|
knob = always_redraw(lambda: Dot(
|
||
|
|
track.get_start() + UP * value_tracker.get_value() * length,
|
||
|
|
radius=0.13, color=colour))
|
||
|
|
fill = always_redraw(lambda: Line(
|
||
|
|
track.get_start(),
|
||
|
|
track.get_start() + UP * value_tracker.get_value() * length,
|
||
|
|
color=colour, stroke_width=5))
|
||
|
|
return VGroup(track), fill, knob
|
||
|
|
|
||
|
|
|
||
|
|
class XYPad(Scene):
|
||
|
|
def construct(self):
|
||
|
|
self.camera.background_color = BG
|
||
|
|
|
||
|
|
title = Text("One pad, two numbers", font_size=44, color=PRIMARY,
|
||
|
|
weight=BOLD, font=MONO)
|
||
|
|
self.play(Write(title), run_time=1.5)
|
||
|
|
self.wait(1.0)
|
||
|
|
self.play(title.animate.scale(0.62).to_edge(UP, buff=0.5), run_time=1.0)
|
||
|
|
self.wait(0.3)
|
||
|
|
|
||
|
|
# the pad
|
||
|
|
side = 3.4
|
||
|
|
ax = ValueTracker(0.5)
|
||
|
|
bx = ValueTracker(0.5)
|
||
|
|
|
||
|
|
pad = Square(side_length=side, color=PRIMARY, stroke_width=5)
|
||
|
|
pad.set_fill(PRIMARY, opacity=0.05)
|
||
|
|
pad.shift(LEFT * 3.0)
|
||
|
|
bl = pad.get_corner(DL)
|
||
|
|
|
||
|
|
grid = VGroup()
|
||
|
|
for f in (0.25, 0.5, 0.75):
|
||
|
|
grid.add(Line(bl + RIGHT * f * side, bl + RIGHT * f * side + UP * side,
|
||
|
|
color=PRIMARY, stroke_width=1).set_opacity(0.15))
|
||
|
|
grid.add(Line(bl + UP * f * side, bl + UP * f * side + RIGHT * side,
|
||
|
|
color=PRIMARY, stroke_width=1).set_opacity(0.15))
|
||
|
|
|
||
|
|
dot = always_redraw(lambda: Dot(
|
||
|
|
bl + RIGHT * ax.get_value() * side + UP * bx.get_value() * side,
|
||
|
|
radius=0.13, color=WHITE))
|
||
|
|
|
||
|
|
# guide lines from the dot to each axis
|
||
|
|
vline = always_redraw(lambda: DashedLine(
|
||
|
|
bl + RIGHT * ax.get_value() * side,
|
||
|
|
bl + RIGHT * ax.get_value() * side + UP * bx.get_value() * side,
|
||
|
|
color=A_COL, stroke_width=2).set_opacity(DIM))
|
||
|
|
hline = always_redraw(lambda: DashedLine(
|
||
|
|
bl + UP * bx.get_value() * side,
|
||
|
|
bl + RIGHT * ax.get_value() * side + UP * bx.get_value() * side,
|
||
|
|
color=B_COL, stroke_width=2).set_opacity(DIM))
|
||
|
|
|
||
|
|
self.play(Create(pad), Create(grid), run_time=1.2)
|
||
|
|
self.add(dot)
|
||
|
|
pad_label = Text("XY pad", font_size=24, color=PRIMARY, font=MONO)
|
||
|
|
pad_label.next_to(pad, DOWN, buff=0.4)
|
||
|
|
self.play(FadeIn(pad_label), run_time=0.6)
|
||
|
|
self.wait(0.5)
|
||
|
|
self.add(vline, hline)
|
||
|
|
|
||
|
|
# A slider (horizontal) and B slider (vertical) on the right
|
||
|
|
a_track, a_fill, a_knob = slider(ax, 3.0, A_COL, horizontal=True)
|
||
|
|
a_group = VGroup(a_track, a_fill, a_knob)
|
||
|
|
a_group.shift(RIGHT * 3.0 + UP * 1.4)
|
||
|
|
a_lab = always_redraw(lambda: Text(
|
||
|
|
f"A = {ax.get_value():.2f}", font_size=28, color=A_COL, font=MONO
|
||
|
|
).next_to(a_track, UP, buff=0.3))
|
||
|
|
|
||
|
|
b_track, b_fill, b_knob = slider(bx, 3.0, B_COL, horizontal=False)
|
||
|
|
b_group = VGroup(b_track, b_fill, b_knob)
|
||
|
|
b_group.shift(RIGHT * 3.6 + DOWN * 1.4)
|
||
|
|
b_lab = always_redraw(lambda: Text(
|
||
|
|
f"B = {bx.get_value():.2f}", font_size=28, color=B_COL, font=MONO
|
||
|
|
).next_to(b_track, RIGHT, buff=0.4))
|
||
|
|
|
||
|
|
self.play(Create(a_track), Create(b_track), run_time=1.0)
|
||
|
|
self.add(a_fill, a_knob, a_lab, b_fill, b_knob, b_lab)
|
||
|
|
self.wait(0.5)
|
||
|
|
|
||
|
|
note = Text("A reads across, B reads up -- independently",
|
||
|
|
font_size=24, color=WHITE, font=MONO).to_edge(DOWN, buff=0.55)
|
||
|
|
self.play(FadeIn(note), run_time=0.8)
|
||
|
|
self.wait(1.0)
|
||
|
|
|
||
|
|
# move horizontally only: A changes, B holds
|
||
|
|
self.play(ax.animate.set_value(0.9), run_time=1.6, rate_func=smooth)
|
||
|
|
self.wait(0.6)
|
||
|
|
# move vertically only: B changes, A holds
|
||
|
|
self.play(bx.animate.set_value(0.85), run_time=1.6, rate_func=smooth)
|
||
|
|
self.wait(0.6)
|
||
|
|
# a diagonal sweep: both change together
|
||
|
|
self.play(ax.animate.set_value(0.15), bx.animate.set_value(0.2),
|
||
|
|
run_time=2.0, rate_func=smooth)
|
||
|
|
self.wait(0.6)
|
||
|
|
self.play(ax.animate.set_value(0.65), bx.animate.set_value(0.7),
|
||
|
|
run_time=1.6, rate_func=smooth)
|
||
|
|
self.wait(1.5)
|
||
|
|
|
||
|
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||
|
|
self.wait(0.3)
|