A playful Python toolkit for creative coding and small games.
Project description
Gummy Snake
Gummy Snake is a playful Python toolkit for creative coding and small games. It is for people who want to sketch with code: draw shapes, animate motion, react to input, load sprites, play with pixels, and build visual toys without first building a full app.
The public API is Python-first. Function names use snake_case, sketches are
ordinary Python files, and drawing, export, pixels, text, images, native
interactive windows, and ECS storage/execution are powered by packaged Rust
runtime components. On desktop builds, native windows and input use the
SDL3-backed runtime. The Rust canvas owns the hot renderer state used to
construct draw commands, including the current style, transform stack,
image/text state, and GPU command batches. It also owns the mutable sketch
context state for canvas lifecycle fields, timing, loop/redraw flags, input
snapshots, and in-progress shape buffers. The Rust ECS owns entity/component,
tag, resource, event, query, schedule, spatial-index, and compiled physical-plan
state; Python constructs typed logical plans and keeps the public sketch API
friendly.
Install
pip install gummy-snake
Published wheels include the required Rust gummy_canvas canvas runtime, which
also exposes the packaged Rust ECS bridge. Source or editable installs must build
that PyO3 module; there is no Python renderer or Python ECS execution fallback
for runtime-owned behavior. Local development builds compile SDL3 from
source/static through Rust when native interactive support is enabled, so no
separate system SDL3 install is normally required.
Install optional media helpers when you need camera or video frame sources:
pip install "gummy-snake[media]"
First Sketch
Create a file named circle_sketch.py:
import gummysnake as gs
@gs.setup
def setup() -> None:
gs.create_canvas(400, 300)
gs.no_stroke()
@gs.draw
def draw() -> None:
gs.background(245)
gs.fill(255, 90, 90)
gs.circle(200, 150, 100)
gs.run()
Run it:
python circle_sketch.py
For repeatable scripts, use a bounded headless render:
gs.run(headless=True, max_frames=1)
Callbacks can also be async def, which is useful with async-compatible asset
helpers:
image = None
@gs.preload
async def preload() -> None:
global image
image = await gs.load_image_async("sprite.png")
What You Can Make
- 2D drawings with shapes, curves, color, transforms, and blend modes.
- Animated sketches using the familiar
setup()anddraw()lifecycle. - Decorator-based sketches, async-compatible callbacks, and object-oriented
Sketchsubclasses. - Image and pixel experiments, including canvas export.
- Text, font measurement, and accessibility descriptions.
- Interactive sketches with SDL3-backed native windows, mouse, keyboard, and touch state when native window support is available.
- WEBGL-style 3D sketches with primitives, lights, materials, models, textures, and shader objects. Built-in model and primitive draws use retained Rust/GPU buffers, GPU transforms/projection/depth, and built-in material shaders when GPU drawing is available.
- Data-oriented simulations and games with the Rust-accelerated ECS: dataclass
components/resources, typed tags/events, decorated systems, deterministic
system ordering, grouped joins, spatial relations, and Rust physical execution
before every
draw()call. - Dense 2D scenes that rely on internal mixed primitive, line, sprite, and text batching rather than one Python-to-Rust call per draw.
- Small games and visual toys using the examples as starting points.
Loaded images, models/meshes, and sounds keep Rust-managed asset handles behind
friendly Python wrappers. This is intentional for performance: bulk asset bytes,
geometry arrays, parsing, export, and metadata extraction should stay in the
Rust canvas runtime so sketches avoid repeated Python object materialization and
per-element loops. Normal load_image(); image(...) sprite drawing can stay on
the fast renderer path, model export can use Rust-owned geometry without first
creating Python Vec3 objects, and built-in WEBGL model draws can reuse
retained GPU vertex/index buffers while the GPU handles transform, projection,
depth testing, texture sampling, and material lighting. Loaded sounds keep their
bytes and duration metadata in CanvasSound until user code asks for Python
bytes.
Image-local resize, mask, filter, crop/copy, and alpha compositing delegate
bulk byte work to the Rust canvas runtime while keeping the Python Image
API and version semantics.
ECS component and resource schemas are declared with Python dataclasses. Default
Python field types map to Rust storage columns, and typing.Annotated with
gummysnake.ecs.types lets sketches request narrower or vector/list storage
such as UInt16, Float32, Vec3F32, or List(Float64). Registered systems
are ordinary decorated Python functions that return lazy ecs.Action trees; the
plan is serialized to Rust, optimized into a physical plan, cached, and executed
against Rust-owned columns. Python UDFs marked with @ecs.udf are the explicit
escape hatch for side effects or external APIs and are the only ECS plan nodes
that execute Python at runtime.
For pixel effects, load_pixels() returns gummysnake.core.pixels.PixelBuffer,
a mutable list-like RGBA byte buffer that tracks dirty regions;
load_pixel_bytes() provides a bytes readback path; and update_pixels()
accepts lists, PixelBuffer, and buffer-like inputs such as bytes,
bytearray, and memoryview. Buffer-like uploads use the Rust canvas
buffer-protocol path without an intermediate Python bytes(...) copy, exact
no-op byte uploads are skipped, and dirty row-aligned PixelBuffer changes can
upload as smaller Rust regions. Small canvas get() and set() region
operations use Rust region calls instead of reconstructing the full canvas as a
Python image.
For dense drawing loops, gs.fast() returns a frame-local facade that keeps
public style/transform state while reducing global-mode dispatch overhead.
Fill-only rectangles, triangles, circles, axis-aligned ellipses, compatible line
runs, and mixed stroked/fill primitive groups can batch into compact Rust
commands with per-record style and transform data. Supported primitive batches
use procedural GPU instance paths where possible; static unchanged command
streams can be retained and reused; unsupported transforms fall back to the
general vertex path without changing public API behavior. Sprite-heavy loops can
batch through the Rust image path with per-sprite transforms, source rectangles,
tint, sampling, and blend state, including an internal atlas path for ordered
draws from a small texture set.
Text-heavy overlays can use text_batch() and text_widths() to submit many
labels or measurements with fewer Python calls while staying on the Rust-owned
text path. The renderer keeps text_width(), ascent/descent, and
text_bounds() consistent with the current style used for drawing, and it mixes
GPU glyph-atlas text with batched cached line-texture atlas fallback internally
when that is needed to preserve ordered output around intervening shapes or
images.
ECS runs after frame timing/input state is updated and before plugin
before_draw hooks and user draw(). Use gs.add_system(system, order=...),
before=[...], after=[...], or named system sets to make execution order
explicit. do_in_order() observes serial read-after-write order;
do_in_parallel() is for independent snapshot-style actions. Strict mode
(gs.configure_ecs(strict=True)) rejects ambiguous duplicate writes, while
non-strict mode remains deterministic with last-write-wins warnings that can be
suppressed via warn_on_ambiguity=False.
Opt-in enable_performance_diagnostics() counters can identify readback, pixel
conversion, upload, direct model/shape draw, primitive/image batch size and
flush shape, GPU vertex-buffer, texture cache, GPU blend/region-effect passes,
glyphon-backed text drawing, cached-text atlas fallback, and CPU compositing
fallback paths. Use gs.ecs_diagnostics() for ECS counters such as physical
plan compiles/runs, rows scanned, fields written, UDF calls, ambiguity warnings,
event queues, and spatial index candidate/exact rows.
HiDPI/Retina rendering keeps sketch coordinates logical while physical pixel
buffers and GPU vertices are scaled by pixel_density().
Learn More
- Getting started
- Examples
- API reference, including the ECS reference
- Contributor docs, including the ECS architecture
For Contributors
This repository uses uv for Python commands:
uv sync --dev
uv run ruff check .
uv run mypy src
uv run pytest
uv run python scripts/source_size_audit.py
uv run python scripts/structure_audit.py
The canvas runtime is a required PyO3 module for development/source installs and also exposes the Rust ECS bridge:
uvx maturin develop --manifest-path crates/gummy_canvas/Cargo.toml --features extension-module
Use --release for benchmark/performance comparisons; a debug extension build
can make ECS and rendering benchmarks look dramatically slower.
The refactored Python package is split by responsibility: user-facing wrapper
functions live in topic modules under src/gummysnake/api/ (for example
lifecycle.py, input.py, images.py, pixels.py, text.py, models.py,
shaders.py, sound.py, media.py, and three_d.py), SketchContext mixins
live in src/gummysnake/context_mixins/, lifecycle code and object-mode facade
groups live in src/gummysnake/sketch/, enum-backed constants live in
src/gummysnake/constants/, and thin canvas backend/renderer facades delegate to
src/gummysnake/backend/canvas_runtime/host/ and
src/gummysnake/backend/canvas_runtime/renderer/. The ECS public API and logical
plan builders live in src/gummysnake/ecs/, while canonical storage and physical
execution live in crates/gummy_ecs and are exposed through the gummy_canvas
PyO3 module. The renderer internals are grouped around bridge calls, lifecycle,
counters, caches, payload builders, and primitive/image/text/pixel drawing.
Shared test fakes live in tests/helpers/, fixtures live in tests/fixtures/,
and generated example output stays under the
gitignored examples/output/ tree. The native desktop runtime itself lives in
crates/gummy_canvas, owns sketch context state, canvas draw state, command
construction, cache/dirty-state helpers, and GPU render-pass batching, and uses
SDL3 for windowing, resizing, and input event collection. Python keeps the
public API, callbacks, plugin hooks, and friendly wrapper objects.
The contributor documentation explains the architecture, lifecycle, testing workflow, and release shape in more detail:
- Contributor guide
- Architecture
- Backend and renderer boundaries
- Runtime model
- Runtime diagnostics
- ECS architecture
- ECS debugging and performance triage
- Build capabilities
- API performance policy
- Text renderer decision
- Testing and CI
Performance benchmarks are opt-in:
uv run pytest tests/benchmark/test_canvas_backend_perf.py --run-benchmarks
uv run pytest tests/benchmark/test_api_overhead_perf.py --run-benchmarks
uv run pytest tests/benchmark/test_image_pipeline_perf.py --run-benchmarks
uv run pytest tests/benchmark/test_model_export_perf.py --run-benchmarks
uv run pytest tests/benchmark/test_webgl_3d_perf.py --run-benchmarks
uv run pytest tests/benchmark/test_ecs_perf.py --run-benchmarks
uv run pytest tests/benchmark/test_ecs_spatial_perf.py --run-benchmarks
Canvas backend benchmark scenarios measure native interactive presentation and
are expected to average at least 240 FPS. Headless/offscreen numbers are useful
for export diagnostics, but they are not the runtime performance acceptance
metric. The canvas benchmark payload includes renderer metrics for draw counts,
primitive/image batch records, flushes, largest coalesced batch size, vertex
uploads, texture uploads/reuse, text cache hits, pixel readbacks/uploads, GPU
region effects, and presented/rendered frame counts. WEBGL frame-style benchmark
scenarios use the same FPS floor.
High-count primitive and sprite stress variants keep explicit 60 FPS gates for
10k stress scenes, and the high-count primitive gate covers 10k, 50k, and 100k
static retained-batch scenes behind --run-high-count-benchmarks.
Model export benchmarks use a memory budget for streaming OBJ/STL output.
Machine-specific baseline snapshots live in tests/benchmark/baselines/; keep
captured values as measured and note both the required 240 FPS floor and the
higher recovered-variant margin target when applicable.
Long-running resource lifecycle checks are also opt-in:
uv run pytest tests/stress --run-stress -q -s
Compatibility
Gummy Snake keeps the sketch lifecycle familiar, but it is not a browser port. It does not include DOM helpers, browser-only APIs, JavaScript aliases, or a Pillow/Pyglet/Python renderer fallback. Unsupported features raise explicit package errors so sketches fail clearly.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gummy_snake-0.8.0.tar.gz.
File metadata
- Download URL: gummy_snake-0.8.0.tar.gz
- Upload date:
- Size: 425.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
465febbae43625a92ed6ffef891fb8d16b6afab0c74cf9035c1288f3db0fe6f2
|
|
| MD5 |
7108c4ce016d4aab086edff2d3279aca
|
|
| BLAKE2b-256 |
8a32d259b12582a72a2d74f031c08544a51378e98de213c99ba6ee64ffc9f6a7
|
Provenance
The following attestation bundles were made for gummy_snake-0.8.0.tar.gz:
Publisher:
publish.yml on JonathanHHenson/gummy_snake
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gummy_snake-0.8.0.tar.gz -
Subject digest:
465febbae43625a92ed6ffef891fb8d16b6afab0c74cf9035c1288f3db0fe6f2 - Sigstore transparency entry: 2047334918
- Sigstore integration time:
-
Permalink:
JonathanHHenson/gummy_snake@ddd62bd57936e8a2c8027956399009cbee2da491 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/JonathanHHenson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ddd62bd57936e8a2c8027956399009cbee2da491 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gummy_snake-0.8.0-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: gummy_snake-0.8.0-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 5.9 MB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1843a02437cfe4f4b47588b7d53b88dea3c39e52da0e7a93664e907601931a11
|
|
| MD5 |
69228f2a0094e6b35157d0e999340c40
|
|
| BLAKE2b-256 |
dba863c69b01d8d88dde84d656b1a29eaf8e22c9120a09348ec4f01a69ff364b
|
Provenance
The following attestation bundles were made for gummy_snake-0.8.0-cp312-cp312-win_amd64.whl:
Publisher:
publish.yml on JonathanHHenson/gummy_snake
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gummy_snake-0.8.0-cp312-cp312-win_amd64.whl -
Subject digest:
1843a02437cfe4f4b47588b7d53b88dea3c39e52da0e7a93664e907601931a11 - Sigstore transparency entry: 2047334954
- Sigstore integration time:
-
Permalink:
JonathanHHenson/gummy_snake@ddd62bd57936e8a2c8027956399009cbee2da491 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/JonathanHHenson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ddd62bd57936e8a2c8027956399009cbee2da491 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gummy_snake-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: gummy_snake-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 6.5 MB
- Tags: CPython 3.12, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e0159f96863347c95fce4153623b6053acffa8c5ad55ed672bcfcf7baf12b6e
|
|
| MD5 |
6ba68ef8d06e447d0d48a7b7ae66f0df
|
|
| BLAKE2b-256 |
dd295f6426fbb1b53d65093f94278cfdaff480c73dfa5f5dd2cd236909eb1eaf
|
Provenance
The following attestation bundles were made for gummy_snake-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish.yml on JonathanHHenson/gummy_snake
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gummy_snake-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
6e0159f96863347c95fce4153623b6053acffa8c5ad55ed672bcfcf7baf12b6e - Sigstore transparency entry: 2047335082
- Sigstore integration time:
-
Permalink:
JonathanHHenson/gummy_snake@ddd62bd57936e8a2c8027956399009cbee2da491 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/JonathanHHenson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ddd62bd57936e8a2c8027956399009cbee2da491 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gummy_snake-0.8.0-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: gummy_snake-0.8.0-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 5.4 MB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e815383ac3ef054ca46649f46923db868946ea996dd5cacac0fe5f3db57a66c2
|
|
| MD5 |
12eee3ed2fc6c9bcf8280b798b986399
|
|
| BLAKE2b-256 |
968eb86efab63fd7de129bb342d770e3efd6c4f67d30c71867b78f0df50e4b3f
|
Provenance
The following attestation bundles were made for gummy_snake-0.8.0-cp312-cp312-macosx_11_0_arm64.whl:
Publisher:
publish.yml on JonathanHHenson/gummy_snake
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gummy_snake-0.8.0-cp312-cp312-macosx_11_0_arm64.whl -
Subject digest:
e815383ac3ef054ca46649f46923db868946ea996dd5cacac0fe5f3db57a66c2 - Sigstore transparency entry: 2047334987
- Sigstore integration time:
-
Permalink:
JonathanHHenson/gummy_snake@ddd62bd57936e8a2c8027956399009cbee2da491 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/JonathanHHenson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ddd62bd57936e8a2c8027956399009cbee2da491 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gummy_snake-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl.
File metadata
- Download URL: gummy_snake-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl
- Upload date:
- Size: 5.7 MB
- Tags: CPython 3.12, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c247cdee58f7d2f31668d0e0156f929574509ca55153b680dfae2b46407fe66
|
|
| MD5 |
813ceaf8e594ecc2de94552d641d37a6
|
|
| BLAKE2b-256 |
601ddffc58f50cd0826a03ba217286badb016b742a26611f02003fdec0ff0a12
|
Provenance
The following attestation bundles were made for gummy_snake-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl:
Publisher:
publish.yml on JonathanHHenson/gummy_snake
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gummy_snake-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl -
Subject digest:
5c247cdee58f7d2f31668d0e0156f929574509ca55153b680dfae2b46407fe66 - Sigstore transparency entry: 2047335037
- Sigstore integration time:
-
Permalink:
JonathanHHenson/gummy_snake@ddd62bd57936e8a2c8027956399009cbee2da491 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/JonathanHHenson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ddd62bd57936e8a2c8027956399009cbee2da491 -
Trigger Event:
push
-
Statement type: