main.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. // Command icon draws the MPCkit app icon: a 4x4 pad grid on the app's dark
  2. // background, with pad 1 (bottom left, as on the device) lit in the accent
  3. // colour. It writes a 1024x1024 PNG; `wails3 generate icons` makes the
  4. // Windows and macOS icons from it.
  5. //
  6. // go run ./tools/icon build/appicon.png
  7. //
  8. // With -dmg it writes the background of the macOS disk image instead: a
  9. // plain light 540x380 image (Finder labels the icons with dark text).
  10. //
  11. // go run ./tools/icon -dmg build/darwin/dmg-background.png
  12. package main
  13. import (
  14. "flag"
  15. "fmt"
  16. "image"
  17. "image/color"
  18. "image/png"
  19. "math"
  20. "os"
  21. )
  22. const (
  23. size = 1024
  24. ss = 4 // supersampling per axis, for smooth edges
  25. )
  26. // Colours from the app (frontend/src/app.css).
  27. var (
  28. bg = rgb(0x15, 0x17, 0x1A)
  29. edge = rgb(0x35, 0x3A, 0x43)
  30. pad = rgb(0x33, 0x38, 0x42)
  31. padEdge = rgb(0x5A, 0x61, 0x6E)
  32. accent = rgb(0xF5, 0xA5, 0x24)
  33. )
  34. func rgb(r, g, b uint8) color.NRGBA { return color.NRGBA{r, g, b, 0xFF} }
  35. type rect struct{ x0, y0, x1, y1, r float64 }
  36. // inside reports whether (x, y) is inside the rounded rectangle.
  37. func (q rect) inside(x, y float64) bool {
  38. if x < q.x0 || x >= q.x1 || y < q.y0 || y >= q.y1 {
  39. return false
  40. }
  41. cx := math.Max(q.x0+q.r, math.Min(x, q.x1-q.r))
  42. cy := math.Max(q.y0+q.r, math.Min(y, q.y1-q.r))
  43. return (x-cx)*(x-cx)+(y-cy)*(y-cy) <= q.r*q.r
  44. }
  45. func (q rect) inset(d float64) rect {
  46. return rect{q.x0 + d, q.y0 + d, q.x1 - d, q.y1 - d, math.Max(q.r-d, 0)}
  47. }
  48. type layer struct {
  49. shape rect
  50. col color.NRGBA
  51. }
  52. func main() {
  53. dmg := flag.Bool("dmg", false, "write the disk image background instead of the icon")
  54. flag.Parse()
  55. if flag.NArg() != 1 {
  56. fmt.Fprintln(os.Stderr, "usage: icon [-dmg] out.png")
  57. os.Exit(2)
  58. }
  59. if *dmg {
  60. img := image.NewNRGBA(image.Rect(0, 0, 540, 380))
  61. for i := 0; i < len(img.Pix); i += 4 {
  62. img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = 0xF2, 0xF3, 0xF5, 0xFF
  63. }
  64. write(flag.Arg(0), img)
  65. return
  66. }
  67. // Body: a rounded square with a margin, as macOS icons have; the
  68. // same image works for Windows and Linux.
  69. body := rect{64, 64, 960, 960, 200}
  70. layers := []layer{{body, edge}, {body.inset(10), bg}}
  71. // 4x4 pads, 140 px with 32 px gaps, centred in the body.
  72. const cell, gap = 140.0, 32.0
  73. start := (size - (4*cell + 3*gap)) / 2
  74. for row := 0; row < 4; row++ {
  75. for col := 0; col < 4; col++ {
  76. x := start + float64(col)*(cell+gap)
  77. y := start + float64(row)*(cell+gap)
  78. r := rect{x, y, x + cell, y + cell, 28}
  79. if row == 3 && col == 0 { // pad 1: bottom left
  80. layers = append(layers, layer{r, accent})
  81. continue
  82. }
  83. layers = append(layers, layer{r, padEdge}, layer{r.inset(8), pad})
  84. }
  85. }
  86. img := image.NewNRGBA(image.Rect(0, 0, size, size))
  87. for py := 0; py < size; py++ {
  88. for px := 0; px < size; px++ {
  89. var r, g, b, a float64
  90. for sy := 0; sy < ss; sy++ {
  91. for sx := 0; sx < ss; sx++ {
  92. x := float64(px) + (float64(sx)+0.5)/ss
  93. y := float64(py) + (float64(sy)+0.5)/ss
  94. // Topmost layer that covers the point wins.
  95. for i := len(layers) - 1; i >= 0; i-- {
  96. if layers[i].shape.inside(x, y) {
  97. c := layers[i].col
  98. r += float64(c.R)
  99. g += float64(c.G)
  100. b += float64(c.B)
  101. a++
  102. break
  103. }
  104. }
  105. }
  106. }
  107. if a == 0 {
  108. continue // transparent outside the body
  109. }
  110. img.SetNRGBA(px, py, color.NRGBA{
  111. uint8(r/a + 0.5), uint8(g/a + 0.5), uint8(b/a + 0.5),
  112. uint8(255*a/(ss*ss) + 0.5),
  113. })
  114. }
  115. }
  116. write(flag.Arg(0), img)
  117. }
  118. func write(path string, img image.Image) {
  119. f, err := os.Create(path)
  120. if err != nil {
  121. fmt.Fprintln(os.Stderr, err)
  122. os.Exit(1)
  123. }
  124. if err := png.Encode(f, img); err != nil {
  125. fmt.Fprintln(os.Stderr, err)
  126. os.Exit(1)
  127. }
  128. if err := f.Close(); err != nil {
  129. fmt.Fprintln(os.Stderr, err)
  130. os.Exit(1)
  131. }
  132. }