# gsichtl
Deterministic pixel-art avatars from any seed. *Gsichtl* is Bavarian for
"little face".


The seed is any byte string, typically a public user or group id. The same
seed always gives the same picture, so every client draws it locally and
nothing has to be stored or sent. The crate does no I/O, is
`#![forbid(unsafe_code)]` and has one dependency, `blake3`, plus `png` behind
an optional feature.
## Generators
- `gsichtl::monster(seed)`: an 8x8 monster with antennae, horns or ears, big
eyes and a mouth. Up to 324,000 monsters: 4 backgrounds, 10 bodies,
6 accents, 6 tops, 3 silhouettes, 3 brows, 5 eyes and 5 mouths.
- `gsichtl::nerd(seed)`: an 8x8 human face with hair, glasses or goggles and
sometimes headphones. Up to 1,327,104 nerds: 4 backgrounds, 6 skins,
8 hair colors, 3 frames, 6 accents, 2 heads, 6 hair styles, 4 eyes,
4 mouths and with or without headphones.
- `gsichtl::face(seed)`: a monster or a nerd, chosen by the seed (about half
each). It is not necessarily `monster(seed)` or `nerd(seed)`.
- `gsichtl::crest(seed)`: a 10x10 shield with a division and an emblem, for
groups. 28,800 crests: 4 backgrounds, 6 fields, 5 second fields, 2 metals,
3 shields, 5 divisions and 8 emblems.
- `gsichtl::badge(seed)`: a 12x12 round badge (disc, octagon or cog) with a
division, an emblem and sometimes a metal rim, for groups. 57,600 badges:
4 backgrounds, 6 fields, 5 second fields, 2 metals, 3 shapes, with or
without a rim, 5 divisions and 8 emblems.
- `gsichtl::realm(seed)`: a 20x20 small world for groups: a floating island,
a planet or a floating patch of sea, with a landmark, a sky detail and
sometimes a waterfall or a ring. Made to stand free, rendered with
`to_rgba_transparent` or `to_png_transparent`. 34,560 realms:
4 backgrounds, 3 shapes (an island counts twice in the draw), 6 biomes,
5 landmarks per shape, 6 accents, with or without a waterfall or ring, and
4 sky details.
- `gsichtl::retro(seed)`: a 20x20 retro game sprite for groups: a creature,
a weapon, an item or a piece of old tech, palette-swapped in neon colors,
sometimes in front of an explosion, a blood splatter or sparkles. Original
designs in the style of classic games. Made to stand free like a realm.
21,504 retros: 4 backgrounds, 24 motifs, 8 colors, 7 second colors and
4 effects.
- `gsichtl::icon(seed)`: a realm or a retro sprite, chosen by the seed (about
half each), the group counterpart of `face`. It is not necessarily
`realm(seed)` or `retro(seed)`.
The face counts are upper bounds: where no part uses the accent color, two
faces that differ only in their accent look the same.
## Installation
```toml
[dependencies]
gsichtl = "0.1"
```
For PNG output, turn on the `png` feature:
```toml
[dependencies]
gsichtl = { version = "0.1", features = ["png"] }
```
## Usage
Render to RGBA8:
```rust
let avatar = gsichtl::face(b"alice");
// 12 px per cell, 16 px of background around the grid: 128 x 128 RGBA8.
let image = avatar.to_rgba(12, 16);
assert_eq!((image.width, image.height), (128, 128));
```
Draw the cells yourself:
```rust
let a = gsichtl::crest(b"community-7");
for y in 0..a.side() {
for x in 0..a.side() {
let color = a.cell(x, y).unwrap_or(a.background());
// Fill the square at (x, y) with `color`, an sRGB `[u8; 3]`.
}
}
```
Encode as PNG, with the `png` feature:
```rust
let png = gsichtl::face(b"alice").to_png(16, 8)?;
std::fs::write("alice.png", png)?;
```
An image is `side * cell_px + 2 * margin_px` pixels wide and high, at most
3060. `cell_px == 0` gives a plain square of background.
`to_rgba` and `to_png` are opaque. `to_rgba_transparent` and
`to_png_transparent` render the same picture, but leave the background and
the margin fully transparent, so the avatar stands free on your own surface:
```rust
let image = gsichtl::crest(b"community-7").to_rgba_transparent(3, 2);
assert_eq!(&image.pixels[..4], &[0, 0, 0, 0]);
```
## How it works
The seed goes through BLAKE3 in derive-key mode, with one context string per
generator: `gsichtl 2026-10-02 face v1`, `gsichtl 2026-10-02 monster v1`,
`gsichtl 2026-10-02 nerd v1`, `gsichtl 2026-10-02 crest v1`,
`gsichtl 2026-10-03 badge v1`, `gsichtl 2026-10-03 realm v1`,
`gsichtl 2026-10-03 retro v1` and `gsichtl 2026-10-03 icon v1`. The
extendable output is the stream of draws:
each draw takes 4 bytes, modulo the number of choices. The parts are small
character grids, painted in a fixed order. Eyes, brows and mouths are clipped
to the painted body, so they never leave the silhouette.
BLAKE3 serves as a deterministic mixer here, not for security. Its output is
fixed by its specification, so updating the `blake3` crate does not change a
picture.
## Sample sheets
```sh
cargo run --example render --features png -- <dir> [count]
```
writes `face-NN.png` from the seed `user-N`, and `crest-NN.png`,
`badge-NN.png`, `realm-NN.png` and `retro-NN.png` from the seed `community-N`
into `<dir>`. Faces and badges have 16 px cells and an 8 px margin. Crests
have 18 px cells and a 14 px margin; realms and retro sprites have 10 px cells
and a 4 px margin on a transparent background, so all of them are 208 px wide
like a badge. The sheets above show 32 faces, and 16 crests above 16 badges
above 16 realms above 16 retro sprites:
```sh
magick montage <dir>/face-*.png -tile 8x -geometry +4+4 -background none docs/faces.png
magick montage <dir>/crest-0?.png <dir>/crest-1[0-5].png \
<dir>/badge-0?.png <dir>/badge-1[0-5].png \
<dir>/realm-0?.png <dir>/realm-1[0-5].png \
<dir>/retro-0?.png <dir>/retro-1[0-5].png \
-tile 8x -geometry +4+4 -background none docs/groups.png
```
## Stability
The same seed gives the same avatar within a `0.x` minor version. Changing a
part, a palette, the draw order or a context string is a breaking change and
bumps the minor version, because everyone who draws an avatar for the same id
has to see the same picture. `tests/gsichtl.rs` pins fingerprints of known
seeds.
## Not an identity check
Anyone can upload a picture that looks like another person's generated face.
A generated avatar is a decoration; never present it as verification of who
someone is.
## Minimum Rust version
Rust 1.91, edition 2024.
## License
MIT, see [LICENSE](LICENSE).