A 2D anime-style character creator that draws characters programmatically from parametric vector shapes (SVG), rather than compositing pre-made art. A small skeleton of proportion anchors (head size, shoulder width, hip width, etc.) drives every shape, hair, face, clothes, limbs, so style stays consistent by construction instead of by curating matching assets. No AI image generation is involved when a character is drawn (but see How this was made: the code is AI-written, and the designs began from AI-generated references).
Animation is out of scope. Backgrounds and text overlays are a possible later addition (they'd slot in as extra SVG layers).
Fourteen named characters for Valley of Mist, plus guests from two other
projects (Katherina from ../time_slider_katharina; Gero and Linnea, and
Dritan and Lindita, from ../short_stories), and Risa, a schoolgirl from no
story of ours, all rendering at the chibi build. They are all the tall chibi, the one figure since
2026-09-26, when the realistic build and the old compressed chibi were
retired; a height from 0.8 to 1.3 stretches it for a younger or a
taller character. Risa is the exception in proportion: she is drawn on her own
longer body, measured off her reference (docs/risa-plan.md).
All are first drafts: each carries the colouring, frame and garments
its design calls for, judged at the size a character is actually seen on a
sheet rather than at full height. What is deliberately left off is listed
per cluster in docs/character-roster-plan.md, and the two least finished
are Krista and Chiyo, whose references lean hardest on a head accessory.
| Satoko | Satoshi | Kyoko | Tomohiro |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Chiyo | Daizen | Elara | Haruto |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Keiko | Krista | Reika | Reinhard |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Tenno | Viktor | Katherina | Gero |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Linnea | Dritan | Lindita | Risa |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
All of them live in ref-out/ as both .png and .svg. They are
transparent, so a render drops onto a scene as it is.
Under the clothes there is a body. The tunic and the boots are optional
like every other garment, and with the tunic off a figure shows its body in
a plain base layer (underwear that is always worn), with bare feet when the
boots come off too. That is what lets new garments be layered over a real
torso and skin tones be judged on more than a face and two hands; the web
tool offers the toggles and a row of skin tone swatches. See
docs/bare-body-plan.md.
They are also only two people. Kyoko is Satoko before the dye and the
burn, and Tomohiro is Satoshi the same way, so each pair is one preset
and a replace() of three fields rather than two sets of numbers that
happen to agree. That is deliberate: the resemblance is the point, and a
copied face keeps it only until somebody tunes an eye. See
docs/character-roster-plan.md.
The table is the tall chibi, the only figure since 2026-09-26: the
realistic build, which never worked as well, was retired with its
ref-out/real/ renders (docs/tall-chibi-plan.md).
ref-out/cover.svg is there too: a book cover composed around one of
them by cover.py, which is the same drawing code with a backdrop, mist
and a title stacked over it. That one is opaque, being a page rather than
a figure. Render it with ./cover.sh.
ref-out/sheet.svg is the whole cast on one page, from sheet.py. That
is where a cast gets judged rather than a character: whether these people
look like they come from one world is a question about the set. Render it
with ./sheet.sh.
ref-out/sheet_satoshi.svg is the same page with the other of the two
reference rosters: Satoshi's persona rather than Satoko's, so Tomohiro
stays and Satoko and Kyoko drop. Ten of the twelve are the same
characters as sheet.svg; presets.ROSTERS["satoshi"] is the one place
that says which two swap. Render it with ./sheet_satoshi.sh.
sheet.py also takes --members name1,name2,..., an arbitrary subset
overriding --roster, which is the hook ../valley_of_mist (the novel
this cast is for) uses to generate one character sheet per chapter,
restricted to whoever that chapter actually names: see that repo's
valley_of_mist_tools/generate_assets.py, which also draws its character
references and cover from here (render.sh, cover.sh). This repo stays
the generic drawing tool; that repo owns the chapter-scanning and
publication-specific detail.
The tables above link ref-out/on-white/, which is the same characters
drawn on a white background. That exists for this page and nothing else:
the outline is #0d0d0d and a dark theme here is about #0d1117, so a
transparent figure on it loses its whole outer contour, which is the
hard-edged look the project is for. Reach for the transparent ones for
anything real.
Both sets are the current state of the named characters, so they have to be refreshed whenever a shape changes, otherwise these tables show art the code no longer produces:
./refresh-ref-out.sh # re-render every named character, report what moved
./refresh-ref-out.sh --check # compare only, write nothing, exit 1 if staleIt renders every character in PRESETS and both pages, so adding a character means adding it to presets.py and
nothing else.
Everything else generated goes to out/, which is not checked in.
Current shape set: head (a circle at chibi scale, narrowing to a jaw as the build gets taller), face (eyes, brows, mouth, blush, scar, spectacles), a beard, five hairstyles that optionally divide into locks and change to a second tone over their lower half, and a layered outfit of tunic, undersleeves, belt, apron, skirt, underskirt, trousers and boots, plus uniform trim (standing collar, button placket, chest pockets, cross-body strap, tall boot shaft), an open coat at any length, and robe pieces (crossed front, hanging sleeve, obi), plus arms, legs and feet. All flat cel-shaded. A garment is worn when its color is set, so characters differ by which layers they have rather than by bespoke code.
Named characters live in presets.py, so a character is a checked-in
artifact that gets re-rendered as the shape code improves.
Not yet built: no pose variety, no picker UI, and no hair that leaves the
skull silhouette and comes back, which is what a ponytail and a topknot
both need. See STATUS.md for what is still weak and
docs/character-roster-plan.md for what each character is still waiting
on.
uv sync # venv, dependencies, dev tools, and this package (editable)
brew install cairo # for PNG exportDrawing needs nothing but the standard library: an SVG document is text.
cairosvg turns that text into a PNG and needs the system cairo
library, so it is an extra rather than a requirement. Without it,
rendering still writes a valid .svg, you just don't get a .png.
uv sync --no-dev --extra png # just the package and PNG export
pip install anime-character-creator[png] # or without uv, from a checkoutrender.sh is the way in. It runs the CLI from the project's own venv
with the one environment variable PNG export needs, since cairosvg
loads libcairo through dlopen, which on macOS doesn't look in
Homebrew's prefix.
./render.sh --out out/demo \
--hair-color "#e8b84b" --eye-color "#4a9c6d" \
--outfit-color "#4f7a52" --skin-tone "#f2c9a1" --boot-color "#5b4632"
./render.sh --out out/satoko --preset satoko
./render.sh --out out/satoshi --preset satoshiThe same CLI is installed as anime-character-creator and as
python -m anime_character_creator; both skip the cairo environment
render.sh sets, so on macOS they write the SVG and report that PNG
export is unavailable.
--preset starts from a named character in presets.py; any flag given
alongside it overrides that one value.
Garment flags, one per layer: --tunic-color (also spelled
--outfit-color), --undersleeve-color, --belt-color,
--apron-color, --skirt-color, --underskirt-color,
--trouser-color, --pouch-color, --boot-color, plus --skirt-length (hip 0 to ankle
1). A layer is worn when it has a color, so these add layers; to take one
away, drop it from the preset, since the command line has no way to say
"none".
Shape flags: --hairstyle (long_blunt, long_center_part, long_parted,
long_traced, long_fringed, short_crop, short_layered or short_tousled),
--hair-length, --frame (shoulder against hip, -1 to 1, only bites at
taller builds).
Expression flags, each neutral at its default: eye shape
(--eye-size, --eye-width, --eye-openness, --eye-lower-lid,
--eye-tilt, --eye-corner, --iris-size), plus --brow-tilt, --brow-weight,
--mouth-curve, --mouth-width, --blush, --scar-side.
./render.sh --out out/grumpy --mouth-curve -0.6 --blush 0 --brow-tilt 0.5--expression lays a named mood (hollow, stern, grim, sorrow,
resolute) over the character's resting face, changing only what the
face is doing and leaving what it is alone, so the same mood works on
any character. Individual knobs still win over it.
Add --flat to disable the cel-shading shadow shapes and see the flat
silhouette only. Output is written as both .svg (inspect/edit
directly in a browser or Inkscape) and .png (if cairosvg works).
Every render embeds a link that reproduces the exact character back into
both files, by default: an SVG <metadata> block, and a PNG tEXt chunk
(keyword Source, readable with any tool that reads PNG text, e.g.
Pillow's Image.open(...).text) carrying the same link, so a file that
leaves this repo, downloaded, shared, dropped into another project, stays
reconstructable. sheet.sh does the same with one link per member and
cover.sh with the featured character. Pass --no-metadata to skip it and
get a plain, minimal file. (This is a CLI-only default: render_character()
and friends default to off as a library, which is what keeps ref-out/
byte-for-byte comparable; see attribution.py.)
The background is transparent, so a render drops onto a scene as it
is. Pass --background white (or any SVG paint) if you want it filled.
The package lives in src/anime_character_creator/. Longer notes on the
machinery are in docs/architecture.md, and the
public surface is written up in docs/api.md.
skeleton.py:Skeletondataclass: head center/radius, the neck/shoulder/waist/hip/hem/limb widths, and the y-coordinates (neck, shoulder, waist, hip, hem, knee, ankle, foot) every shape positions itself against. Every character's comes fromskeleton_for(p): its body profile (BODY_TYPES, measured tall-chibi proportions) laid over the chibi build, stretched byheightwhen that is not 1.0.Skeleton.buildis the chibi's, the same for every figure.hair_marginis the headroom above the skull, in head radii, so hair has somewhere to go at a build where the head fills a third of the frame.colorutil.py:shade()derives a darker/more-saturated "shadow" tone from a base color, so palettes for cel-shading are computed, not hand-picked per shape.character.py: builds each body part as an SVG shape (paths, circles, capsule-strokes) positioned from the skeleton, then stacks them in z-order into one<svg>document.CharacterParamsholds the colors, anOutfitof garments, and aFaceStyleof expression knobs (eye aperture shape, brow tilt/weight, mouth curve/width, blush, cheek scar);render_character()is the entry point. The eye is an almond built from four quadratics, so one set of knobs spans a tall round chibi eye and a narrow lidded adult one.HAIRSTYLESmaps a name to the four outlines a haircut needs, plus an optional set of strands dividing the mass into locks, which is how a second cut gets added without touching the parts that draw hair.presets.py: named characters asCharacterParamsvalues,SATOKOandSATOSHI. Reachable from the CLI via--preset. The two share a palette through module constants, since they are meant to read as related.generate.py: CLI: renders one character to SVG, rasterizes to PNG viacairosvgif available. Installed as theanime-character-creatorcommand.
As a library, the whole thing is two calls:
from dataclasses import replace
from pathlib import Path
from anime_character_creator import PRESETS, build_skeleton, render_character
svg = render_character(PRESETS["satoko"], build_skeleton(heads=4.0))
Path("out/satoko-mid.svg").write_text(svg)
# a variant of a named character
grumpier = replace(PRESETS["satoshi"], face=replace(PRESETS["satoshi"].face, brow_tilt=0.6))uv sync # everything, including the dev tools
uv run pytest # imports, renders every preset, checks ref-out/ is current
uv run ruff check . # lint
uv run ruff format . # format
uv run pdoc anime_character_creator -o out/apidocs # generated API docsThe test suite is a smoke check, not a judge of whether a shape looks
right: that is decided by eye against ref/. What it catches is a broken
import, a part that stopped drawing, and a ref-out/ that no longer
matches the code.
CLAUDE.md holds the rules a change to this repo has to respect, and
STATUS.md is the running state: what is done, what is weak, what is
next.
This project was built with AI coding agents, directed by its owner: mostly Claude Code (Anthropic), with some work done with Cursor (using Grok models) and opencode (using the free "Big Pickle" model). AI-written code can be wrong; the repository has a test suite, but treat it as you would any software you have not read line by line.
- The drawing is ordinary code: no image generator runs when a character is
drawn. But the characters' designs began from AI-generated reference images
(some made with Grok, kept in
ref/), which set the style and the first roster, and several characters, Katherina and Risa among them, were traced from such references. - The documentation was written by Claude Code.
- Commits here carry no co-author trailer (this repository's own rule), so the history does not show which were AI-written: assume most of the code was.
- The companion app that uses this (anime_ai_companion) was built the same way and says so in its own README.
- No pre-drawn art, no scraping. Every pixel comes from shapes this code defines. This sidesteps licensing entirely and is what keeps every character on-model.
- Skeleton-relative, not hardcoded coordinates. Every shape is a
function of
head_rand the anchor y-values, not fixed pixels, so adjusting proportions (e.g. moving away from chibi later) doesn't require touching every shape. - Flat color, hard-edged (no gradients), and no shading plane across a garment. Matches the target cel-shaded anime look and is cheap to render. A second tone is for small elements, a pouch flap, a boot cuff, the turn under a hem, where it reads as thickness; a tunic, skirt or trouser leg stays one flat color and gets its form from the outline and the line work.
- Iterate on shape, not variety, first. Get one hairstyle/outfit looking right before adding a second, a shape template with wrong proportions is wrong for every character generated from it.




















0 comments
log in to comment.