You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

colour.go 4.5KB

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