1package hintlabel
2
3import (
4 "image/color"
5 "math"
6 "strconv"
7)
8
9// Style is the resolved, physical-pixel label style and is also a cache key.
10type Style struct {
11 FontPx float64
12 Background, Foreground color.RGBA
13}
14
15// FontPixels resolves an explicit logical size or the output's pixel density.
16// Both millimetre dimensions must be plausible for the untransformed mode.
17func FontPixels(width, height, mmWidth, mmHeight int, scale, logical float64) float64 {
18 return min(maxFontPx, fontPixels(width, height, mmWidth, mmHeight, scale, logical))
19}
20
21// maxFontPx bounds the physical font size, whatever the configuration or the
22// scale ask for.
23const maxFontPx = 256
24
25func fontPixels(width, height, mmWidth, mmHeight int, scale, logical float64) float64 {
26 if scale <= 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
27 scale = 1
28 }
29 if logical > 0 && !math.IsInf(logical, 0) {
30 return logical * scale
31 }
32 if width > 0 && height > 0 && mmWidth > 0 && mmHeight > 0 {
33 ratio := (float64(width) / float64(height)) / (float64(mmWidth) / float64(mmHeight))
34 if math.Abs(ratio-1) <= .2 {
35 return min(math.Round(40*scale), max(math.Round(13*scale), math.Round(3.2*float64(width)/float64(mmWidth))))
36 }
37 }
38 return 13 * scale * math.Sqrt(max(1, float64(height)/(1080*scale)))
39}
40
41func parseColor(s string, fallback color.RGBA) color.RGBA {
42 if len(s) != 7 || s[0] != '#' {
43 return fallback
44 }
45 n, err := strconv.ParseUint(s[1:], 16, 24)
46 if err != nil {
47 return fallback
48 }
49 return color.RGBA{uint8(n >> 16), uint8(n >> 8), uint8(n), 255}
50}
51
52func luminance(c color.RGBA) float64 {
53 linear := func(v uint8) float64 {
54 x := float64(v) / 255
55 if x <= .04045 {
56 return x / 12.92
57 }
58 return math.Pow((x+.055)/1.055, 2.4)
59 }
60 return .2126*linear(c.R) + .7152*linear(c.G) + .0722*linear(c.B)
61}
62
63func contrast(a, b color.RGBA) float64 {
64 x, y := luminance(a), luminance(b)
65 return (max(x, y) + .05) / (min(x, y) + .05)
66}
67
68// Colors derives ink from the background. Near-black and near-white have a
69// narrow luminance gap where neither meets AA; in that gap move HSL
70// lightness away from .5, preserving hue and saturation.
71// Explicit foreground overrides are honoured as supplied.
72func Colors(active, background, foreground string) (color.RGBA, color.RGBA) {
73 bg := parseColor(background, parseColor(active, color.RGBA{128, 128, 128, 255}))
74 dark, light := color.RGBA{17, 17, 17, 255}, color.RGBA{245, 245, 245, 255}
75 ink := dark
76 if contrast(bg, light) > contrast(bg, dark) {
77 ink = light
78 }
79 for contrast(bg, ink) < 4.5 {
80 // Move HSL lightness toward zero while preserving hue and saturation.
81 hi, lo := max(bg.R, bg.G, bg.B), min(bg.R, bg.G, bg.B)
82 l := (float64(hi) + float64(lo)) / 510
83 if l > .5 {
84 // Above .5, moving toward white increases contrast with dark ink.
85 bg.R += uint8(math.Ceil(float64(255-bg.R) * .01))
86 bg.G += uint8(math.Ceil(float64(255-bg.G) * .01))
87 bg.B += uint8(math.Ceil(float64(255-bg.B) * .01))
88 ink = dark
89 } else {
90 bg.R = uint8(float64(bg.R) * .99)
91 bg.G = uint8(float64(bg.G) * .99)
92 bg.B = uint8(float64(bg.B) * .99)
93 ink = light
94 }
95 }
96 return bg, parseColor(foreground, ink)
97}
98
99func mix(ink, bg color.RGBA) color.RGBA {
100 channel := func(a, b uint8) uint8 { return uint8(math.Round(.45*float64(a) + .55*float64(b))) }
101 return color.RGBA{channel(ink.R, bg.R), channel(ink.G, bg.G), channel(ink.B, bg.B), 255}
102}