diff --git a/assets/README.md b/assets/README.md
index 24daef9..a404f3c 100644
--- a/assets/README.md
+++ b/assets/README.md
@@ -58,6 +58,21 @@ its anti-aliased distance to the stroked outline:
| `icons/skrog-128.png` | VS Code Marketplace icon (the extension copies this file) |
| `icons/skrog-256.png`, `-512.png` | general raster use |
| `icons/social-preview.png` | 1280×640 GitHub social preview (upload in repo Settings) |
+| `icons/avatar-512.png` | 512×512 GitHub **organisation avatar** — the icon beside `wslkit / skrog`. Upload at [the org's profile settings](https://github.com/organizations/wslkit/settings/profile); there is no API for it, and GitHub has no per-repository avatar |
+
+> **Use `avatar-512.png`, not `skrog-512.png`, for the avatar.** The latter is
+> the bare mark on a *transparent* ground, which is right for a page that
+> supplies its own background and wrong for an avatar: the mark is `#2F3B45`
+> and GitHub's dark theme is `#0d1117`, so a transparent upload would very
+> nearly disappear for every dark-theme visitor. An avatar is an `
` on
+> whatever background the viewer's theme picked, with no styling hook to fix
+> it, so it has to carry its own contrast — hence the opaque off-white ground,
+> the same one the social card uses.
+>
+> The mark is centred on its **ink**, not its raster box: a hull body plan
+> sits low in its own square, and box-centring left it 13px low at 512.
+> `writeAvatar` measures rather than correcting by a constant, so it stays
+> right if the geometry in `tools/internal/mark` changes.
The tray status icons (`cmd/skrogtray/icons_windows.go`) are the mark tinted to
the engine state — green (running) / grey (idle) / red (down) — generated by the
diff --git a/assets/icons/avatar-512.png b/assets/icons/avatar-512.png
new file mode 100644
index 0000000..3594050
Binary files /dev/null and b/assets/icons/avatar-512.png differ
diff --git a/tools/genicons/main.go b/tools/genicons/main.go
index b587eac..44227f6 100644
--- a/tools/genicons/main.go
+++ b/tools/genicons/main.go
@@ -34,18 +34,90 @@ import (
// brand is the slate mark color, matching the SVG masters.
var brand = color.RGBA{0x2F, 0x3B, 0x45, 0xFF}
+// ground is the off-white the social card and the avatar sit on. Shared so the
+// two cannot drift into two different whites.
+var ground = color.RGBA{0xF7, 0xF6, 0xF3, 0xFF}
+
// writeSocial renders a 1280x640 GitHub social-preview card: the mark centered
// on the off-white brand ground. Uploaded via the repo's settings, not embedded.
func writeSocial(path string) {
const w, h, markPx = 1280, 640, 440
dst := image.NewRGBA(image.Rect(0, 0, w, h))
- draw.Draw(dst, dst.Bounds(), &image.Uniform{color.RGBA{0xF7, 0xF6, 0xF3, 0xFF}}, image.Point{}, draw.Src)
+ draw.Draw(dst, dst.Bounds(), &image.Uniform{ground}, image.Point{}, draw.Src)
m := mark.Raster(markPx, brand)
ox, oy := (w-markPx)/2, (h-markPx)/2
draw.Draw(dst, image.Rect(ox, oy, ox+markPx, oy+markPx), m, image.Point{}, draw.Over)
writePNG(path, dst)
}
+// writeAvatar renders the square org/user avatar GitHub shows beside
+// "wslkit / skrog", and anywhere else an account is named.
+//
+// It is NOT skrog-512.png, which is the bare mark on a transparent ground.
+// That file is right for a docs page that supplies its own background and
+// wrong here: the mark is #2F3B45, GitHub's dark theme is #0d1117, and a
+// transparent avatar would put one on the other and very nearly disappear.
+// An avatar has no styling hook to fix that with — it is an
on whatever
+// background the viewer's theme picked.
+//
+// So it gets the same treatment as the social card: an opaque off-white
+// ground, which reads on both themes because it carries its own contrast.
+//
+// GitHub downscales to 460px and then to ~40px in the repo header, so the
+// mark is inset rather than bled to the edge — at header size a full-bleed
+// mark loses its outline to the crop.
+func writeAvatar(path string) {
+ const size, markPx = 512, 360
+ dst := image.NewRGBA(image.Rect(0, 0, size, size))
+ draw.Draw(dst, dst.Bounds(), &image.Uniform{ground}, image.Point{}, draw.Src)
+
+ m := mark.Raster(markPx, brand)
+
+ // Centre the INK, not the raster box. The mark's drawing does not fill its
+ // own square evenly -- a hull body plan is wider than it is tall and sits
+ // low in the frame -- so centring the box left the glyph 13px low at this
+ // size, which reads as a misaligned logo rather than as the shape's own
+ // proportions.
+ //
+ // Measured rather than corrected by a constant, so this stays true if the
+ // geometry in tools/internal/mark ever changes.
+ b := inkBounds(m)
+ ox := (size - b.Dx()) / 2
+ oy := (size - b.Dy()) / 2
+ draw.Draw(dst,
+ image.Rect(ox-b.Min.X, oy-b.Min.Y, ox-b.Min.X+markPx, oy-b.Min.Y+markPx),
+ m, image.Point{}, draw.Over)
+ writePNG(path, dst)
+}
+
+// inkBounds is the tight box around everything non-transparent in img.
+func inkBounds(img image.Image) image.Rectangle {
+ b := img.Bounds()
+ minX, minY, maxX, maxY := b.Max.X, b.Max.Y, b.Min.X-1, b.Min.Y-1
+ for y := b.Min.Y; y < b.Max.Y; y++ {
+ for x := b.Min.X; x < b.Max.X; x++ {
+ if _, _, _, a := img.At(x, y).RGBA(); a > 0 {
+ if x < minX {
+ minX = x
+ }
+ if y < minY {
+ minY = y
+ }
+ if x > maxX {
+ maxX = x
+ }
+ if y > maxY {
+ maxY = y
+ }
+ }
+ }
+ }
+ if maxX < minX || maxY < minY {
+ return b // nothing drawn; centre the box and let the caller see it
+ }
+ return image.Rect(minX, minY, maxX+1, maxY+1)
+}
+
func writePNG(path string, img image.Image) {
f, err := os.Create(path)
must(err)
@@ -66,6 +138,7 @@ func main() {
writePNG("assets/icons/skrog-512.png", mark.Raster(512, brand))
writePNG("assets/icons/favicon-32.png", mark.Raster(32, brand))
writeSocial("assets/icons/social-preview.png")
+ writeAvatar("assets/icons/avatar-512.png")
// Windows .ico with the sizes Explorer, the taskbar and the tray use.
writeICO("assets/icons/skrog.ico", []int{16, 24, 32, 48, 64, 128, 256}, brand)