colour.go

  1package chroma
  2
  3import (
  4	"fmt"
  5	"math"
  6	"strconv"
  7	"strings"
  8)
  9
 10// ANSI2RGB maps ANSI colour names, as supported by Chroma, to hex RGB values.
 11var 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.
 51type Colour int32
 52
 53// NewColour creates a Colour directly from RGB values.
 54func 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.
 62func (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).
 77func (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.
 96func (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.
106func (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.
132func (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.
138func 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) //nolint:gosec
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.
150func 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.
159func (c Colour) IsSet() bool { return c != 0 }
160
161func (c Colour) String() string   { return fmt.Sprintf("#%06x", int(c-1)) }
162func (c Colour) GoString() string { return fmt.Sprintf("Colour(0x%06x)", int(c-1)) }
163
164// Red component of colour.
165func (c Colour) Red() uint8 { return uint8(((c - 1) >> 16) & 0xff) } //nolint:gosec
166
167// Green component of colour.
168func (c Colour) Green() uint8 { return uint8(((c - 1) >> 8) & 0xff) } //nolint:gosec
169
170// Blue component of colour.
171func (c Colour) Blue() uint8 { return uint8((c - 1) & 0xff) } //nolint:gosec
172
173// Colours is an orderable set of colours.
174type Colours []Colour
175
176func (c Colours) Len() int           { return len(c) }
177func (c Colours) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
178func (c Colours) Less(i, j int) bool { return c[i] < c[j] }
179
180// Convert colours to #rrggbb.
181func 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}