// Package idn is quarry's identity mark: one hue and one small grid per
// person, derived from the name alone.
//
// It lives on its own because THREE packages need the same answer and a
// person must look the same in all of them. web draws the mark beside a
// byline, api hands it to the markup resolver so an @mention carries it,
// and both hash the name to the same hue. When userHue was written twice —
// once in web, once in api — nothing checked that the two agreed; they did,
// by luck, and a one-character edit in either would have given the same
// person two colours on the same page.
//
// Colours are OKLCH: its hue wheel is perceptually even and every person
// sits at the same lightness, so no hue reads louder than another. Hue and
// chroma are identity and never move; lightness is the context axis, set by
// the theme token --idn-l and nudged by callers for shade steps.
package idn
import (
"fmt"
"hash/fnv"
"strings"
"sync"
)
func sum(name string) uint64 {
h := fnv.New64a()
h.Write([]byte(name))
return h.Sum64()
}
// Hue is the person's colour, 0-359. One hue per person keeps the mark
// recognisable at a glance: the colour IS the identity as much as the
// pattern is.
func Hue(name string) int {
return int(sum(name) % 360)
}
// Chroma is the person's chroma band: 0 vivid, 1 soft. A second fixed
// identity dimension so two neighbouring hues still read as two people.
func Chroma(name string) int {
return int(sum(name) >> 40 & 1)
}
// Fill is the person's colour at the given lightness expression, e.g.
// "var(--idn-l)" or "calc(var(--idn-l) + 8%)". Chroma comes from the theme
// token for the person's band so themes can retune it in one place.
func Fill(name, lightness string) string {
return fmt.Sprintf("oklch(%s var(--idn-c%d) %d)", lightness, Chroma(name), Hue(name))
}
// Tone is the custom-property pair a stylesheet needs to paint a person:
// `--uh: H; --uc: var(--idn-cN)`, for a style attribute.
func Tone(name string) string {
return fmt.Sprintf("--uh: %d; --uc: var(--idn-c%d)", Hue(name), Chroma(name))
}
var memo sync.Map // name -> string
// Mark is the person's identicon as an inline SVG: a 5x3 half-grid
// mirrored into a 5x5 face, in two lightness steps of their hue, on a wash
// of the same hue. Fills go in style= because a presentation attribute
// cannot hold var().
//
// Memoised because a busy page draws the same handful of people dozens of
// times, and the answer never changes.
func Mark(name string) string {
if v, ok := memo.Load(name); ok {
return v.(string)
}
v := sum(name)
cells := v >> 9 // 15 bits: which of the 5x3 half-grid cells are on
shades := v >> 24 // 15 more: which on-cells take the deeper shade
if cells&0x7FFF == 0 {
cells = 0x1084 // a degenerate hash still gets a face: centre column
}
lit := Fill(name, "var(--idn-l)")
deep := Fill(name, "calc(var(--idn-l) - 12%)")
var b strings.Builder
fmt.Fprintf(&b, `<svg class="idn" viewBox="-0.6 -0.6 6.2 6.2" aria-hidden="true">`)
fmt.Fprintf(&b, `<rect x="-0.6" y="-0.6" width="6.2" height="6.2" rx="1" style="fill:%s" fill-opacity="0.14"/>`, lit)
cell := func(x, y int, fill string) {
fmt.Fprintf(&b, `<rect x="%d" y="%d" width="1" height="1" rx="0.22" style="fill:%s"/>`, x, y, fill)
}
for row := 0; row < 5; row++ {
for col := 0; col < 3; col++ {
i := uint(row*3 + col)
if cells>>i&1 == 1 {
fill := lit
if shades>>i&1 == 1 {
fill = deep
}
cell(col, row, fill)
if col < 2 {
cell(4-col, row, fill)
}
}
}
}
b.WriteString(`</svg>`)
out := b.String()
memo.Store(name, out)
return out
}