Scenes and notebook results
A scene is a Python file in the view project that defines one manimlib.Scene subclass. It reads notebook results through three functions from manim_marimo:
from manimlib import DOWN, FadeIn, Scene, Text
from manim_marimo import output, value
class Summary(Scene):
def construct(self):
mean = float(value("summary.mean", default=0))
self.play(FadeIn(Text(f"Mean {mean:.2f}")))
chart = output("distribution_figure", height=3)
if chart is not None:
self.play(FadeIn(chart.to_edge(DOWN)))
self.wait(1)| Read | Returns |
|---|---|
value(selector, *, default=None) | The value's JSON form |
output(selector, *, default=None, **image) | The value as an ImageMobject, such as a matplotlib figure |
cell(name, *, default=None, **image) | The output of the cell called name as an ImageMobject |
Studio renders the scene again whenever one of these results changes in the notebook.
Values
value() returns what marimo-export produces for the notebook variable:
| Notebook value | Scene receives |
|---|---|
| NumPy array | Nested lists of numbers |
| pandas or polars DataFrame | A list of row objects |
datetime.date | ISO 8601 text |
mo.ui element, such as a slider | Its current value through name.value |
Selectors reach into values with dots and indexes, such as summary.mean, shape.value, or points[0]. Wrap a value in np.asarray() to animate it.
Figures and cell outputs
output() reads a notebook value as an image. Studio draws a matplotlib figure as a PNG, and the scene receives an ImageMobject. Keyword arguments such as height pass through to ImageMobject. A literal height, such as output("distribution_figure", height=2.6), also sets the size Studio draws the figure at, so its tick labels read at the scene's own font sizes.
cell() reads the rendered output of a named cell. Name a cell by its function, def distribution_chart():. A cell whose last expression is a figure arrives as that figure. A cell that shows text, a table, or a layout of several outputs has no image form and returns default.
Defaults
Every read returns its default when the notebook has not computed the result, when an output or cell has no image form, and when the scene runs outside Studio, for example with manimgl scene.py. A present None value stays None.
The build runs each scene with every read at its default, skipping animation. A scene that fails there fails the build at its own line, and Studio keeps the last published view. Give each read a default the scene can render:
circle = [[[0, 0], [np.cos(t), np.sin(t)]] for t in np.linspace(0, 2 * np.pi, 61)]
chain = np.asarray(value("chain", default=circle), dtype=float)How Studio finds the reads
Studio reads the scene's source without running it and records every literal target. Write each target as a string literal. A computed target, such as value(name), reports manim-read-dynamic and always returns its default.
A scene's reads include those in project modules it imports. Shared helpers can live beside the scenes:
studio/analysis/animation/
scene.py from helpers import axes_for
helpers.py reads value("bounds")A change to bounds renders scene.py again, and leaves scenes that do not import helpers untouched.
Shapes that trace to their results
Each render records which notebook result every shape shows, so a reader can select a shape in Studio's preview and reach the cell that computed it. The tracer follows a read through the names it is bound to and the indexes the scene takes of it:
joint = np.asarray(value("joint"), dtype=float)
for i in range(6):
for j in range(6):
square = Rectangle(fill_opacity=joint[i, j]) # traces to joint[i][j]
for diagonal, probability in enumerate(probabilities):
target = bars[diagonal].copy().set_height(probability * 12)
bars[diagonal].become(target) # traces to probabilities[diagonal]Bind each read to a name, index it with names, integers, and arithmetic, and walk it with enumerate() and zip(), so each shape names the cell of the table it shows. Traced shapes lists what the tracer records.
Scenes with many shapes
A scene that moves hundreds of shapes spends its render in Python, once per frame. The epidemic example moves two hundred people through a 19-second movie at 1080p and 30 frames per second, and renders it in less time than it plays, by keeping that Python small:
- Compute every position of a frame at once with NumPy, then move each shape in one loop of
move_to()calls inside a single updater. - Change a shape's color, a ring, or a link only when its state changes, and update the few that are active.
- Set a
DecimalNumberorIntegeronly when the number it shows changes. Each new value redraws its glyphs.
Tracing records the boxes of the moving shapes at most twelve times a second, so a higher frame rate does not multiply its cost.
One scene per file
The file's scene is the one Scene subclass it defines that no other class in the file extends. A base class in the same file, or in an imported helper, stays available:
class Styled(Scene):
default_camera_config = {"background_color": "#ffffff"}
class Animation(Styled):
def construct(self): ...Put a second animation in its own file and show it with its own render host.
LaTeX
Text renders with system fonts. Tex and MathTex call a LaTeX installation on the machine that renders the scene.