Go · 2738 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package avatars owns avatar resolution for shithub user profiles. S09
4 // ships the deterministic SVG identicon (rendered server-side, no upload
5 // needed) plus the routing skeleton for serving uploaded avatars from
6 // object storage. Real upload (resize / EXIF-strip) lands in S10.
7 package avatars
8
9 import (
10 "crypto/sha256"
11 "fmt"
12 "strings"
13 )
14
15 // IdenticonSize is the SVG viewBox side length. The image is square and
16 // renders at any CSS px size via width/height attributes.
17 const IdenticonSize = 5
18
19 // palette is the fixed accent set used to color identicons. Sourced from
20 // Primer's accent ramp so the result feels native to the rest of the
21 // shithub chrome. 12 entries gives reasonable variety without
22 // overwhelming the page.
23 var palette = []string{
24 "#e85aad", // pink
25 "#bf3989", // pink-deep
26 "#a371f7", // purple
27 "#8957e5", // purple-deep
28 "#388bfd", // blue
29 "#1f6feb", // blue-deep
30 "#3fb950", // green
31 "#238636", // green-deep
32 "#d29922", // yellow-deep
33 "#db6d28", // orange
34 "#bd561d", // orange-deep
35 "#f85149", // red
36 }
37
38 // Identicon returns an inline SVG identicon for username. Same input →
39 // same output (deterministic). The SVG carries width/height attributes
40 // so it can be embedded as inline markup OR served as a standalone file.
41 //
42 // The pattern is a 5×5 grid mirrored horizontally — left two columns
43 // are mirrored to the right, the middle column is independent. Each
44 // cell is colored if the corresponding bit in the digest is 1.
45 func Identicon(username string, pixelSize int) string {
46 if pixelSize <= 0 {
47 pixelSize = 80
48 }
49 digest := sha256.Sum256([]byte(strings.ToLower(username)))
50 color := palette[int(digest[0])%len(palette)]
51
52 var b strings.Builder
53 fmt.Fprintf(&b,
54 `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" shape-rendering="crispEdges" role="img" aria-label="identicon">`,
55 pixelSize, pixelSize, IdenticonSize, IdenticonSize)
56 fmt.Fprintf(&b, `<rect width="%d" height="%d" fill="#21262d"/>`, IdenticonSize, IdenticonSize)
57
58 // Walk 15 cells (5 rows × 3 unique columns). Mirror the first two
59 // columns onto the last two so the identicon is symmetric.
60 bit := 0
61 for row := 0; row < IdenticonSize; row++ {
62 for col := 0; col < 3; col++ {
63 b8 := digest[1+(bit/8)] // bytes 1..15 cover the 25 bits we sample
64 on := (b8 >> (uint(bit) % 8)) & 1
65 bit++
66 if on != 1 {
67 continue
68 }
69 fmt.Fprintf(&b, `<rect x="%d" y="%d" width="1" height="1" fill="%s"/>`, col, row, color)
70 if col < 2 {
71 mirror := IdenticonSize - 1 - col
72 fmt.Fprintf(&b, `<rect x="%d" y="%d" width="1" height="1" fill="%s"/>`, mirror, row, color)
73 }
74 }
75 }
76 b.WriteString(`</svg>`)
77 return b.String()
78 }
79