1package tray
2
3import (
4 "math"
5
6 "github.com/bnema/neferbar/internal/theme"
7)
8
9// maxSamples caps the pixels counted per icon: enough for a color, and it
10// keeps the sums below 2^32 (4096 * 255 * 255) and the cost of a large icon
11// low.
12const maxSamples = 4096
13
14// histogram finds the dominant color of an icon. Colors are counted in 4096
15// buckets (4 bits per channel), weighted by opacity and saturation so a logo's
16// color wins over its grey outline. It is reused between icons: no
17// allocation.
18type histogram struct {
19 weight [4096]uint32
20 sum [4096][3]uint32
21}
22
23// dominant returns the dominant color of argb, an SNI pixmap: big-endian
24// ARGB32, 4 bytes per pixel. It reports false when the icon is empty, fully
25// transparent or grey. Large icons are sampled evenly.
26func (h *histogram) dominant(argb []byte) (RGB, bool) {
27 if len(argb) < 4 {
28 return RGB{}, false
29 }
30 clear(h.weight[:])
31 clear(h.sum[:])
32 // An odd pixel step crosses every column of a power-of-two wide icon,
33 // so samples do not line up in a few columns.
34 step := (len(argb)/4 + maxSamples - 1) / maxSamples
35 if step > 1 {
36 step |= 1
37 }
38 for i := 0; i+3 < len(argb); i += 4 * step {
39 a, r, g, b := uint32(argb[i]), uint32(argb[i+1]), uint32(argb[i+2]), uint32(argb[i+3])
40 if a < 128 {
41 continue
42 }
43 hi, lo := max(r, g, b), min(r, g, b)
44 chroma := hi - lo
45 if chroma < 40 {
46 continue // grey, black or white: outline and shading
47 }
48 w := a * chroma >> 8
49 k := (r>>4)<<8 | (g>>4)<<4 | b>>4
50 h.weight[k] += w
51 h.sum[k][0] += r * w
52 h.sum[k][1] += g * w
53 h.sum[k][2] += b * w
54 }
55 best := -1
56 for k, w := range h.weight {
57 if w > 0 && (best < 0 || w > h.weight[best]) {
58 best = k
59 }
60 }
61 if best < 0 {
62 return RGB{}, false
63 }
64 w := h.weight[best]
65 s := h.sum[best]
66 return RGB{uint8(s[0] / w), uint8(s[1] / w), uint8(s[2] / w)}, true
67}
68
69// readable moves c toward fg until it has enough contrast with bg, so a dark
70// logo still shows on a dark bar.
71func readable(c, bg, fg RGB) RGB {
72 const minContrast = 3.0 // WCAG's minimum for large text and icons
73 for i := 0; i < 10 && contrast(c, bg) < minContrast; i++ {
74 c = theme.Mix(c, fg, 0.25)
75 }
76 return c
77}
78
79// contrast is the WCAG contrast ratio of two colors, from 1 to 21.
80func contrast(a, b RGB) float64 {
81 la, lb := luminance(a), luminance(b)
82 if la < lb {
83 la, lb = lb, la
84 }
85 return (la + 0.05) / (lb + 0.05)
86}
87
88func luminance(c RGB) float64 {
89 lin := func(v uint8) float64 {
90 x := float64(v) / 255
91 if x <= 0.04045 {
92 return x / 12.92
93 }
94 return math.Pow((x+0.055)/1.055, 2.4)
95 }
96 return 0.2126*lin(c[0]) + 0.7152*lin(c[1]) + 0.0722*lin(c[2])
97}