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.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.1.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.1-cp39-abi3-win_amd64.whl (3.8 MB view details)

Uploaded CPython 3.9+Windows x86-64

codimate-0.1.1-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.1-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.1-cp39-abi3-macosx_11_0_arm64.whl (3.9 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

codimate-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl (3.9 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: codimate-0.1.1.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.1.tar.gz
Algorithm Hash digest
SHA256 bacc7d75c020c884b41237c649fb4ede5811bdf9f35bf4595c856b7b2f61a66b
MD5 1a14e1ec329f07683320725df5a6b74f
BLAKE2b-256 178dc6a46a9d6c9bbc002cb03190f77fcf56e1b0705d9b2d2b4cec49aa5975c0

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: codimate-0.1.1-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.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 b5851e441c26cedfe32afc91aa62dfd17e2aa9e19a8dd93da952c65cc373dead
MD5 90d2acc01c851c5ce65a3457af73c249
BLAKE2b-256 6d1b63043612d0344df713beaa46c82c257d9734bcc6140e898e9582481fe318

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for codimate-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 9f326e3607ebffecaf522ae4e31451c7c99567a69251d054ac91c6f04a32f13c
MD5 4db6e97b9173e9949161e53e934eea7e
BLAKE2b-256 8e2ace28f0396ac99ac88c787115f7e9085e5f440a23cb3842f8dd871bd5918a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for codimate-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 0b0ba3207a750b2fdf7e6a45e4fc5f22d03a158ccdc823c4f4ca69eb5db4f27c
MD5 4edd879cc56aca976171297b41fe1f5c
BLAKE2b-256 025ca61a454058c0ed3c329a543063d99d8c5e74136dfe1e165ee16e9488d258

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for codimate-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7fa9b45c0a1f2096b01984cc29426331aaab42aee573eb324a9eabe2c393ec59
MD5 ede0acf8fdc4ee1d68e1673af20f5750
BLAKE2b-256 1c6c57eb04161e67850848589035e875483edefcea6537bdc919676145dee0c2

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for codimate-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 219efef7694437632d739dfddf48003b1f854e147bfd74efa4ec2d392fdb7a34
MD5 33d1b5512873abdb0df1e5a63857f2cd
BLAKE2b-256 be4ac8a5d5bc486b92f8fd016fb9d75b69bfc810b59d5bc7d3ae587c8d5b3d20

See more details on using hashes here.

Provenance

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

0.1.2

6 files

This release

0.1.1 This release

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