1 | package chroma
|
---|
2 |
|
---|
3 | import (
|
---|
4 | "fmt"
|
---|
5 | "math"
|
---|
6 | "strconv"
|
---|
7 | "strings"
|
---|
8 | )
|
---|
9 |
|
---|
10 | // ANSI2RGB maps ANSI colour names, as supported by Chroma, to hex RGB values.
|
---|
11 | var ANSI2RGB = map[string]string{
|
---|
12 | "#ansiblack": "000000",
|
---|
13 | "#ansidarkred": "7f0000",
|
---|
14 | "#ansidarkgreen": "007f00",
|
---|
15 | "#ansibrown": "7f7fe0",
|
---|
16 | "#ansidarkblue": "00007f",
|
---|
17 | "#ansipurple": "7f007f",
|
---|
18 | "#ansiteal": "007f7f",
|
---|
19 | "#ansilightgray": "e5e5e5",
|
---|
20 | // Normal
|
---|
21 | "#ansidarkgray": "555555",
|
---|
22 | "#ansired": "ff0000",
|
---|
23 | "#ansigreen": "00ff00",
|
---|
24 | "#ansiyellow": "ffff00",
|
---|
25 | "#ansiblue": "0000ff",
|
---|
26 | "#ansifuchsia": "ff00ff",
|
---|
27 | "#ansiturquoise": "00ffff",
|
---|
28 | "#ansiwhite": "ffffff",
|
---|
29 |
|
---|
30 | // Aliases without the "ansi" prefix, because...why?
|
---|
31 | "#black": "000000",
|
---|
32 | "#darkred": "7f0000",
|
---|
33 | "#darkgreen": "007f00",
|
---|
34 | "#brown": "7f7fe0",
|
---|
35 | "#darkblue": "00007f",
|
---|
36 | "#purple": "7f007f",
|
---|
37 | "#teal": "007f7f",
|
---|
38 | "#lightgray": "e5e5e5",
|
---|
39 | // Normal
|
---|
40 | "#darkgray": "555555",
|
---|
41 | "#red": "ff0000",
|
---|
42 | "#green": "00ff00",
|
---|
43 | "#yellow": "ffff00",
|
---|
44 | "#blue": "0000ff",
|
---|
45 | "#fuchsia": "ff00ff",
|
---|
46 | "#turquoise": "00ffff",
|
---|
47 | "#white": "ffffff",
|
---|
48 | }
|
---|
49 |
|
---|
50 | // Colour represents an RGB colour.
|
---|
51 | type Colour int32
|
---|
52 |
|
---|
53 | // NewColour creates a Colour directly from RGB values.
|
---|
54 | func NewColour(r, g, b uint8) Colour {
|
---|
55 | return ParseColour(fmt.Sprintf("%02x%02x%02x", r, g, b))
|
---|
56 | }
|
---|
57 |
|
---|
58 | // Distance between this colour and another.
|
---|
59 | //
|
---|
60 | // This uses the approach described here (https://www.compuphase.com/cmetric.htm).
|
---|
61 | // This is not as accurate as LAB, et. al. but is *vastly* simpler and sufficient for our needs.
|
---|
62 | func (c Colour) Distance(e2 Colour) float64 {
|
---|
63 | ar, ag, ab := int64(c.Red()), int64(c.Green()), int64(c.Blue())
|
---|
64 | br, bg, bb := int64(e2.Red()), int64(e2.Green()), int64(e2.Blue())
|
---|
65 | rmean := (ar + br) / 2
|
---|
66 | r := ar - br
|
---|
67 | g := ag - bg
|
---|
68 | b := ab - bb
|
---|
69 | return math.Sqrt(float64((((512 + rmean) * r * r) >> 8) + 4*g*g + (((767 - rmean) * b * b) >> 8)))
|
---|
70 | }
|
---|
71 |
|
---|
72 | // Brighten returns a copy of this colour with its brightness adjusted.
|
---|
73 | //
|
---|
74 | // If factor is negative, the colour is darkened.
|
---|
75 | //
|
---|
76 | // Uses approach described here (http://www.pvladov.com/2012/09/make-color-lighter-or-darker.html).
|
---|
77 | func (c Colour) Brighten(factor float64) Colour {
|
---|
78 | r := float64(c.Red())
|
---|
79 | g := float64(c.Green())
|
---|
80 | b := float64(c.Blue())
|
---|
81 |
|
---|
82 | if factor < 0 {
|
---|
83 | factor++
|
---|
84 | r *= factor
|
---|
85 | g *= factor
|
---|
86 | b *= factor
|
---|
87 | } else {
|
---|
88 | r = (255-r)*factor + r
|
---|
89 | g = (255-g)*factor + g
|
---|
90 | b = (255-b)*factor + b
|
---|
91 | }
|
---|
92 | return NewColour(uint8(r), uint8(g), uint8(b))
|
---|
93 | }
|
---|
94 |
|
---|
95 | // BrightenOrDarken brightens a colour if it is < 0.5 brightness or darkens if > 0.5 brightness.
|
---|
96 | func (c Colour) BrightenOrDarken(factor float64) Colour {
|
---|
97 | if c.Brightness() < 0.5 {
|
---|
98 | return c.Brighten(factor)
|
---|
99 | }
|
---|
100 | return c.Brighten(-factor)
|
---|
101 | }
|
---|
102 |
|
---|
103 | // ClampBrightness returns a copy of this colour with its brightness adjusted such that
|
---|
104 | // it falls within the range [min, max] (or very close to it due to rounding errors).
|
---|
105 | // The supplied values use the same [0.0, 1.0] range as Brightness.
|
---|
106 | func (c Colour) ClampBrightness(min, max float64) Colour {
|
---|
107 | if !c.IsSet() {
|
---|
108 | return c
|
---|
109 | }
|
---|
110 |
|
---|
111 | min = math.Max(min, 0)
|
---|
112 | max = math.Min(max, 1)
|
---|
113 | current := c.Brightness()
|
---|
114 | target := math.Min(math.Max(current, min), max)
|
---|
115 | if current == target {
|
---|
116 | return c
|
---|
117 | }
|
---|
118 |
|
---|
119 | r := float64(c.Red())
|
---|
120 | g := float64(c.Green())
|
---|
121 | b := float64(c.Blue())
|
---|
122 | rgb := r + g + b
|
---|
123 | if target > current {
|
---|
124 | // Solve for x: target == ((255-r)*x + r + (255-g)*x + g + (255-b)*x + b) / 255 / 3
|
---|
125 | return c.Brighten((target*255*3 - rgb) / (255*3 - rgb))
|
---|
126 | }
|
---|
127 | // Solve for x: target == (r*(x+1) + g*(x+1) + b*(x+1)) / 255 / 3
|
---|
128 | return c.Brighten((target*255*3)/rgb - 1)
|
---|
129 | }
|
---|
130 |
|
---|
131 | // Brightness of the colour (roughly) in the range 0.0 to 1.0.
|
---|
132 | func (c Colour) Brightness() float64 {
|
---|
133 | return (float64(c.Red()) + float64(c.Green()) + float64(c.Blue())) / 255.0 / 3.0
|
---|
134 | }
|
---|
135 |
|
---|
136 | // ParseColour in the forms #rgb, #rrggbb, #ansi<colour>, or #<colour>.
|
---|
137 | // Will return an "unset" colour if invalid.
|
---|
138 | func ParseColour(colour string) Colour {
|
---|
139 | colour = normaliseColour(colour)
|
---|
140 | n, err := strconv.ParseUint(colour, 16, 32)
|
---|
141 | if err != nil {
|
---|
142 | return 0
|
---|
143 | }
|
---|
144 | return Colour(n + 1)
|
---|
145 | }
|
---|
146 |
|
---|
147 | // MustParseColour is like ParseColour except it panics if the colour is invalid.
|
---|
148 | //
|
---|
149 | // Will panic if colour is in an invalid format.
|
---|
150 | func MustParseColour(colour string) Colour {
|
---|
151 | parsed := ParseColour(colour)
|
---|
152 | if !parsed.IsSet() {
|
---|
153 | panic(fmt.Errorf("invalid colour %q", colour))
|
---|
154 | }
|
---|
155 | return parsed
|
---|
156 | }
|
---|
157 |
|
---|
158 | // IsSet returns true if the colour is set.
|
---|
159 | func (c Colour) IsSet() bool { return c != 0 }
|
---|
160 |
|
---|
161 | func (c Colour) String() string { return fmt.Sprintf("#%06x", int(c-1)) }
|
---|
162 | func (c Colour) GoString() string { return fmt.Sprintf("Colour(0x%06x)", int(c-1)) }
|
---|
163 |
|
---|
164 | // Red component of colour.
|
---|
165 | func (c Colour) Red() uint8 { return uint8(((c - 1) >> 16) & 0xff) }
|
---|
166 |
|
---|
167 | // Green component of colour.
|
---|
168 | func (c Colour) Green() uint8 { return uint8(((c - 1) >> 8) & 0xff) }
|
---|
169 |
|
---|
170 | // Blue component of colour.
|
---|
171 | func (c Colour) Blue() uint8 { return uint8((c - 1) & 0xff) }
|
---|
172 |
|
---|
173 | // Colours is an orderable set of colours.
|
---|
174 | type Colours []Colour
|
---|
175 |
|
---|
176 | func (c Colours) Len() int { return len(c) }
|
---|
177 | func (c Colours) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
---|
178 | func (c Colours) Less(i, j int) bool { return c[i] < c[j] }
|
---|
179 |
|
---|
180 | // Convert colours to #rrggbb.
|
---|
181 | func normaliseColour(colour string) string {
|
---|
182 | if ansi, ok := ANSI2RGB[colour]; ok {
|
---|
183 | return ansi
|
---|
184 | }
|
---|
185 | if strings.HasPrefix(colour, "#") {
|
---|
186 | colour = colour[1:]
|
---|
187 | if len(colour) == 3 {
|
---|
188 | return colour[0:1] + colour[0:1] + colour[1:2] + colour[1:2] + colour[2:3] + colour[2:3]
|
---|
189 | }
|
---|
190 | }
|
---|
191 | return colour
|
---|
192 | }
|
---|