Skip to main content

Codimate

PyPI Python

Turn a running algorithm into an explainer video.

You write your algorithm as normal Python and say what each step looks like. Codimate works out the motion, the timing, and every frame.

import codimate as cm

@cm.trace()
def bubble_sort(values):
    for i in range(len(values)):
        for j in range(len(values) - 1 - i):
            cm.emit("compare", items=[values[j], values[j + 1]])
            if values[j] > values[j + 1]:
                values[j], values[j + 1] = values[j + 1], values[j]
                cm.emit("swap", items=[values[j], values[j + 1]])

def bars(frame):
    scene = cm.Scene()
    active = frame.items()

    for slot, item in cm.row(frame.state, gap=40):
        bar = scene.group(item.id, slot)
        bar.rect("bar", h=item.value * 70, at=cm.at(bottom=0)) \
           .fill("orange" if item in active else "blue")
        bar.text("label", item.value, size=32, at=cm.at(top=20))

    return scene

cm.explain(
    trace=bubble_sort(cm.items([3, 1, 4, 2])),
    view=bars,
    motion=[cm.Rule("*", position="lift_carry_drop", clearance=90)],
    timing=cm.Timing(default=0.55, events={"swap": 0.9}),
).render("results/bubble_sort.mp4")

That is the whole program. Run it, get results/bubble_sort.mp4.

Install

pip install codimate

Codimate is a Rust engine with a Python front end, but the wheels are prebuilt, so there is no Rust toolchain to install. One wheel covers every Python from 3.9 up, on Linux, macOS and Windows.

Working on the engine

Only if you are changing the Rust, rather than using Codimate:

git clone https://github.com/darhnoel/codimate && cd codimate
python3 -m venv .venv && .venv/bin/pip install maturin
.venv/bin/maturin develop --release
.venv/bin/python python/examples/bubble_sort/main.py

Use --release. Without it the Rust engine is unoptimized and renders roughly 17x slower — a scene that draws in 6ms takes 100ms. Only leave it off if you are debugging the engine itself.

What else you need

ffmpeg does the video encoding. Codimate uses the one on your PATH if you have it (brew install ffmpeg, apt install ffmpeg) and otherwise falls back to the copy that comes with imageio-ffmpeg, which pip installs for you. Point CODIMATE_FFMPEG at a binary to override both.

typst is needed only if you use scene.formula to typeset LaTeX maths (brew install typst). Everything else renders without it.

The four pieces

algorithm   your normal code, with emit() where something happens
view        what one moment looks like
motion      how things travel between moments
timing      how long each moment lasts

algorithm — write it the way you normally would. Call emit() after you change your data; Codimate snapshots the result for you.

view — a function from one moment to a picture. It receives frame.state (your data at that moment) and frame.event (what just happened). It runs once per event, not once per frame, so it can be as slow as you like.

motion — patterns matched against shape names. First match wins, and a straight line is the default, so most explanations need no rules at all.

motion=[cm.Rule("*", position="lift_carry_drop", clearance=90)]

A shape inside a group is named group/child, so "3/*" targets one group. A rule cannot make something move that did not move.

path
straight a straight line, easing in and out — the default
linear a straight line at constant speed, for things mid-journey at every event
lift_carry_drop arcs up and over, then falls; takes clearance

Use linear when something turns or orbits: easing would make it accelerate and stop inside every segment.

Every path eases in and out. cm.ease(t) calls into the Engine if you need the curve itself — to draw it, or to pace something by hand — so you never keep a second copy that can drift.

timing — seconds per event, by event name.

cm.Timing(default=0.55, events={"swap": 0.9}, opening=0.8, final_hold=1.2)

The one thing to understand

Codimate has no idea what a "swap" looks like. It only knows this thing was here, and now it is there. The name you give each shape is how you tell it what moved.

scene.group(item.id, slot)    # the name follows the THING
scene.group(position, slot)   # the name follows the PLACE

Same algorithm, same data, completely different video:

what you see
name follows the thing bar "3" is in slot 0, then slot 1 — it slides across
name follows the place slot 0 stays put and changes height — bars morph in place

Both are useful. Sorting wants the first. A grid, a heatmap or a matrix multiply wants the second — cells don't travel, they light up:

scene.group(("cell", row, col), slot)

Codimate cannot tell you which you meant, so this is the one decision worth thinking about.

cm.items() is how a plain value gets a name of its own. Two 3s in a list are two different bars, and only an identity can say so:

values = cm.items([3, 1, 4, 2])   # each gets an .id and a .value

