//go:build ignore // +build ignore // Quick generator for the scanner PWA's launcher icons. Run via: // // go run scripts/gen-scanner-icons.go // // Produces frontend/public/icons/scanner-{192,512}.png — black-on-green // rounded-square marks with a centred "GG" wordmark. Stays in the repo // so the icons can be regenerated when the brand changes, and so the // reader doesn't have to wonder where the binary asset came from. package main import ( "fmt" "image" "image/color" "image/png" "os" "path/filepath" ) const ( bg = "#0a0a0a" // app background brand = "#22c55e" // guestguard green textCol = "#0a0a0a" // text drawn over the green disc ) type icon struct { size int path string } func main() { icons := []icon{ {192, "frontend/public/icons/scanner-192.png"}, {512, "frontend/public/icons/scanner-512.png"}, } for _, it := range icons { if err := writeIcon(it); err != nil { fmt.Fprintln(os.Stderr, "icon:", it.path, err) os.Exit(1) } fmt.Println("wrote", it.path) } } func writeIcon(it icon) error { if err := os.MkdirAll(filepath.Dir(it.path), 0o755); err != nil { return err } img := image.NewRGBA(image.Rect(0, 0, it.size, it.size)) bgC := mustParseHex(bg) brandC := mustParseHex(brand) textC := mustParseHex(textCol) // Solid background, rounded corners — Android maskable expects the // safe inner area; we leave 8% padding so a circular mask doesn't // crop the brand mark. radius := it.size / 6 for y := 0; y < it.size; y++ { for x := 0; x < it.size; x++ { if inRoundedRect(x, y, it.size, it.size, radius) { img.Set(x, y, bgC) } else { img.Set(x, y, color.RGBA{0, 0, 0, 0}) } } } // Centered green disc, ~62% of canvas — leaves room for the maskable // safe-zone trim. cx, cy := it.size/2, it.size/2 rDisc := int(float64(it.size) * 0.31) for y := cy - rDisc; y <= cy+rDisc; y++ { for x := cx - rDisc; x <= cx+rDisc; x++ { dx, dy := x-cx, y-cy if dx*dx+dy*dy <= rDisc*rDisc { img.Set(x, y, brandC) } } } // "GG" wordmark — two stylised C shapes (open on the right) made of // thick ring segments. Avoids depending on a font file in this // throwaway generator; produces a clean recognisable mark at both // 192 and 512. drawG(img, cx-rDisc/2-1, cy, int(float64(rDisc)*0.38), int(float64(rDisc)*0.10), textC) drawG(img, cx+rDisc/2+1, cy, int(float64(rDisc)*0.38), int(float64(rDisc)*0.10), textC) f, err := os.Create(it.path) if err != nil { return err } defer f.Close() return png.Encode(f, img) } func drawG(img *image.RGBA, cx, cy, r, thickness int, c color.Color) { // Open-right ring + small inward tick on the bottom — looks enough // like a "G" to read at icon scale. for y := cy - r; y <= cy+r; y++ { for x := cx - r; x <= cx+r; x++ { dx, dy := x-cx, y-cy d2 := dx*dx + dy*dy if d2 <= r*r && d2 >= (r-thickness)*(r-thickness) { // Open the right side so the shape reads as a G. if dx > r-thickness*3 && dy > -thickness && dy < thickness { continue } img.Set(x, y, c) } } } // Inward horizontal tick at mid-right for the G's crossbar. for x := cx; x <= cx+r/2; x++ { for y := cy - thickness/2; y <= cy+thickness/2; y++ { img.Set(x, y, c) } } } func inRoundedRect(x, y, w, h, r int) bool { if x < r && y < r { dx, dy := r-x, r-y return dx*dx+dy*dy <= r*r } if x >= w-r && y < r { dx, dy := x-(w-r-1), r-y return dx*dx+dy*dy <= r*r } if x < r && y >= h-r { dx, dy := r-x, y-(h-r-1) return dx*dx+dy*dy <= r*r } if x >= w-r && y >= h-r { dx, dy := x-(w-r-1), y-(h-r-1) return dx*dx+dy*dy <= r*r } return true } func mustParseHex(s string) color.RGBA { if len(s) != 7 || s[0] != '#' { panic("bad hex: " + s) } var r, g, b uint8 fmt.Sscanf(s, "#%02x%02x%02x", &r, &g, &b) return color.RGBA{r, g, b, 0xff} }