## Deterministic avatar system (avatar-v1). ## ## canonical seed = HKDF-SHA256(ikm=DID, salt="freeq-world-avatar", info="avatar-v1") ## All traits are abstract — nothing is inferred from profile data. import std/[math, tables] import ./sha256 type Traits* = OrderedTable[string, string] Avatar* = object schema*: string did*: string baseGenerator*: string canonicalSeedHex*: string traits*: Traits Facing* = enum fSouth = "south", fNorth = "north", fEast = "east", fWest = "west" Sprite* = object width*, height*: int pixels*: seq[int] ## palette indices palette*: seq[string] const traitKeys* = [ "body_silhouette", "head_shape", "skin_palette", "hair_shape", "hair_palette", "eye_pixels", "shirt_jacket", "pants_skirt", "shoes", "accessory", "idle_movement", "walk_cadence", "speech_sound", "accent_palette", "arrival_effect", "musical_leitmotif"] const options*: array[15, (string, seq[string])] = [ ("body_silhouette", @["slim", "broad", "round", "tall", "compact", "angular"]), ("head_shape", @["round", "square", "oval", "wide"]), # stylized fictional palette — deliberately includes non-naturalistic hues ("skin_palette", @["#e8b48c", "#c68863", "#8d5a3b", "#5c3a26", "#f4d6b8", "#9fb8ad", "#b7a6d9", "#7d9c6f"]), ("hair_shape", @["crop", "spikes", "bob", "long", "curls", "mohawk", "bald", "bun"]), ("hair_palette", @["#2b2b3a", "#5a3825", "#c9a227", "#b8434e", "#3f7cac", "#67c26b", "#e8e6df", "#8447ad"]), ("eye_pixels", @["dot", "wide", "wink", "visor"]), ("shirt_jacket", @["#3f7cac", "#b8434e", "#67c26b", "#c9a227", "#8447ad", "#3a9188", "#d97b29", "#6d7680"]), ("pants_skirt", @["#31394d", "#5a3825", "#3a5a40", "#6b3a55", "#444444", "#7a5c99"]), ("shoes", @["#1e1e28", "#5a3825", "#b8434e", "#e8e6df"]), ("accessory", @["none", "hat", "glasses", "scarf", "antenna", "flower", "headphones", "badge"]), ("idle_movement", @["bob", "sway", "blink", "tap"]), ("walk_cadence", @["steady", "bouncy", "brisk", "ambling"]), ("speech_sound", @["beep", "blip", "burble", "chirp", "hum"]), ("accent_palette", @["#ffd166", "#06d6a0", "#ef476f", "#118ab2", "#f78c6b", "#9b5de5"]), ("arrival_effect", @["sparkle", "dissolve", "drop", "teleport-rings"])] func optionsFor*(key: string): seq[string] = for (k, v) in options: if k == key: return v raise newException(KeyError, "no options for trait " & key) type Prng* = object a, b, c, d: uint32 func u32le(seed: openArray[byte], i: int): uint32 = for k in 0 ..< 4: result = result or (uint32(seed[i + k]) shl (8 * k)) func initPrng*(seed: openArray[byte]): Prng = ## sfc32 seeded from the first 16 bytes of `seed` — bit-identical to the ## JavaScript original the fixture was generated with. Prng(a: u32le(seed, 0), b: u32le(seed, 4), c: u32le(seed, 8), d: u32le(seed, 12)) func next*(p: var Prng): float = ## Next double in [0, 1). let t = p.a + p.b + p.d let d = p.d + 1 let a = p.b xor (p.b shr 9) let b = p.c + (p.c shl 3) let c = ((p.c shl 21) or (p.c shr 11)) + t p = Prng(a: a, b: b, c: c, d: d) float(t) / 4294967296.0 func pick*(p: var Prng, opts: openArray[string]): string = let i = int(floor(p.next() * float(opts.len))) opts[min(i, opts.len - 1)] func canonicalSeed*(did: string): seq[byte] = ## 32-byte canonical seed for a DID. hkdf(did, "freeq-world-avatar", "avatar-v1", 32) func deriveAvatar*(did: string): Avatar = let seed = canonicalSeed(did) var rng = initPrng(seed) var traits: Traits for key in traitKeys: traits[key] = if key == "musical_leitmotif": # pointer to the leitmotif derivation (own HKDF domain) "motif-v1" else: pick(rng, optionsFor(key)) Avatar(schema: "freeq.at/profile/avatar/v1", did: did, baseGenerator: "avatar-v1", canonicalSeedHex: toHex(seed), traits: traits) const W* = 16 H* = 24 const SKIN = 1 HAIR = 2 SHIRT = 3 PANTS = 4 SHOE = 5 INK = 6 ACCENT = 7 WHITE = 8 func renderSpritePixels*(av: Avatar, facing = fSouth, frame = 0): Sprite = ## Procedural 16x24 pixel body. Rows: hair/head 0-9, torso 10-17, legs 18-21, ## shoes 22-23. Facing changes the face; walk frames swing arms and legs. let t = av.traits var px = newSeq[int](W * H) proc put(x, y, c: int) = if x >= 0 and x < W and y >= 0 and y < H: px[y * W + x] = c proc hspan(x0, x1, y, c: int) = for x in x0 .. x1: put(x, y, c) let silhouette = t["body_silhouette"] bodyW = if silhouette in ["broad", "round"]: 10 elif silhouette == "slim": 6 else: 8 bodyX0 = (W - bodyW) div 2 bodyX1 = bodyX0 + bodyW - 1 headShape = t["head_shape"] headW = if headShape == "wide": 10 else: 8 headX0 = (W - headW) div 2 headX1 = headX0 + headW - 1 headTop = if silhouette == "tall": 1 else: 2 hair = t["hair_shape"] acc = t["accessory"] # head for y in headTop ..< 10: let shrink = if headShape in ["round", "oval"] and (y == headTop or y == 9): 1 else: 0 hspan(headX0 + shrink, headX1 - shrink, y, SKIN) # hair if hair != "bald": hspan(headX0, headX1, headTop, HAIR) hspan(headX0, headX1, headTop + 1, HAIR) if hair in ["spikes", "mohawk"]: let mohawk = hair == "mohawk" start = headX0 + (if mohawk: headW div 2 - 1 else: 0) stop = if mohawk: headX0 + headW div 2 else: headX1 step = if mohawk: 1 else: 2 var x = start while x <= stop: put(x, headTop - 1, HAIR) x += step if hair in ["bob", "long", "curls"]: put(headX0, headTop + 2, HAIR) put(headX1, headTop + 2, HAIR) put(headX0, headTop + 3, HAIR) put(headX1, headTop + 3, HAIR) if hair == "long": put(headX0, headTop + 4, HAIR) put(headX1, headTop + 4, HAIR) if hair == "bun": put(W div 2, headTop - 1, HAIR) # face (not shown when facing north) if facing != fNorth: let eyeY = headTop + 4 eyes = t["eye_pixels"] mid = W div 2 eyeL = case facing of fEast: mid of fWest: headX0 + 1 else: headX0 + 2 eyeR = case facing of fEast: headX1 - 1 of fWest: mid - 1 else: headX1 - 2 if eyes == "visor": hspan(eyeL, eyeR, eyeY, ACCENT) else: put(eyeL, eyeY, INK) if eyes != "wink": put(eyeR, eyeY, INK) if eyes == "wide": put(eyeL, eyeY - 1, WHITE) put(eyeR, eyeY - 1, WHITE) elif hair != "bald": # back of head is hair for y in headTop ..< 9: hspan(headX0, headX1, y, HAIR) # torso for y in 10 ..< 18: hspan(bodyX0, bodyX1, y, SHIRT) # accent stripe hspan(bodyX0, bodyX1, 13, ACCENT) # arms (swing with walk frame) let swing = case frame of 0: 0 of 1: 1 else: -1 put(bodyX0 - 1, 11 + swing, SKIN) put(bodyX0 - 1, 12 + swing, SKIN) put(bodyX1 + 1, 11 - swing, SKIN) put(bodyX1 + 1, 12 - swing, SKIN) # legs let legW = max(2, bodyW div 2 - 1) gap = case frame of 0: 2 of 1: 4 else: 0 legL0 = W div 2 - gap div 2 - legW legR0 = W div 2 + int(ceil(float(gap) / 2.0)) for y in 18 ..< 22: hspan(legL0, legL0 + legW - 1, y, PANTS) hspan(legR0, legR0 + legW - 1, y, PANTS) for y in 22 ..< 24: hspan(legL0, legL0 + legW - 1, y, SHOE) hspan(legR0, legR0 + legW - 1, y, SHOE) # accessory case acc of "hat": hspan(headX0 - 1, headX1 + 1, headTop, ACCENT) hspan(headX0, headX1, headTop - 1, ACCENT) of "antenna": put(W div 2, headTop - 1, INK) put(W div 2, headTop - 2, ACCENT) of "scarf": hspan(bodyX0, bodyX1, 10, ACCENT) of "glasses": if facing != fNorth: hspan(headX0 + 1, headX1 - 1, headTop + 4, INK) of "headphones": put(headX0 - 1, headTop + 3, ACCENT) put(headX1 + 1, headTop + 3, ACCENT) of "flower": put(headX1, headTop + 1, ACCENT) of "badge": put(bodyX0 + 1, 11, ACCENT) else: discard Sprite(width: W, height: H, pixels: px, palette: @["transparent", t["skin_palette"], t["hair_palette"], t["shirt_jacket"], t["pants_skirt"], t["shoes"], "#14141c", # outline/eyes t["accent_palette"], "#ffffff"]) func spriteHash*(av: Avatar): string = ## Deterministic content hash over all four facings — conformance fixture value. var bs: seq[byte] for facing in [fSouth, fNorth, fEast, fWest]: let s = renderSpritePixels(av, facing, 0) for p in s.pixels: bs.add byte(p and 255) for color in s.palette: for ch in color: bs.add byte(ch) toHex(digest(bs))