Items compare by value, so your algorithm stays ordinary Python (values[j] > values[j + 1]), and they stay themselves across every moment of the trace. You only need them for things that move — a grid keyed by position doesn't.

What you can draw

scene.rect(name, h=, w=, at=)
scene.circle(name, r=, at=)
scene.text(name, content, size=, at=)
scene.polygon(name, points)                             # cm.ngon, cm.star
scene.curve(name, points, w=)                           # smooth, through the points
scene.svg(name, "logo.svg", size=)                       # vector art, as real geometry
scene.line(name, start=slot_or_point, end=slot_or_point, w=)
scene.formula(name, r"\frac{a}{b}", size=, at=)         # LaTeX, needs `typst`
cm.measure(text, size) -> (w, h)                        # to size a box around text
scene.group(name, slot)      # a place to draw a thing made of several shapes

Each takes only what decides what the shape is. Everything else — colour, outline, rotation, layer, opacity — is said afterwards on the handle it hands back, so no call grows past five arguments:

scene.circle("bob", r=28, at=(x, y)).fill("orange").on(layer=4)
scene.rect("card", h=120, w=200).fill("#243046", edge="grey").round(8)
scene.polygon("tri", cm.ngon(3, r=60)).grow(1.8).turn(12)

Place things by whichever edge you actually mean. at= takes a point, a Slot, or cm.at(...) when an edge is what you mean:

scene.rect("bar", h=200, w=90, at=(640, 460))               # a point
scene.rect("bar", h=200, w=90, at=cm.at(x=640, bottom=560)) # sits on a line
scene.text("label", 3, size=32, at=cm.at(x=640, top=580))   # under something

No baselines, no y - h/2. Give both y and top and you get an error, not a silent winner.

Colors are names (white, black, red, orange, blue, green, grey, yellow, cyan) or #rrggbb. Higher layer draws on top.

Shapes that appear or disappear between moments fade, and shapes that change size, colour or position tween. You do not ask for any of that.

Where things sit

You should not be inventing layout arithmetic. cm.row() and cm.column() divide the canvas and hand back one slot per item; scene.group() puts a thing in one:

for slot, item in cm.row(frame.state, gap=40):
    bar = scene.group(item.id, slot)
    bar.rect("bar", h=item.value * 70, at=cm.at(bottom=0))
    bar.text("label", item.value, at=cm.at(top=20))

Inside a group, 0 is the group's own point. cm.at(bottom=0) stands the bar on it; cm.at(top=20) puts the label 20 below it. w defaults to the group's width and x to its centre, so the only thing left to say is the one thing a bar and a label disagree about.

A name can be built from other names. Nested keys flatten, so you never concatenate tuples by hand — and a line takes Slots directly rather than making you pull .x and .y out of them:

scene.line(("edge", src, dst), start=at[src], end=at[dst])   # -> "edge/0/1/1/2"

Everything on a group moves as one thing. The bar and its label cannot come apart, because the Engine sees them as 3/bar and 3/label — one name, two shapes. Groups nest, so a thing made of a thing made of a thing still travels as a unit.

A slot is a place, not a shape — nothing draws it. Hand one to group() and you never take it apart; read slot.x, slot.bottom, slot.left, slot.top, slot.right, slot.w, slot.h for the odd case that needs it.

cm.column() stacks instead of spreading — layers of a network, levels of a tree — and its slots anchor at their centre rather than a baseline:

for slot in cm.column(4, gap=44, at=cm.at(x=640)):
    scene.group(("neuron", 1, i), slot).circle("body", r=30)

Widths, spacing and the baseline come from the canvas unless you override them (size=, at=, within=). The canvas is 1280x720 by default:

cm.canvas(1920, 1080)     # everything below follows
cm.width(), cm.height()   # for the odd thing you place by hand

Output resolution is separate from your coordinates. scale only changes how many pixels each coordinate becomes, so nothing in your view has to move:

.render("out.mp4", fps=60, scale=1.5)   # 1080p60 from the default canvas

Frames are rasterized at the larger size rather than upscaled afterwards, so 1080p is genuinely drawn at 1080p.

How it works

Python (once per event)          Rust (once per frame)
──────────────────────           ─────────────────────
algorithm + emit()
view → a Scene per event
motion rules, durations
        │
        └─ hands over ONCE ───▶  pair shapes by item
                                 build tweens
                                 rasterize
                                 pipe to ffmpeg
                                        │
                                        ▼
                                    out.mp4

One handover, not one per frame. A frame is never played forward — it is computed from scratch at time t, so any moment can be rendered in any order, and twice the same way.

