Interactive controls
Besides the features on this page, wanim is almost fully compatible with stock manim code. If you want an AI to write a wanim demo, copy this page into your LLM.
Drag a value, flip a switch, and watch the picture respond. This page covers how to write, use, and configure such scenes.
Write an interactive scene
An interactive scene inherits from InteractiveScene and splits its script into
two methods:
class Demo(InteractiveScene):
def blueprint(self):
... # build the picture and the controls
def construct(self):
... # the animation, played after the reader presses Next
Try the example below: drag the slider, flip the switch, or type in the number. Then press > at the bottom-right of the canvas to run the animation.
How the two methods work
blueprint()builds the picture and places the controls. It stays live: while the reader is on this page, it runs again on every frame, so the picture follows the reader.construct()runs when the reader presses >. From there the scene is an ordinary manim scene:self.play,self.wait, andadd_updaterall work. Reactions (below) stop working.
Controls are ordinary mobjects. You can place them, animate them, and read their
values with get_value(). For a 3D scene, inherit from InteractiveThreeDScene
instead; the 3D section below shows an example.
Reactions: add_reaction
The connection between a control and the picture is made with add_reaction.
A reaction is a small function that changes an object based on the current
value of a control:
circle.add_reaction(lambda m: m.set_width(slider.get_value()))
Read it from the inside out:
lambda m: ...is a function that takes one argument, which the code callsm. Heremis the object the reaction belongs to, which iscircle.m.set_width(...)changes the width of that object.slider.get_value()returns the slider's current value.
wanim calls the function once per frame during the blueprint. The circle's width therefore follows the slider. You do not need to write a loop or an update step.
A few rules:
- An object can have several reactions. They run in the order you added them, so several controls can drive one object.
- A reaction can also belong to the whole scene.
self.add_reaction(refresh)callsrefresh(scene)once per frame, after the object reactions. Use it for updates that touch several objects at once, such as rearranging a group. - Reactions run only during the blueprint. In
construct(), useadd_updater. - Reactions run every frame, so keep them cheap. The most common slowdown is
creating new
Textobjects inside a reaction. For a number that changes, create aDecimalNumberonce and callset_value(...)on it each frame.
Interactive 3D scenes
InteractiveThreeDScene is the 3D version. It has the 3D camera of
ThreeDScene and the blueprint and construct model above. On an idle blueprint
page, the reader can drag empty space to orbit the camera and scroll to
zoom. Controls keep working: pointer events go to a control first, and only a
drag on the background moves the camera.
Labels, controls, and any HUD text should be added with
add_fixed_in_frame_mobjects. Otherwise they are placed in the 3D scene and
tilt with the camera. The camera moves only on idle blueprint pages. While
self.play(...) runs, orbit and zoom are paused.
How to use it
While a blueprint page is idle, you can:
- Drag a slider's knob along its track.
- Click a switch to toggle it.
- Click a number to type a new value. Press Enter, or click elsewhere, to confirm. Press Escape to cancel. Drag a number up or down to scrub it.
- Drag on a 3D scene's background to orbit, and scroll to zoom.
To move on, use the buttons in the bottom-right corner of the canvas:
>goes to the next page. On a blueprint, this runs itsconstruct().<goes to the previous page.↻replays the current page.
Keyboard shortcuts: Space or → for the next page, ← for the previous page,
and ↑ to replay. They do not fire while you are typing in a number field.
Controls
| Control | Interaction | get_value() returns |
|---|---|---|
Slider(start, end, value_range, init_value) | drag along a straight track | float |
PathSlider(path, value_range, is_closed) | drag along any curve or closed loop | float |
Switch(value) | click to toggle | bool |
InputDecimalNumber(value, num_decimal_places) | click to type, drag to scrub | float |
InputInteger(value) | click to type, drag to scrub in whole steps | int |
InputDecimalMatrix([[...], ...]) | edit any cell; the matrix re-lays out | the cell's get_value() |
InputDecimalTable([[...], ...]) | edit any cell; grid lines rebuild | the cell's get_value() |
Slider and PathSlider
Slider moves along the straight line from start to end. value_range sets
the values at the two ends, and init_value sets the starting value.
slider = Slider(
start=axes.c2p(0, 0),
end=axes.c2p(4, 0),
value_range=(0.5, 3), # the left end is 0.5, the right end is 3
init_value=1,
color=YELLOW,
track_opacity=1, # set to 0 to hide the track
knob_opacity=1, # set to 0 to hide the knob
)
PathSlider works the same way along any VMobject, such as an arc, a
polyline, or a VGroup joined into a path. Values follow arc length, and
dragging stays on the path. For a closed loop such as a Circle, set
is_closed=True. Values then wrap around, and dragging across the seam continues
from the other side.
path = Arc(radius=2, start_angle=0, angle=PI)
angle_slider = PathSlider(path, value_range=(0, 180), init_value=90, color=YELLOW)
Hovering over the knob or dragging it enlarges it slightly. Set
knob_active_scale to change the amount, or to 1 to turn the effect off.
To use your own shape as the knob, hide the built-in knob and attach the shape:
thumb = Triangle(fill_opacity=1, color=BLUE)
thumb.move_to(slider.knob.get_center())
slider.attach(thumb) # the built-in knob becomes invisible; thumb follows the value
unattach() removes the attached shapes, and attached lists them. Hit testing
always uses the track, so the control can still be dragged when the knob is
invisible.
Switch
is_green = Switch(True) # the first argument is the starting state
is_green.set_value(False)
is_green.toggle()
square.add_reaction(lambda m: m.set_color(GREEN if is_green.get_value() else RED))
Number inputs
InputDecimalNumber accepts the same arguments as DecimalNumber, plus:
| Argument | Default | Meaning |
|---|---|---|
value_range | none | closed range (low, high). Typed values outside it are clamped to the range |
drag_pixels | 150 | how many pixels of vertical drag cover the whole range |
drag_span | the range width | how much the value changes over drag_pixels |
highlight_opacity | 0.25 | strength of the hover highlight behind the number |
Commas are off by default, so 10000 is shown as 10000, not 10,000.
InputInteger works the same way with no decimal places. Its value is always an
integer, and dragging moves in whole steps.
num_decimal_places also changes how the value is displayed. With one decimal
place, a typed 2.03 is shown as 2.0. Use a fixed-precision input when the
displayed text must match exactly what the reader typed.
Matrix and table inputs
InputDecimalMatrix and InputDecimalTable accept the same arguments as
DecimalMatrix and DecimalTable. Every cell is an editable number. When a cell
changes, the layout updates and the matrix keeps its centre. Pass options to each
cell with element_to_mobject_config, for example
{"value_range": (0, 10)}.
A = InputDecimalMatrix([[1, 2], [3, 4]], num_decimal_places=1, h_buff=1.5)
A.to_edge(LEFT)
cells = A.get_entries()
det = DecimalNumber(0, num_decimal_places=2).to_edge(DOWN)
det.add_reaction(lambda m: m.set_value(
cells[0].get_value() * cells[3].get_value()
- cells[1].get_value() * cells[2].get_value()
))
self.add(A, det)
For a table, read one cell with T.get_entries_without_labels([1, 1]).get_value().
If your code changes a cell's value, call relayout() to update the layout.
Multi-step stories
A scene with one blueprint() and one construct() is a single page. To go
through several pages in order, use set_storyboard. It takes a list of
[blueprint, construct] pairs, one pair per page. set_script is an alias.
Define it in setup():
Each pair is one page, and the > button moves through them in order. In a
pair, either half can be None to skip it.
Slides mode
By default, a presentation plays each construct() through and then moves on to
the next scene. It pauses only at the start of each blueprint. For
lecture-style material, turn on slides mode at the top of the file:
wanim_config({"slides_mode": True})
In slides mode, every blueprint and every construct ends on a pause, so each one
is a page of its own. < returns to the previous page and restores the
control values the reader had set on that blueprint. ↻ replays a blueprint
from a clean state, and replays a construct's animation.
Several Scene classes in one file play in order, as one presentation. Helper
classes that are not scenes are ignored.