Skip to main content

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, and add_updater all 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 calls m. Here m is the object the reaction belongs to, which is circle.
  • 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) calls refresh(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(), use add_updater.
  • Reactions run every frame, so keep them cheap. The most common slowdown is creating new Text objects inside a reaction. For a number that changes, create a DecimalNumber once and call set_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 its construct().
  • < 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​

ControlInteractionget_value() returns
Slider(start, end, value_range, init_value)drag along a straight trackfloat
PathSlider(path, value_range, is_closed)drag along any curve or closed loopfloat
Switch(value)click to togglebool
InputDecimalNumber(value, num_decimal_places)click to type, drag to scrubfloat
InputInteger(value)click to type, drag to scrub in whole stepsint
InputDecimalMatrix([[...], ...])edit any cell; the matrix re-lays outthe cell's get_value()
InputDecimalTable([[...], ...])edit any cell; grid lines rebuildthe 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:

ArgumentDefaultMeaning
value_rangenoneclosed range (low, high). Typed values outside it are clamped to the range
drag_pixels150how many pixels of vertical drag cover the whole range
drag_spanthe range widthhow much the value changes over drag_pixels
highlight_opacity0.25strength 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.

note

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.