color.gno
3.03 Kb · 121 lines
1package art
2
3import "strings"
4
5// Default is the colour that means "let the consumer decide": the terminal's
6// own foreground or background in ANSI, and transparent in SVG.
7const Default = -1
8
9// RGB packs three 0-255 components into the 0xRRGGBB form this package uses.
10// Components outside 0-255 are clamped rather than wrapped, so a computed
11// channel cannot silently alias onto a neighbouring one.
12func RGB(r, g, b int) int {
13 return clamp8(r)<<16 | clamp8(g)<<8 | clamp8(b)
14}
15
16// Split returns the three 0-255 components of a packed colour. Default splits
17// to zeroes, so callers must test for Default before calling it rather than
18// after.
19func Split(c int) (r, g, b int) {
20 if c < 0 {
21 return 0, 0, 0
22 }
23 return (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff
24}
25
26// ParseHex reads "#rrggbb", "#rgb", or either without the leading '#', and
27// reports whether it understood the string. An empty string is not an error but
28// it is not a colour either: it reports Default, false, which is how a palette
29// marks a transparent entry.
30func ParseHex(s string) (int, bool) {
31 s = strings.TrimSpace(s)
32 s = strings.TrimPrefix(s, "#")
33 switch len(s) {
34 case 3:
35 r, ok1 := hexDigit(s[0])
36 g, ok2 := hexDigit(s[1])
37 b, ok3 := hexDigit(s[2])
38 if !ok1 || !ok2 || !ok3 {
39 return Default, false
40 }
41 // #abc is #aabbcc, not #0a0b0c.
42 return RGB(r*17, g*17, b*17), true
43 case 6:
44 r, ok1 := hexByte(s[0], s[1])
45 g, ok2 := hexByte(s[2], s[3])
46 b, ok3 := hexByte(s[4], s[5])
47 if !ok1 || !ok2 || !ok3 {
48 return Default, false
49 }
50 return RGB(r, g, b), true
51 }
52 return Default, false
53}
54
55// Hex is the inverse of ParseHex. Default renders as the empty string, because
56// there is no hex spelling of "the consumer's default".
57func Hex(c int) string {
58 if c < 0 {
59 return ""
60 }
61 r, g, b := Split(c)
62 var sb strings.Builder
63 sb.WriteByte('#')
64 writeHexByte(&sb, r)
65 writeHexByte(&sb, g)
66 writeHexByte(&sb, b)
67 return sb.String()
68}
69
70// Luminance returns perceived brightness on 0-255, using the Rec. 709 weights
71// (0.2126 R, 0.7152 G, 0.0722 B) in integer arithmetic. Default is treated as
72// black, so an unset colour ramps to the darkest glyph rather than to a
73// midpoint that would read as real content.
74//
75// This is the function [Ramp] keys on, and the package doc explains why keying
76// on it is the wrong default for a sprite.
77func Luminance(c int) int {
78 if c < 0 {
79 return 0
80 }
81 r, g, b := Split(c)
82 return (2126*r + 7152*g + 722*b) / 10000
83}
84
85func clamp8(v int) int {
86 if v < 0 {
87 return 0
88 }
89 if v > 255 {
90 return 255
91 }
92 return v
93}
94
95func hexDigit(c byte) (int, bool) {
96 switch {
97 case c >= '0' && c <= '9':
98 return int(c - '0'), true
99 case c >= 'a' && c <= 'f':
100 return int(c-'a') + 10, true
101 case c >= 'A' && c <= 'F':
102 return int(c-'A') + 10, true
103 }
104 return 0, false
105}
106
107func hexByte(hi, lo byte) (int, bool) {
108 h, ok1 := hexDigit(hi)
109 l, ok2 := hexDigit(lo)
110 if !ok1 || !ok2 {
111 return 0, false
112 }
113 return h*16 + l, true
114}
115
116const hexDigits = "0123456789abcdef"
117
118func writeHexByte(sb *strings.Builder, v int) {
119 sb.WriteByte(hexDigits[(v>>4)&0xf])
120 sb.WriteByte(hexDigits[v&0xf])
121}