Metadata-Version: 2.4
Name: sprout-toolkit
Version: 0.4.1
Summary: Deterministic procedural 2D asset generation for Expo + react-native-skia
Author: Tzinny
License: MIT License
        
        Copyright (c) 2026 Tzinny
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://sprout-toolkit.tzinny.com/
Project-URL: Documentation, https://sprout-toolkit.tzinny.com/docs
Project-URL: Repository, https://github.com/Tzinny-dev/sprout-toolkit
Project-URL: Issues, https://github.com/Tzinny-dev/sprout-toolkit/issues
Project-URL: Changelog, https://github.com/Tzinny-dev/sprout-toolkit/blob/main/CHANGELOG.md
Project-URL: Funding, https://paypal.me/carlostzin
Project-URL: Buy me a coffee, https://www.buymeacoffee.com/tzinny
Keywords: procgen,sprites,spritesheet,skia,expo,react-native,game-dev
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pillow<13,>=10
Requires-Dist: typer>=0.12
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Dynamic: license-file

# sprout-toolkit

[![PyPI version](https://img.shields.io/pypi/v/sprout-toolkit)](https://pypi.org/project/sprout-toolkit/)
[![CI](https://github.com/Tzinny-dev/sprout-toolkit/actions/workflows/tests.yml/badge.svg)](https://github.com/Tzinny-dev/sprout-toolkit/actions/workflows/tests.yml)
[![Pipeline](https://github.com/Tzinny-dev/sprout-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/Tzinny-dev/sprout-toolkit/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)

**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](https://expo.dev) + [`react-native-skia`](https://github.com/Shopify/react-native-skia).
It generates atlases, tilemaps, autotiles, bitmap fonts and SkSL shaders from JSON specs.

![Hero blob walk cycle](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/hero_walk.gif)
![Spark burst](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/spark.gif)

---

## 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.

| | |
|---|---|
| ![Hero blob walk cycle, 8 frames](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/hero_walk.gif) | ![Spark burst, 8 frames](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/spark.gif) |
| `blob_walk` — 8-frame walk cycle (`specs/demo.json`) | `particles/spark` — one-shot animated burst (`specs/particles.json`) |

![Autotile island, 47 variants](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/island.png)

`terrain` + 47-variant autotile: island assembled from `mask → frame` lookups
(`specs/autotile.json`, `canonicalMask` in the generated `index.ts`).

![Props atlas: rock, bush, chest, mushroom, flower](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/props.png)

`props` — 5 kinds × deterministic variants, each frame with an anchor point
(`specs/props.json`).

![Critter roster: 5 archetypes](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/critter.png)

`critter` — 5 archetypes (quadruped, bird, fish, reptile, bug) with
composable parts + idle frames ([`critter_idle.gif`](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/critter_idle.gif))
(`specs/critter.json`).

![Flora roster](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/flora.png)

`flora` — canopies with sway ([`flora_sway.gif`](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/flora_sway.gif))
(`specs/flora.json`).

![Face moods](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/face.png)

`face` — 16 moods with blink (`specs/face.json`).

![Object grammar: 7 families, 27 forms](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/objects.png)

`props` v3 object grammar — 7 families × 27 forms for flavors + things
(`specs/objects.json`).

![UI atlas: button states, sliders, 9-patch panels](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/ui.png)

`ui` — button states, slider progress + knob, 9-patch panel (`specs/ui.json`).

![SPROUT rendered with the generated bitmap font](https://raw.githubusercontent.com/Tzinny-dev/sprout-toolkit/main/docs/showcase/sprout_text.png)

`font` — bitmap font rasterized from the bundled TTF, with advance metrics for
text layout (`specs/font.json`).

## Installation

```bash
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`)

```bash
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`](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](CHANGELOG.md)). Point it at a non-default interpreter with
`SPROUT_PYTHON=/path/to/python3`.

## Quick start

### Generate assets

```bash
# 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`)

```bash
# 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`](docs/spec.md#layoutsupersample).

### 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`:

```json
{
  "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:

```bash
# 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.json` or `out/index.ts` from a
> `--flat` batch 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; see
> `CHANGELOG.md` for 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

```bash
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](CHANGELOG.md).

## 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](CHANGELOG.md).

### Releasing (P1 checklist)

1. Bump `version` in `pyproject.toml`, `sprout/__init__.py` **and**
   `npm/package.json` (all three are asserted equal by `tests/test_fase8.py`;
   `publish.yml`'s `check-version` job also verifies the first two).
2. Add a `## [<version>]` section to `CHANGELOG.md` under `[Unreleased]`.
3. Tag and push: `git tag v<version> && git push origin v<version>` **is not
   enough on its own** — the `publish` workflow is `workflow_dispatch` only
   (never tag-triggered). Go to GitHub Actions → `publish` → "Run workflow" and
   pass the exact version string.
4. Add a GitHub Release (auto-drafting from the tag is fine) with the
   `CHANGELOG` section as notes.
5. 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**:

1. On https://pypi.org/manage/project/sprout-toolkit/release/<version>/, click
   **Delete all files** (irreversible) for small issues, or **yank** (keeps the
   version but blocks installs) for egregious bugs.
2. Bump a patch (`0.2.1` → `0.2.2`) immediately with a fix; never re-use a
   yanked version number.
3. If the GitHub Release asset was also bad, delete the tag + release and
   re-tag on the fixed commit.
4. 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 in `sprout/assets/fonts/`) —
  Bitstream Vera license, see `sprout/assets/fonts/DejaVuSansMono-Bold.LICENSE.txt`

## License

MIT — see [LICENSE](LICENSE).