Under the hood

The Rust crates are the Engine. You do not need to read them to use Codimate, and the Rust API is not a second way to author explanations — see ADR 0008.

crates/codimate-core/      pure animation model — f(t) → Scene
crates/codimate-animation/ duration and composition
crates/codimate-render/    tiny-skia CPU raster
crates/codimate-export/    raw RGBA → ffmpeg
crates/codimate-glyph/     text → glyph outlines
crates/codimate-py/        the bindings — the diff lives here
python/codimate/           the Python package

More

All of this is also a site — https://darhnoel.github.io/codimate/ — with the API reference generated from the docstrings alongside it. Build it locally with python docs/build_site.py.

The guide, in order. Four chapters; read them front to back the first time.

  1. Writing Your First Animation — build one from an empty file, meeting all four pieces on the way.
  2. What You Have to Work With — the shapes, what they are enough for, and a car built out of them.
  3. How Codimate Thinks — why motion is derived rather than authored, and the one decision you have to make.
  4. Reference — every call and parameter, on one page.

Then python/examples/, eight worked examples with notes, and the decisions behind the design.

cargo test                                  # the Engine
.venv/bin/python python/tests/run.py             # the Authoring Surface

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

codimate-0.1.2.tar.gz (2.5 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

codimate-0.1.2-cp39-abi3-win_amd64.whl (3.8 MB view details)

Uploaded CPython 3.9+Windows x86-64

codimate-0.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.1 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

codimate-0.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

codimate-0.1.2-cp39-abi3-macosx_11_0_arm64.whl (3.9 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

codimate-0.1.2-cp39-abi3-macosx_10_12_x86_64.whl (4.0 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file codimate-0.1.2.tar.gz.

File metadata

  • Download URL: codimate-0.1.2.tar.gz
  • Upload date:
  • Size: 2.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for codimate-0.1.2.tar.gz
Algorithm Hash digest
SHA256 05ae7f259b468c2828893f2fa8b33d36498be001b1dbf887725af8a663572883
MD5 380f48ee93e0cf20b2af7863da24f350
BLAKE2b-256 7509f2b2e6d80e294d57d052fe40f752d1dc21e7e3fbaa5e608ddcd564a510ae

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.2.tar.gz:

Publisher: release.yml on darhnoel/codimate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codimate-0.1.2-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: codimate-0.1.2-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.8 MB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for codimate-0.1.2-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 4dcab175ab8be2ce2d5db6b2a004978f5cdf9daf373a1f0005b8107934130328
MD5 cc63fb0b0aed2be444e8dc6ba43e7a4f
BLAKE2b-256 500786aaf0f8d289780be273ec071114f6a434bdd11378d1b5945201bc793a28

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.2-cp39-abi3-win_amd64.whl:

Publisher: release.yml on darhnoel/codimate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codimate-0.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for codimate-0.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 69e1818a869bc967eff874ce824237dee134b1947e10aef86ba30682aaf55a5f
MD5 377783b66ac992c53ec12cf1da6fb402
BLAKE2b-256 ee95d6b27559609404244955c5ad9cb8f57ac6c8147a0959bb2d026541192ff4

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on darhnoel/codimate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codimate-0.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for codimate-0.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 f69f98bb4f79eb1f2441a01c58e8132e17bcef81ce96ceb030a0dfdd0b5b05c8
MD5 51f8d2692c4554e74570e24c49ac3d1e
BLAKE2b-256 fa15bde71c2a24d274a65d8466840fdcf8cafaa2e4dce110dff443c16cd1252b

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on darhnoel/codimate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codimate-0.1.2-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for codimate-0.1.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 cf51eea440987013f1cbe5c33c70a4569f8797f11bff7dd2507ab1b292dfdb1d
MD5 39a754ffc05675b4889f1f10555b4560
BLAKE2b-256 95739553b28650411794bd53ea634280e5931142cd35abd12facce622ea23119

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.2-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on darhnoel/codimate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codimate-0.1.2-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for codimate-0.1.2-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 69f0d84f6fe9f476858c64525956ba3dabbc926f0ac4c363e0104617e0fdc1fa
MD5 f1f0dd7366f8d8019c0a54f1614b58df
BLAKE2b-256 78489ce22dc00781aadb1b7096fba1459f898139aec78eaf0359fb225503bce6

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.2-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on darhnoel/codimate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.4

6 files

0.1.3

6 files

This release

0.1.2 This release

6 files

0.1.1

6 files

0.1.0

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page