memlnaut-nisps/assets/media/src/s1_knob_mapping.py

113 lines
4.1 KiB
Python

"""Manifold explainer 1 — Knob = 1:1 mapping.
A knob rotates; a paired 0-1 number rises and falls in lockstep. The point: a
physical control is nothing more than a readout of one number.
Render:
manim -qh s1_knob_mapping.py KnobMapping
"""
from manim import *
BG = "#1C1C1C"
PRIMARY = "#58C4DD" # control side
NUMBER = "#83C167" # the number / value side
ACCENT = "#FFFF00" # the live value
DIM = 0.4
MONO = "Monospace"
def knob_group(value_tracker, radius=1.3):
"""A dial whose pointer angle tracks value in [0,1] over a 300 degree sweep."""
start_ang = -150 * DEGREES # bottom-left
sweep = 300 * DEGREES
face = Circle(radius=radius, color=PRIMARY, stroke_width=6)
face.set_fill(PRIMARY, opacity=0.06)
# the swept arc (the track the pointer travels along)
track = Arc(radius=radius + 0.18, start_angle=start_ang, angle=sweep,
color=PRIMARY, stroke_width=4).set_opacity(DIM)
def make_pointer():
ang = start_ang + value_tracker.get_value() * sweep
tip = np.array([np.cos(ang), np.sin(ang), 0]) * (radius - 0.18)
line = Line(ORIGIN, tip, color=ACCENT, stroke_width=8)
return line
pointer = always_redraw(make_pointer)
hub = Dot(ORIGIN, radius=0.08, color=ACCENT)
return VGroup(face, track), pointer, hub
class KnobMapping(Scene):
def construct(self):
self.camera.background_color = BG
title = Text("A knob is just a number", 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)
val = ValueTracker(0.5)
dial, pointer, hub = knob_group(val)
dial.shift(LEFT * 3.2)
pointer.shift(LEFT * 3.2)
hub.shift(LEFT * 3.2)
knob_label = Text("control", font_size=24, color=PRIMARY, font=MONO)
knob_label.next_to(dial, DOWN, buff=0.4)
self.play(Create(dial), FadeIn(hub), run_time=1.2)
self.add(pointer)
self.play(FadeIn(knob_label), run_time=0.8)
self.wait(0.5)
# the number side: a vertical 0-1 bar + live readout
bar_h = 3.0
bar = Rectangle(width=0.5, height=bar_h, color=NUMBER, stroke_width=4)
bar.shift(RIGHT * 2.6)
bar_bottom = bar.get_bottom()
fill = always_redraw(lambda: Rectangle(
width=0.5, height=max(1e-3, val.get_value() * bar_h),
color=NUMBER, fill_color=NUMBER, fill_opacity=0.8, stroke_width=0,
).move_to(bar_bottom + UP * (val.get_value() * bar_h) / 2))
tick0 = Text("0", font_size=20, color=NUMBER, font=MONO).next_to(bar, DL, buff=0.15)
tick1 = Text("1", font_size=20, color=NUMBER, font=MONO).next_to(bar, UL, buff=0.15)
readout = always_redraw(lambda: Text(
f"{val.get_value():.2f}", font_size=40, color=ACCENT, font=MONO,
).next_to(bar, RIGHT, buff=0.5))
num_label = Text("value", font_size=24, color=NUMBER, font=MONO)
num_label.next_to(bar, DOWN, buff=0.55)
# the binding arrow
arrow = Arrow(dial.get_right() + RIGHT * 0.1, bar.get_left() + LEFT * 0.1,
color=WHITE, stroke_width=4, buff=0.2).set_opacity(DIM)
self.play(Create(bar), FadeIn(tick0), FadeIn(tick1), run_time=1.0)
self.add(fill, readout)
self.play(GrowArrow(arrow), FadeIn(num_label), run_time=1.0)
self.wait(1.0)
bind = Text("they move in lockstep", font_size=26, color=WHITE, font=MONO)
bind.to_edge(DOWN, buff=0.6)
self.play(FadeIn(bind), run_time=0.8)
self.wait(0.8)
# sweep up, down, and to extremes -- pointer and number never disagree
self.play(val.animate.set_value(1.0), run_time=2.0, rate_func=smooth)
self.wait(0.8)
self.play(val.animate.set_value(0.0), run_time=2.5, rate_func=smooth)
self.wait(0.8)
self.play(val.animate.set_value(0.73), run_time=1.5, rate_func=smooth)
self.wait(1.5)
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
self.wait(0.3)