@@ -0,0 +1,78 @@ |
| 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 | +} |