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.arc(name, r=, sweep=(0, 120))                     # an arc, a dial, a pie slice
scene.svg(name, "logo.svg", size=)                       # vector art, as real geometry
scene.image(name, "photo.jpg", size=)                    # a picture: PNG or JPEG
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.4.tar.gz (2.6 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.4-cp39-abi3-win_amd64.whl (3.9 MB view details)

Uploaded CPython 3.9+Windows x86-64

codimate-0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.2 MB view details)

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

codimate-0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.2 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

codimate-0.1.4-cp39-abi3-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

codimate-0.1.4-cp39-abi3-macosx_10_12_x86_64.whl (4.1 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: codimate-0.1.4.tar.gz
  • Upload date:
  • Size: 2.6 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.4.tar.gz
Algorithm Hash digest
SHA256 af4b4d03379408eb1850fd9d8d43b592f57ba53ae7561a39c7d121573dc74005
MD5 9662524ac8766bac535072530ed61f37
BLAKE2b-256 0a9181c592cf40f715bc6da522a78affa7c89f1a80eadaa47e64fae41a5b125f

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.4.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.4-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: codimate-0.1.4-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.9 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.4-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 5ea5dd38b64ed416f9c78b399659ab22e227f84d4e6fa98ab7911010eeaa90ad
MD5 0f9b962ea42d9e4066ee8c2475fb0176
BLAKE2b-256 52a2fc57cbc2f34b079bf078b69e11a0c7343a13bcd5ddd9ccae5211a349311c

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.4-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.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for codimate-0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ced7402ed0f5b39aa2281745b9e41385a6a2e98e2b9b9aa4f3f7ebdbebf07a57
MD5 dca74d1ed606f2e6149a0efd7902140e
BLAKE2b-256 c8a3d21aed7b8b5765732811572465ddca3f8c0da293d40a2257ffa66daca68a

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.4-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.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for codimate-0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 f880f99c8b986adbb96840f78f02cc6d5b223650350337d972758ceb7f2bf639
MD5 8556ef6d3171ef6769aa2040388412ed
BLAKE2b-256 8f9379272d8963e1e3fbfe706176a87b39d6af155f94bc261c72d9c5e26dc66b

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.4-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.4-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for codimate-0.1.4-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c69c49c9300161d93fa6a1e05006e10317caf5b60624d488e9f39159f8f21877
MD5 fb9e7473e717e5b5fcbeb52d6cf4dc78
BLAKE2b-256 5a076df1b5e77c1992489276ac7f0e93dbfe324bb19387ddcd214739fcfdfdf9

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.4-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.4-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for codimate-0.1.4-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 2fcb1ee16955aa69803f5ef3e85fe62332d20304c2af7be13d326a57b9a556e5
MD5 0e8f63d1c2f64a168703e2c9834dcfee
BLAKE2b-256 030d28f6b52f033bf4b922dca0a447296e315a7854ee97d6dbb08ade496c3d3f

See more details on using hashes here.

Provenance

The following attestation bundles were made for codimate-0.1.4-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

This release

0.1.4 This release

6 files

0.1.3

6 files

0.1.2

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