Conversation
Fifteen shapes, nine primitives and six named objects, and a rule that a sixteenth needs an amendment to VISUAL-SYSTEM.md before it is drawn. The rule is enforced rather than remembered: the shape list is code, a storyboard that names something outside it fails, and a shape that is drawable but undescribed fails too. Importing xraymanim deliberately does not import manim. The grammar, the storyboards, the catalogue and the checks are all manim free, so every CI job runs them, and the drawing half sits behind the anim extra where only the one job that renders pays for thirty packages and a Cairo build. Each animation is planned as a storyboard first, which is data and checks in milliseconds: the ninety second cap, the caption track, the shapes it draws. A scene then has to play the beats it planned, checked by a tally in tear_down, so a scene cannot quietly drift from its plan. That matters because a render takes a minute and a mistake in the plan costs several. a01 walks answer = 6 * 7 through the eight artifacts of the pipeline, with the tokens, the tree, the symbol table and both instruction listings taken from a real 3.15.0rc1 run rather than typed from memory. a02 is a list with two names pointing at it and a refcount going one, two, one, zero. The GIFs are committed because a reader on GitHub or in Colab cannot run a build first, and they go through ffmpeg with a generated palette, which is several times smaller than what manim writes. CI re-renders instead of comparing bytes, because video encoders are not reproducible across platforms and a checksum would fail for reasons nobody can fix. Closes #15
There are no manimpango or pycairo wheels for 3.15 yet, so both build from source on the runner and need the headers. Locally they are already there, courtesy of Homebrew, which is why this only turned up in CI.
ubuntu-latest does not have ffmpeg any more, and the step meant to catch that was written as `ffmpeg -version | head -n 1`. Under set -e a pipeline's exit status is the last command's, so head succeeded and the missing ffmpeg went unnoticed until the render failed two minutes later.
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #15.
Some things about CPython are hard to teach in a still picture, because the whole point of them is that something changes. A refcount going up and down, an instruction pointer walking along a strip of bytecode, a value moving on and off the stack. This adds the library those get made with, and the first two animations built from it.
The fifteen shapes
Nine primitives (
box,arrow,slots,column,stream,tree,graph,counter,highlight) and six named objects (PyObjectBox,RefArrow,Frame,CodeStrip,DictTable,ArenaMap). The primitives are shapes. The named ones are nouns: aPyObjectBoxis not a box with a label on it, it is the two fields every object in CPython actually starts with, drawn the same way every time so that a reader stops decoding the picture and starts looking at what changed in it.The issue asked for a documented rule that a new shape needs an amendment to the visual system document before it is drawn. Written as prose that would be a rule somebody has to remember, so it is code instead.
grammar.SHAPESis the list, a storyboard naming a shape outside it fails its own check, and a shape that is drawable but not described inxraymanim/VISUAL-SYSTEM.mdfails too. The three cannot drift apart.Colours come from
pyxray.theme, the same six tones the diagrams and the charts already use, so an animation and the diagram next to it in a lesson are recognisably from one project. There is a test that a tone renamed in pyxray shows up here rather than being copied and frozen.Importing it does not import manim
manim is about thirty packages and a Cairo build. Every CI job would otherwise pay for that, so the package is split down the middle. The grammar, the storyboards, the catalogue and the checks are all manim free and run everywhere. The drawing half is behind
[project.optional-dependencies] render, reached through the rootanimextra, and only the newanimationsjob installs it.Storyboards, because a render costs a minute
Every animation is planned as data before it is drawn. A
Storyboardholds the slug, the lesson it belongs to, the shapes it will use and the caption track as a list of beats with durations, and it checks itself in milliseconds: under the ninety second cap, no caption that wraps onto the picture, no line break inside a caption, at least three beats because two is a slide and a slide should be a still image.The scene then has to play the beats it planned.
Explainer.tear_downcounts them and fails the render if the tally is wrong, so a scene cannot quietly diverge from its plan. That is worth having precisely because inspecting an animation is expensive: a render is a minute, so a mistake in the plan costs several before you see it.xraymanim checkalso confirms the scene file exists and defines the class the slug implies, the GIF is committed and under 6 MB, and every animation is listed inanim/README.md. It is injust checkand every CI job runs it.The two animations
a01, one line of Python, seven stages.
answer = 6 * 7walked through the eight artifacts of the pipeline, ending with the multiplication disappearing between codegen and the optimizer. Every artifact on screen came from a real run ofpyxray.compiler.stageson 3.15.0rc1: the tokens are whattokenizereturns, and both instruction listings are what CPython emits. A reader watching an animation cannot pause and check the bytecode, so nothing in it is typed from memory.a02, a name is a label, not a box.
a = [],b = a,del a, with the refcount on the object going one, two, one, zero and the memory going back at the end. The count is drawn inside the object rather than beside it, because it is a field of the object, and drawing it outside is how a reader ends up believing the interpreter keeps counts in a table somewhere.Why the GIFs are committed
A reader on GitHub or in Colab is looking at a page and cannot run a build first. They go through ffmpeg with a generated palette rather than straight out of manim, which is several times smaller for the same picture and is the difference between this repository staying clonable and not. 2.3 MB and 856 KB.
They are deliberately not compared byte for byte, unlike the notebooks and the diagrams. Video encoders are not reproducible across versions or platforms, so a checksum here would fail for reasons nobody can fix. CI re-renders instead, which catches the failure that actually happens: a scene that raises because the library moved underneath it.
Also in here
just animationsandjust build-animations, with the first one added tojust check. A newanimationsCI job that installs the extra, runs the tests and re-renders both. Axraymanimrow in the README table and a section on the animations. 45 new tests, and the suite goes from 915 to 960.One thing worth flagging: manim's text rendering leaks an unclosed
xml.etree.iterparseiterator, so theResourceWarningarrives from a destructor at an arbitrary later moment. Withfilterwarnings = ["error"]that does not fail the test that caused it, it fails whichever test happened to be running when the collector got round to it. There is a targeted ignore for it with the reason written down. The tests also point manim's media cache at a temporary directory, so a test run leaves nothing in the working tree.Checked
just checkis green: ruff, 960 passed and 1 skipped, 352 citations, 2 blueprints, 96 diagrams, 12 notebooks executed end to end, and 2 animations. Both animations were re-rendered from a clean checkout of this branch.