Creature Avatars
A small tool that turns any text into a deterministic 32×32 creature. Same text, same creature—every time. How it works, why it's unique, and how it could encode identity in an image.
A small tool that turns any text into a deterministic 32×32 creature. Same text, same creature—every time. How it works, why it's unique, and how it could encode identity in an image.
Type a word, a sentence, or gibberish. The same string always produces the same little creature: a 32×32 pixel critter with two to six colors and a transparent background. Complexity (tier, colors, appendages, asymmetry) is derived from the hash of the text, so different text usually gets a different layout; same text always gets the same image.
You can try it here: Creature Avatar — add as many cards as you like, type different text in each, and watch the avatars update as you type. Download any as a PNG or as an animated GIF (one frame per character, so you see the creature evolve from the first letter to the last).
No account needed; it's just for fun.
Under the hood, the avatar is fully deterministic: every choice (colors, shape, features) is derived from the input text by a hash-once-then-slice scheme. The seed is hashed once to 256 bits (8 lanes). For each decision (e.g. palette, eyes, leg angles), a segment id (e.g. "palette", "eyes") is turned into a 53-bit mixer that selects which combination of those 256 bits to use, producing a value in [0, 1). A uniformity finalizer keeps that value well distributed so probabilities (mouth, nose, body, etc.) trigger as intended. Same seed plus same segment id always yields the same value, so the same text always produces the same image.
Complexity is hash-derived (content-sensitive). The complexity tier (1–5), number of colors (2–6 for higher tiers), whether the creature has appendages (arms/legs/tentacles), and vertical asymmetry are all derived from segment hashes—e.g. segmentPick(seed, "complexity_tier", 5) + 1, segmentRoll(seed, "appendages", 0.55), segmentRoll(seed, "asym_enable", 0.25)—not from string length. So changing any character typically produces a different tier and layout. Each tier unlocks more options: more colors, optional eyes, mouth, nose, eyebrows, beard, ears, horn or antlers, arms or legs with varied angles and thickness, and body outlines that can be circles, ellipses, or polygons (triangle through octagon). A small percentage of the time the generator instead draws a cloud, flower, repeating pattern, or space-themed scene (moon, stars, nebula, galaxy). Symmetry is usually left–right; occasionally it's top–bottom. All of that is decided by the hash of the text (and segment ids), so there's no randomness—only the illusion of variety.
The result is a unique visual fingerprint for that string: different text almost always produces a different image, and the same text always reproduces the same image. That makes it possible to tie an identity (e.g. a username or user id) to a picture without storing the identity in the image in plain form.
There is no system RNG and no PRNG. All variety comes from deterministic hashing. The same input always produces the same output on any machine, in any language, in any run—today or years from now.
1. Seed hash (256 bits). Hash only the seed (no segment id yet). No crypto; integer arithmetic only.
i to 5381 + i for i = 0..7.i, in order: low = codeUnit & 0xFF, high = (codeUnit >> 8) & 0xFF. For each lane 0..7: k = (i * 31 + lane * 17) (32-bit unsigned), then lane = mix32(lane, low ^ (k & 0xFF)) and lane = mix32(lane, high ^ ((k >> 8) & 0xFF)). So every character updates all eight lanes (position-dependent mixing); length is not used.mix32(h, byte) = ((h << 5) + h) ^ byte (32-bit unsigned).fmix32(h): k ^= k >>> 16, k = (k * 0x85ebca6b) >>> 0, k ^= k >>> 13, k = (k * 0xc2b2ae35) >>> 0, k ^= k >>> 16. Return the eight final lane values (l0..l7).2. Segment mixer (53 bits). From the segment id only (e.g. "palette", "eyes"): hash its UTF-8 bytes with a small djb2-style loop into lo/hi, apply fmix32 to both, then (fmix32(hi) << 21) | (fmix32(lo) >>> 11) → 53-bit mixer m53. Extract eight 6-bit values: m0 = (m53 >> 0) & 0x3F … m7 = (m53 >> 42) & 0x3F.
3. Segment hash (float in [0, 1)). Combine seed lanes with the mixer: l0m = l0 ^ (m0 * 0x01010101) … l7m = l7 ^ (m7 * 0x01010101) (32-bit). Then low = l0m ^ l2m ^ l4m ^ l6m, high = l1m ^ l3m ^ l5m ^ l7m. Form top53 = (high << 21) | (low >>> 11). Apply the uniformity finalizer: top53 = (top53 * 0x9E3779B97F4A7C15) >>> 11 (64-bit unsigned multiply then unsigned right shift). Return top53 / 2^53. That value is in [0, 1) and is uniformly distributed so feature probabilities (mouth, nose, body, eyebrows, etc.) trigger as intended.
4. How it’s used. Every decision in the generator uses only:
floor(segmentHash(seed, segmentId) * n) % n → integer in 0 .. n-1.segmentHash(seed, segmentId) < p → boolean.The only inputs are the seed string and the fixed set of segment ids. No clocks, no random number generators, no platform APIs. Reimplement the seed hash, segment mixer, and uniformity finalizer in any language (with 64-bit or BigInt for the finalizer step), use the same UTF-16 seed and UTF-8 segment ids, and you get the same sequence of choices and therefore the same image. That makes the system stable and portable for validation, encoding, or replay far into the future or on entirely different stacks (including the Fonsters app).
Because the mapping from text to image is effectively a one-way function (you can't reverse the hash to recover the original string), the avatar can be used in a few interesting ways:
So creature avatars aren't just a fun way to get a consistent icon from a string—they're a simple example of turning identity into a one-way, visual token that can be embedded in an image and later validated against a known set of possibilities.