# wanim — a practical guide for code-generating models wanim runs manim Python in the browser. The source is compiled to JavaScript and drawn live on a ``; there is no Python process, no LaTeX toolchain, no server round-trip and no video file. A scene is a live, pausable animation that can also accept pointer input. This guide is served at https://wanim.org/LLM.txt. Give that URL to a code-generating model instead of copying individual documentation pages; it is self-contained and covers the full supported surface. If you are asked to write a wanim scene, return a complete Python file with one or more `Scene` / `InteractiveScene` subclasses. Do not return JavaScript. ## 1. API coverage wanim targets **manim Community v0.21**. Write standard manim Python and use the manim API you already know; this guide only covers the parts that are specific to wanim: interactive scenes, controls, presentation modes and sharing. If a stock manim CE 0.21 program uses a common feature and fails, treat it as a wanim bug. Known browser-specific differences: - Math is typeset with **MathJax**, not a real LaTeX installation. Common `MathTex`/`Tex` input works; arbitrary `\usepackage`, exotic packages and full LaTeX compilation do not. Keep formulas to standard TeX math. - There is **no video output**: the result is a live canvas. Use the share API or the GIF export instead of `self.render()` / `-o file.mp4`. - No file I/O, network access, threads, `input()`, `os`, `sys`, `subprocess`, or arbitrary pip packages. `import manim`, `numpy`, `math`, `random` and `itertools` are available. - Fonts: `Text` uses the configured font chain (Latin + CJK); use `font_size`, `color`, `weight`, `slant`. `MathTex` is for formulas; `Text` is for prose. - numpy is `numpy-ts`: normal broadcasting, indexing, `np.array`, `np.linalg` and common ufuncs work; exotic dtype/stride tricks may not. ## 2. A minimal scene ```python from manim import * class Demo(Scene): def construct(self): title = Text("Hello wanim", font_size=48, color=BLUE) underline = Line(LEFT, RIGHT).next_to(title, DOWN, buff=0.2) self.play(Write(title), Create(underline)) self.wait(0.6) self.play(FadeOut(title), FadeOut(underline)) ``` - `construct()` is the entry point. Use `self.play(...)` for animations and `self.wait(...)` for real pauses. - Use `run_time=` on an animation to control duration. - Use `self.add(...)` for objects that should appear immediately. - The canvas is 16:9. Keep important content away from the extreme edges; `to_edge(...)`, `to_corner(...)` and `next_to(...)` with `buff=` are reliable. ## 3. Interactive scenes `InteractiveScene` splits a page into two methods: - `blueprint()` builds the live, controllable picture and places controls. It runs once when the page opens; every reader gesture re-runs all reactions. - `construct()` is the ordinary animation that follows. In a two-view article it is the looping animation; in a normal page it plays after the reader presses continue. ```python from manim import * class Demo(InteractiveScene): def blueprint(self): slider = Slider( start=[-4, -2.5, 0], end=[4, -2.5, 0], value_range=(0.5, 3), init_value=1, ) circle = Circle(radius=1, color=BLUE, fill_opacity=0.35) circle.add_reaction( lambda m: circle.become( Circle(radius=slider.get_value(), color=BLUE, fill_opacity=0.35) ) ) self.add(circle, slider) def construct(self): self.play(Create(Circle(radius=1, color=BLUE, fill_opacity=0.35)), run_time=1.2) self.wait(0.8) ``` Key methods and flags: - `obj.add_reaction(lambda m: ...)`: refresh one mobject after a gesture. The callback receives the object itself. Keep it cheap: update an existing object (`set_value`, `become`, `move_to`, `set_color`) instead of creating new `Text`/`MathTex` on every frame. - `self.add_reaction(lambda scene: ...)`: scene-level refresh, useful when several objects must move together (for example `group.arrange(...)`). - `control.on_input(lambda control, previous, next_value: ...)`: called when the **reader** changes a control. Use it to update your own Python state. `set_value()` from code does **not** fire `on_input`. - `SEPARATE = True`: the animation loop uses `construct()` only and the interaction view uses `blueprint()` only. This is the simplest way to pair a looping animation with a separate interactive playground. - `self.clear_stage()` at the end of `blueprint()`: the construct starts on an empty canvas, but Python references (`self.slider.get_value()`, etc.) survive. In the interaction view the clear is deferred until the reader presses continue, so controls do not vanish under their finger. - `self.theme` is `"light"` or `"dark"`; read it in `setup()` to choose colours. - `self.post_message("passed", True)`: tell the host that a small exercise was solved. The host receives the message with a page/instance id; no extra id is needed. - Only `blueprint()` = interaction-only page. Only `construct()` = animation-only page. Both = the paired two-view page. ### Multi-page stories ```python class Story(InteractiveScene): def setup(self): self.set_storyboard([ [self.page1_blueprint, self.page1_construct], [None, self.page2_construct], [self.page3_blueprint, None], ]) def page1_blueprint(self): ... ``` `set_script(...)` is an alias. Each item can also be a dict `{"blueprint": ..., "construct": ...}`. `None` skips that half. ### State pattern When several controls drive the same geometry, keep a plain Python dict/list as the model and let reactions redraw from it: ```python class State(InteractiveScene): def blueprint(self): self.state = {"r": 1.0, "color": BLUE} self.circle = Circle(radius=1, color=BLUE, fill_opacity=0.35) radius = Slider(start=[-4, -2.5, 0], end=[4, -2.5, 0], value_range=(0.5, 3), init_value=1) radius.on_input(lambda c, prev, nxt: self.state.update(r=nxt)) def refresh(_): self.circle.become( Circle(radius=self.state["r"], color=self.state["color"], fill_opacity=0.35) ) self.add_reaction(refresh) self.add(self.circle, radius) ``` ## 4. Control reference All controls implement `get_value()`, `set_value(...)` and `on_input(listener)`. `on_input` receives `(control, previous, next_value)`. Put controls directly on the stage; do not put a `Switch`/`DraggablePoint` in a group that is later scaled or moved. ### Slider ```python slider = Slider( start=[-4, -2.5, 0], # left end, a point end=[4, -2.5, 0], # right end value_range=(0.5, 3), # values at the two ends; default (0, 1) init_value=1, # starting value; defaults to the low end color=YELLOW, # track/knob colour stroke_width=6, track_opacity=1, # 0 hides the track knob_opacity=1, # 0 hides the knob knob_radius=0.12, knob_active_scale=1.25, # hover/drag growth; 1 disables hit_radius=0.3, ) ``` - Drag along the straight track; `get_value()` returns a `float`. - `slider.set_value(2)` moves it from code (does not fire `on_input`). - `slider.attach(mobject)` makes a custom thumb follow the value; the built-in knob becomes invisible. `slider.unattach(mobject)` removes it. - `slider.track` and `slider.knob` are the underlying mobjects. ### PathSlider ```python path = Arc(radius=2, start_angle=0, angle=PI) angle = PathSlider( path, value_range=(0, 180), init_value=90, is_closed=False, # True for a loop: values wrap at the seam color=YELLOW, ) ``` - Drag along any `VMobject` (arc, polyline, `VGroup` joined into a path). Values follow arc length. - `is_closed=True` for a `Circle` or other closed loop; dragging across the seam continues from the other side. - Same `get_value` / `set_value` / `attach` / `on_input` API as `Slider`. ### DraggablePoint ```python p = DraggablePoint( [1, 2, 0], # initial point, like Dot(point, ...) color=YELLOW, radius=0.12, snap=0.5, # optional grid snap; or [x_step, y_step] hit_radius=0.3, # pick radius in world units active_scale=1.25, ) p.attach(label) # label follows the point x = p.get_center()[0] # numpy array [x, y, 0] ``` - Drag freely in the plane; `get_center()` is exact even with attached mobjects. `get_value()` returns the same array. - `p.set_value([2, 1, 0])` / `p.move_to(...)` move it from code without firing `on_input`. - `p.unattach(label)` removes an attached mobject. ### Switch ```python sw = Switch(True) # initial boolean sw = Switch(value=False, width=0.66, height=0.33, color=BLUE_E) sw.get_value() # bool sw.toggle() sw.set_value(False) ``` Click anywhere on the switch to flip it. ### Chip ```python chip = Chip( "angle form", # label value=False, color=PURPLE_C, # accent colour when selected height=0.52, font_size=24, padding=0.22, corner_radius=0.11, fill_color="#202020", # unselected fill stroke_color=GREY_C, stroke_width=1.5, ) chip.get_value() # bool chip.toggle() ``` A chip is a tag-like toggle; a selected chip tints its fill, border and label. ### InputDecimalNumber ```python n = InputDecimalNumber( 1.0, num_decimal_places=1, # display precision value_range=(0, 10), # optional clamp drag_pixels=150, # vertical pixels for the full range drag_span=None, # value change over drag_pixels; defaults to range width highlight_opacity=0.25, ) n.get_value() # float n.set_value(3.5) ``` Click to type, drag vertically to scrub. Typed values outside `value_range` are clamped. `num_decimal_places` also rounds the displayed value. ### InputInteger ```python n = InputInteger(3, value_range=(0, 10)) n.get_value() # int ``` Same as `InputDecimalNumber` with zero decimals; dragging moves in whole steps. ### InputDecimalMatrix ```python A = InputDecimalMatrix( [[1, 2], [3, 4]], num_decimal_places=1, h_buff=1.5, element_to_mobject_config={"value_range": (0, 10)}, ) cells = A.get_entries() a00 = cells[0].get_value() A.relayout() # call after changing a cell from code ``` Every cell is editable. The matrix re-lays out around its centre when a cell changes. ### InputDecimalTable ```python T = InputDecimalTable( [["x", "y"], [1, 2]], num_decimal_places=1, element_to_mobject_config={"value_range": (0, 10)}, ) v = T.get_entries_without_labels([1, 1]).get_value() T.relayout() ``` Editable table cells; labels are ordinary text. `relayout()` refreshes the grid after programmatic changes. ## 5. 3D and linear algebra ### InteractiveThreeDScene ```python class Orbit(InteractiveThreeDScene): def blueprint(self): self.set_camera_orientation(phi=70 * DEGREES, theta=-45 * DEGREES) axes = ThreeDAxes() sphere = Sphere(radius=1.3, color=BLUE, fill_opacity=0.85) slider = Slider(start=[-5, -3, 0], end=[-1.5, -3, 0], value_range=(-1.5, 1.5), init_value=0) sphere.add_reaction(lambda m: m.move_to([0, 0, slider.get_value()])) self.add_fixed_in_frame_mobjects(slider) # controls/labels stay upright self.add(axes, sphere) ``` - On an idle blueprint the reader drags empty space to orbit and scrolls to zoom; a drag that starts on a control goes to the control instead. - Put controls, labels and HUD text in `self.add_fixed_in_frame_mobjects(...)`; put scene mobjects in `self.add(...)`. - The camera is frozen while `self.play(...)` runs. ### InteractiveLinearTransformationScene ```python class Linear(InteractiveLinearTransformationScene): def blueprint(self): self.add_unit_square(animate=False) self.add_vector([2, 1, 0], color=BLUE, animate=False) handle = DraggablePoint([1, 0, 0], color=GREEN) def set_i(control, previous, next_value): point = control.get_center() matrix = self.get_matrix() matrix[0][0] = point[0] matrix[1][0] = point[1] self.set_matrix(matrix) handle.on_input(set_i) self.add(handle) ``` - `set_matrix([[a, b], [c, d]])` replaces the matrix immediately; call it from a reaction or `on_input`. - `apply_matrix(...)`, `apply_inverse(...)`, `apply_transposed_matrix(...)` compose incrementally: immediate in `blueprint`, animated in `construct`. - `add_vector(v, animate=False)`, `add_unit_square(...)`, `add_transformable_mobject(...)`, `add_moving_mobject(...)` register objects that follow the matrix. - `get_matrix()`, `refresh_transform()`. - The class adds no controls or drag handles; wire your own `DraggablePoint`, `Slider` or `InputDecimalMatrix` to `set_matrix`. ## 6. Presentation modes The mode is a **host/player option**, not a Python setting. Write the scene; the player chooses how to pace it. There is no Python API for this. - `normal` (default): blueprint pages wait for the reader; constructs play through. Good for a standalone scene. - `slides`: pause after every blueprint and construct segment. Good for courseware. - `two-view`: loop the animation; double-click the canvas to interact. Good for an animation embedded in an article or lecture note. - `animation`: loop the animation without controls. Used for GIF export. In HTML: ``; in JavaScript: `runPythonOnCanvas(code, canvas, {mode: "two-view"})`; in the share API: send `"mode": "two-view"`. ## 7. Sharing a scene through the wanim API ```bash curl -X POST https://wanim.org/api/save \ -H 'Content-Type: application/json' \ -d '{"code":"from manim import *\nclass Demo(Scene):\n def construct(self):\n self.play(Create(Circle()))","mode":"two-view"}' # {"id":"a1b2c3d4","mode":"two-view"} ``` - `mode` is optional; default `normal`. - The id is derived from the code and the non-default mode, so uploading the same snippet again returns the same id. - Open it with: - `https://wanim.org/playground?id=` — editor and preview - `https://wanim.org/playground?id=&show=1` — presentation only - `https://wanim.org/studio` — AI Studio for editing a scene; add `?id=` to load an existing snippet - `https://wanim.org/chat` — conversational assistant that can emit complete wanim blocks - `https://wanim.org/embed?id=` — bare iframe player - `GET https://wanim.org/api/get?id=` returns `{kind:"code", code, mode}`. Links created before this schema are read as `normal` snippets. ## 8. Sharing a lecture note A lecture note is Markdown with ```` ```wanim ```` fences. The website editor stores drafts in the browser and publishes a read-only KV copy: ```bash curl -X POST https://wanim.org/api/notes \ -H 'Content-Type: application/json' \ -d '{"title":"Vectors","content":"# Vectors\n\n```wanim\n...\n```"}' # {"id":"AbCd1234","editToken":"...","updatedAt":"..."} ``` - `GET https://wanim.org/api/get?id=` returns `{kind:"note", title, content, mode:"two-view", updatedAt}`. - `PATCH https://wanim.org/api/notes?id=` with `Authorization: Bearer ` and `{"title":..., "content":...}` updates the same note. - The public page is `https://wanim.org/notes?show=`. - The editor is `https://wanim.org/studio/notes`; each ```` ```wanim ```` block is previewed inline and has its own AI conversation. - `editToken` is shown once; anyone with it can update the note. - Inside a note, each ```` ```wanim ```` block is played in `two-view` mode. ## 9. Complete interactive example This is the default lecture-note template: an animation draws a circle, and double-clicking the canvas switches to a slider that changes its radius. ```python from manim import * class CircleRadius(InteractiveScene): SEPARATE = True def blueprint(self): circle = Circle(radius=1, color=BLUE, fill_opacity=0.35) slider = Slider( start=[-4, -2.5, 0], end=[4, -2.5, 0], value_range=(0.5, 3), init_value=1, ) slider.add_reaction( lambda m: circle.become( Circle(radius=slider.get_value(), color=BLUE, fill_opacity=0.35) ) ) self.add(circle, slider) def construct(self): self.play(Create(Circle(radius=1, color=BLUE, fill_opacity=0.35)), run_time=1.2) self.wait(0.8) ``` ## 10. Practical rules - Use a 16:9 safe area; keep labels and controls inside it. - `font_size` 24–48 for labels, 36–56 for titles. Use `Text` for prose, `MathTex` for formulas. - Keep reactions cheap. Do not construct `Text`/`MathTex`/`Matrix` inside a reaction; update an existing object instead. - Prefer `self.wait()` and `run_time=` over busy loops. - Use `self.theme` in `setup()` for light/dark-aware colours. - Never override `__init__` in a `Scene` / `InteractiveScene`; put setup in `setup()`, `blueprint()`, or `construct()`. - The code runs in the reader's browser. Do not put secrets in it, and only share code you trust. - If a feature is unsupported, use the closest supported manim construct and say so in the answer.