sprout-toolkit
SPROUT TOOLKIT — Skia Procedural Rendering & Optimization Unified Toolkit
Grow your 2D assets procedurally.
Sprout is an open-source toolkit for deterministic procedural 2D asset generation
targeting Expo + react-native-skia.
It generates atlases, tilemaps, autotiles, bitmap fonts and SkSL shaders from JSON specs.
Architecture
sprout/
├── sprout/ # Python package
│ ├── __init__.py # __version__
│ ├── __main__.py # `python -m sprout` entry point (npm bin target)
│ ├── cli.py # CLI (Typer): generate, batch, watch, info, lint, diff, validate, catalog, import
│ ├── exporter.py # Spritesheet packing, manifest.json, index.ts (+ tiers/silhouette)
│ ├── spec.py # Load/validate JSON specs (schema v0)
│ ├── catalog.py # Catalog file -> spec codegen (external mapping)
│ ├── palettes.py # Named palettes + outline rule
│ ├── autotile.py # 8-bit masks, 16/47 lookup
│ ├── sksl.py # Runtime SkSL shaders (FBM + tier templates)
│ ├── assets/fonts/ # Bundled font (ships in the wheel)
│ └── generators/ # Generation plugins
│ ├── base.py # FrameData + Generator (contract)
│ ├── terrain.py # Seamless tiles + autotile
│ ├── props.py # Static objects (12 kinds incl. v3 grammar, anchors)
│ ├── particles.py # Animated bursts with easing
│ ├── ui.py # Button, slider, 9-patch panel
│ ├── font.py # Bitmap font from TTF (one glyph per frame)
│ ├── blob_walk.py # 8-frame walk cycle
│ ├── critter.py # 5 archetypes with composable parts
│ ├── flora.py # Canopies + undergrowth, sway anim
│ └── face.py # 16 moods with blink
├── specs/ # Example specs (one per generator)
├── tests/ # 420 tests (determinism, generators, CLI, catalog, tiers)
├── docs/ # spec.md, cli.md, api.md, generators.md + showcase/
├── npm/ # npm wrapper (`npx sprout`, strict version pin)
├── site/ # Landing page (GitHub Pages)
├── .github/workflows/ # tests.yml (matrix + wheel), ci.yml (pipeline), publish.yml, pages.yml
├── LICENSE # MIT
└── pyproject.toml
Features
| Feature | Description |
|---|---|
| Deterministic procgen | Numeric seeds → reproducible assets (testable in CI). |
| 8-bit auto-tile | 8-bit autotile mask (N/E/S/W + diagonals), 16 or 47 variants. |
| Bitmap fonts from TTF | Glyph atlas with advance metrics, ready for text layout. |
| Runtime SkSL shaders | Generated Skia shaders (FBM value-noise, tileable), no bundle weight added. |
| Spec introspection | sprout info/sprout lint/sprout diff to inspect, validate quality, and compare builds. |
| Catalog codegen | sprout catalog + validate --coverage: specs from any catalog file via an external mapping.json. |
| Loose PNG import | sprout import: pack hand-drawn/external frames into an atlas + manifest + index.ts. |
| npm wrapper | npx sprout (Node ≥18) spawning the Python CLI with a strict version pin. |
| Flexible export | PNG8/PNG24, TexturePacker format, atlas mipmaps — all opt-in. |
Showcase
Everything below is generated from JSON specs — same seed, same pixels, every time.
blob_walk — 8-frame walk cycle (specs/demo.json) |
particles/spark — one-shot animated burst (specs/particles.json) |
terrain + 47-variant autotile: island assembled from mask → frame lookups
(specs/autotile.json, canonicalMask in the generated index.ts).
props — 5 kinds × deterministic variants, each frame with an anchor point
(specs/props.json).
critter — 5 archetypes (quadruped, bird, fish, reptile, bug) with
composable parts + idle frames (critter_idle.gif)
(specs/critter.json).
flora — canopies with sway (flora_sway.gif)
(specs/flora.json).
face — 16 moods with blink (specs/face.json).
props v3 object grammar — 7 families × 27 forms for flavors + things
(specs/objects.json).
ui — button states, slider progress + knob, 9-patch panel (specs/ui.json).
font — bitmap font rasterized from the bundled TTF, with advance metrics for
text layout (specs/font.json).
Installation
pip install sprout-toolkit
# or for development:
git clone https://github.com/Tzinny-dev/sprout-toolkit
cd sprout-toolkit
python -m venv .venv && source .venv/bin/activate
pip install -e ".[test]"
The installed command is sprout (not sprout-toolkit — that's just the PyPI distribution name).
npm wrapper (npx sprout)
npm install --save-dev sprout-toolkit
python3 -m pip install "sprout-toolkit==0.4.0" # pinned CLI for the wrapper
npx sprout --version
The sprout bin (npm/bin/sprout.js) spawns
python3 -m sprout and refuses to run when the installed CLI version
differs from the wrapper's own — both channels release in lockstep (see
CHANGELOG). Point it at a non-default interpreter with
SPROUT_PYTHON=/path/to/python3.
Quick start
Generate assets
# New here? 10-minute walkthrough: docs/starter-tutorial.md
# Fastest start: writes starter.json + generates its atlas
sprout init --out ./assets/starter
# Generate every asset defined under specs/
sprout batch specs/ --out ./out # ./out/<spec name>/ per atlas — see below
sprout batch specs/ --out ./out --flat # old shared-directory layout (see Migrating)
# Generate a specific spec
sprout generate specs/demo.json --out ./out
# Watch mode: regenerates when files under specs/ change (Ctrl-C to exit)
sprout watch specs/ --out ./out --interval 1.0
# Inspect a spec (human report or JSON)
sprout info specs/particles.json
sprout info --json specs/particles.json
# Analyze spec quality: atlas padding + empty frames
sprout lint specs/ui.json
sprout lint --json specs/ui.json
# Generate a bitmap font atlas (printable ASCII, bundled font)
sprout generate specs/font.json --out ./out
# Compare two specs, or two already-generated output directories
sprout diff specs/props.json specs/ui.json
sprout diff --json ./out/a ./out/b
# Validate a spec
sprout validate specs/demo.json
# Coverage: fail if any catalog id has no frame (CI check)
sprout validate specs/catalog.json --coverage catalog.ts --field key --map mapping.json
# Codegen: catalog file + external mapping.json -> spec
sprout catalog catalog.ts --field key --map mapping.json --out specs/catalog.json
# Pack loose PNG frames (hand-drawn/external art) into an atlas + manifest
sprout import ./frames/ --out ./assets/gestures --cols 2
# Multi-resolution tiers: one atlas per size + combined index.ts (atlasSources, pickTier)
sprout generate specs/demo.json --tiers 64,128,256 --out ./tiers
# Single-resolution override / prebaked silhouette mask (black x alpha)
sprout generate specs/demo.json --frame-px 128 --out ./out-128
sprout generate specs/demo.json --silhouette --out ./out
Export options (generate / batch)
# PNG8 (indexed palette, lighter atlas) or PNG24 (no alpha)
sprout generate specs/ui.json --png-mode png8
sprout generate specs/autotile.json --png-mode png24 # only if the atlas has no transparency
# In addition to manifest.json, emit <name>.tpsheet.json (TexturePacker JSON Hash format)
sprout generate specs/props.json --texturepacker
# Chain of atlas mip levels (@0.5x, @0.25x, @0.125x by default)
sprout generate specs/particles.json --mipmaps
sprout generate specs/particles.json --mipmaps --mipmap-levels 2
# Antialiased curves: render at Nx framePx and box-filter back down
sprout generate specs/critter.json --supersample 4
--png-mode defaults to rgba (no change). png8 quantizes to 256 colors
while preserving the alpha channel (ideal for pixel-art-style limited
palettes); png24 drops alpha entirely — only use it on atlases with no
real transparency (e.g. terrain). --texturepacker/--mipmaps are
opt-in and don't affect manifest.json/index.ts, except that --mipmaps
adds the mipmaps.levels block to the manifest.
--supersample overrides layout.supersample, which is where the setting
normally lives so the smoothing travels with the spec. PIL's primitives are
aliased, so curves stair-step, and raising framePx does not fix it:
sample: nearest point-samples and throws the extra pixels away instead of
averaging them. Supersampling renders bigger and box-filters, which is what
creates the antialiased edges. The atlas is unchanged in logical terms
(framePx, atlasW/atlasH and every record's x/y/w/h are the
same); the cost is PNG size and render time. 1 — the default — reproduces
output without it byte for byte. See docs/spec.md.
Available generators
generator |
Description | Params |
|---|---|---|
terrain |
Seamless noise tiles + 16/47 autotile | contrast, lift, cells, octaves, bevel, autotile |
blob_walk |
8-frame walk cycle (hero blob) | colors (body, outline, belly, ...) |
props |
Static objects: rock, bush, chest, mushroom, flower — each frame includes an anchor point (frames[].anchor in the manifest, computed from the real alpha bbox) |
kind, fill, outline |
particles |
Animated bursts: spark, smoke, dust, bubble |
kind, particles, core, trail |
ui |
Interface: button (normal/hover/pressed states), slider (progress + knob), panel (9-patch, frames=9) |
kind, fill, outline, accent |
font |
Bitmap font from TTF, one glyph per frame (frames = len(chars)) |
chars, font_path, size, fill |
Example spec using props:
{
"name": "props_atlas",
"seed": 2024,
"layout": { "framePx": 64, "cols": 6, "tileLogical": 32, "sample": "nearest", "supersample": 4 },
"items": [
{ "id": "rock", "generator": "props", "frames": 6, "params": { "kind": "rock" } },
{ "id": "chest", "generator": "props", "frames": 4, "params": { "kind": "chest", "fill": [160, 110, 50] } }
]
}
Migrating to 0.4.0
One breaking change, and it is the layout of batch --out.
batch --out now writes one directory per spec, at <out>/<spec name>/.
Before 0.4.0 every spec wrote into the shared --out under its own
files.atlas name. All 13 bundled specs call that atlas.png, so a single
--out left one atlas holding whichever spec ran last while the command
still reported 13/13 specs ok. The nesting is what makes a shared --out
lossless.
If your read paths break, you have two options:
# 1. update the paths (recommended)
# before: out/atlas.png after: out/my_spec_atlas/atlas.png
# 2. keep the old layout exactly
sprout batch specs/ --out ./out --flat
--flat puts every atlas straight into ./out as before, and refuses (exit 1,
nothing written) if two specs would write the same atlas file. Its other
outputs take the atlas stem as a prefix — ./out/atlas.manifest.json,
./out/atlas.index.ts, and likewise for --silhouette, --texturepacker and
runtime shaders — because one shared directory cannot hold two
manifest.json files. watch --out takes the same flag and the same rule.
If you are on 0.4.0 and read
out/manifest.jsonorout/index.tsfrom a--flatbatch with more than one spec, you were reading the last spec's file and the others' metadata was already gone. 0.4.1 gives each spec its own; seeCHANGELOG.mdfor the rename.
generate --out is unchanged: it never nested, so single-atlas setups
should need no edits at all.
Also new in 0.4.0, none of it breaking: --supersample and the
layout.supersample spec field (antialiased curves, supersample: 1 reproduces
the old bytes), sprout diversity, third-party generators via entry points,
data-driven vocabularies, and meta.provenance.supersample in the manifest.
Development
pip install -e ".[test]"
pytest -q
CI (.github/workflows/tests.yml) runs the full suite on every push/PR,
across Python 3.10–3.13; .github/workflows/ci.yml runs the pipeline
checks (spec lint, end-to-end generation, npm wrapper version pin, and the
catalog coverage example). Publishing to PyPI is manual
(.github/workflows/publish.yml, trusted publishing via OIDC) — see
CHANGELOG.
Releasing / Emergency Rollback
CI runs on every push/PR: tests.yml (Python 3.10–3.13 matrix + wheel
smoke test) and ci.yml (spec lint, generation, npm pin). Publishing to
PyPI is manual (.github/workflows/publish.yml, trusted publishing via
OIDC) — see CHANGELOG.
Releasing (P1 checklist)
- Bump
versioninpyproject.toml,sprout/__init__.pyandnpm/package.json(all three are asserted equal bytests/test_fase8.py;publish.yml'scheck-versionjob also verifies the first two). - Add a
## [<version>]section toCHANGELOG.mdunder[Unreleased]. - Tag and push:
git tag v<version> && git push origin v<version>is not enough on its own — thepublishworkflow isworkflow_dispatchonly (never tag-triggered). Go to GitHub Actions →publish→ "Run workflow" and pass the exact version string. - Add a GitHub Release (auto-drafting from the tag is fine) with the
CHANGELOGsection as notes. - Publish the npm wrapper from
npm/(npm publish) with the same version — the wrapper pins the CLI exactly.
Emergency rollback
PyPI versions are immutable — never delete, only yank:
- On https://pypi.org/manage/project/sprout-toolkit/release//, click Delete all files (irreversible) for small issues, or yank (keeps the version but blocks installs) for egregious bugs.
- Bump a patch (
0.2.1→0.2.2) immediately with a fix; never re-use a yanked version number. - If the GitHub Release asset was also bad, delete the tag + release and re-tag on the fixed commit.
- Post an incident note on the release you yanked linking to the replacement.
Credits
typer+Pillow— CLI and image generation- DejaVu Fonts (
DejaVuSansMono-Bold.ttf, bundled insprout/assets/fonts/) — Bitstream Vera license, seesprout/assets/fonts/DejaVuSansMono-Bold.LICENSE.txt
License
MIT — see LICENSE.
Metadata
Release files for sprout-toolkit 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sprout_toolkit-0.4.1.tar.gz | 343.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sprout_toolkit-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 643.4 kB
Release files / sprout_toolkit-0.4.1.tar.gz
| Download URL | sprout_toolkit-0.4.1.tar.gz |
|---|---|
| Size | 343.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aa3d3c3ae55a4061d62f8c08a9a141897b2dd4584cdc3ccd40d6fe361cdaf5bd
|
|
BLAKE2b-256 checksum How to use checksums |
11a947846d610604e76a0045340e7f92b10ce93b7992e06cbf97f250c092c43c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / sprout_toolkit-0.4.1-py3-none-any.whl
| Download URL | sprout_toolkit-0.4.1-py3-none-any.whl |
|---|---|
| Size | 299.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1fdf1c5acde0e9ed2dbf9ede8990977f28bd7f247f70c874c42f49157b25240d
|
|
BLAKE2b-256 checksum How to use checksums |
eba27f821d0e47474f60f92f5a547eb906620c3db22b11570e30c7fadbf97a73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log