Module:Color

From Space Station 14 Wiki
Revision as of 09:14, 24 May 2025 by Aliser (talk | contribs) (fixed a bug where attempting to create a color from a hex value with alpha was throwing an error (amazing))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Documentation for this module may be created at Module:Color/doc

--- Parse, convert and manipulate color values.
-- source: https://github.com/Firanel/lua-color under MIT
--
-- @classmod Color



local utils = require "Module:Color/utils/init"
local class = require "Module:Color/utils/class"

local colors_css = require "Module:Color/colors/css"


-- Lua 5.1 compat
local bitwise = require "Module:Color/utils/bitwise"
local bit_and = bitwise.bit_and
local bit_lshift = bitwise.bit_lshift
local bit_rshift = bitwise.bit_rshift


-- Utils


local function hcm_to_rgb(h, c, m)
  local r, g, b = 0, 0, 0

  h = h * 6
  local x = c * (1 - math.abs(h % 2 - 1))

  if h <= 1 then
    r, g, b = c, x, 0
  elseif h <= 2 then
    r, g, b = x, c, 0
  elseif h <= 3 then
    r, g, b = 0, c, x
  elseif h <= 4 then
    r, g, b = 0, x, c
  elseif h <= 5 then
    r, g, b = x, 0, c
  elseif h <= 6 then
    r, g, b = c, 0, x
  end

  return r + m, g + m, b + m
end

local function tonumPercent(str)
  if str:sub(-1) == "%" then
    return tonumber(str:sub(1, #str - 1)) / 100
  end
  return tonumber(str)
end



-- Color


--- Color constructor.
--
-- @function Color:__call
--
-- @tparam ?string|table|Color value Color value (default: `nil`)
--
-- @see Color:set

--- Red component.
-- @field r

--- Green component.
-- @field g

--- Blue component.
-- @field b

--- Alpha component.
-- @field a


--- Color class
local Color = class(nil, function (this, value)
  if value then
    this:set(value)
  end
end, {
  __is_color = true,
  r = 0,
  g = 0,
  b = 0,
  a = 1,
})

--- Table of color names.
-- <br>
-- Can be set to a table containing named colors to be used by `Color:set`
-- <br>
-- Values must be compatible with `Color:set`
-- <br>
-- Default: `nil`
--
-- @usage  Color.colorNames = { red = "#ff0000", green = "#00ff00", blue = "#0000ff" }
--local color = Color "green"
Color.colorNames = nil

--- Clone color
--
-- @treturn Color copy
function Color:clone()
  return Color(self)
end

--- Set color to value.
-- <br>
-- Called by constructor
-- <br><br>
-- Possible value types:
-- <ul>
--  <li>`Color`</li>
--  <li>color name as specified in `Color.colorNames`</li>
--  <li>css style functions as string:<ul>
--   <li>`rgb(r, g, b)`</li>
--   <li>`rgba(r, g, b, a)`</li>
--   <li>`hsl(h, s, l)`</li>
--   <li>`hsla(h, s, l, a)`</li>
--   <li>`hsv(h, s, v)`</li>
--   <li>`hsva(h, s, v, a)`</li>
--   <li>`hwb(h, w, b)`</li>
--   <li>`hwba(h, w, b, a)`</li>
--   <li>`cmyk(c, m, y, k)`</li>
--   </ul>
--   Values are in the same ranges as in css ([0;255] for rgb, [0;1] for alpha, ...)<br>
--   functions can be specified in a simplified syntax: `rgb(r, g, b) == rgb r g b`
--  </li>
--  <li>NCol string: `R10, 50%, 50%`</li>
--  <li>hex string: `#rgb` | `#rgba` | `#rrggbb` | `#rrggbbaa` (`#` can be omitted)</li>
--  <li>rgb values in [0;1]: `{r, g, b[, a]}` | `{r=r, g=g, b=b[, a=a]}`</li>
--  <li>hsv values in [0;1]: `{h=h, s=s, v=v[, a=a]}`</li>
--  <li>hsl values in [0;1]: `{h=h, s=s, l=l[, a=a]}`</li>
--  <li>hwb values in [0;1]: `{h=h, w=w, b=b[, a=a]}`</li>
--  <li>cmyk values in [0;1]: `{c=c, m=m, y=y, k=k}`</li>
--  <li>single set mode, table with any combination of the following: <ul>
--   <li>`red`</li>
--   <li>`green`</li>
--   <li>`blue`</li>
--   <li>`alpha`</li>
--   <li>`hue`</li>
--   <li>`saturation`</li>
--   <li>`value`</li>
--   <li>`lightness`</li>
--   <li>`whiteness`</li>
--   <li>`blackness`</li>
--   <li>`cyan`</li>
--   <li>`magenta`</li>
--   <li>`yellow`</li>
--   <li>`key`</li>
--   </ul>
--   All values are in `[0;1]`.<br>
--   They will be applied in the order: `rgba -> hsl -> hwb -> hsv -> cmyk`<br>
--   If `lightness` is given, saturation is treated as hsl saturation,
--   otherwise it will be treated as hsv saturation.
--  </li>
-- </ul>
--
-- @see Color:__call
--
-- @tparam string|table|Color value Color
--
-- @treturn Color self
--
-- @usage color:set "#f1f1f1"
-- @usage color:set "rgba(241, 241, 241, 0.5)"
-- @usage color:set "hsl 180 100% 20%"
-- @usage color:set { r = 0.255, g = 0.729, b = 0.412 }
-- @usage color:set { 0.255, 0.729, 0.412 } -- same as above
-- @usage color:set { h = 0.389, s = 0.65, v = 0.73 }
function Color:set(value)
  assert(value)

  -- from Color
  if value.__is_color then
    self.r = value.r
    self.g = value.g
    self.b = value.b
    self.a = value.a


  elseif type(value) == "string" then
    self.a = 1

    if value:sub(1, 1) ~= "#" then
      if Color.colorNames then
        local c = Color.colorNames[value]
        if c then return self:set(c) end
      end

      local func, values = value:match "(%w+)[ %(]+([x ,.%x%%]+)"
      if func ~= nil then
        if func == "rgb" then
          local r, g, b = values:match "([x.%x]+)[ ,]+([x.%x]+)[ ,]+([x.%x]+)"
          assert(r and g and b)
          self.r = tonumber(r) / 0xff
          self.g = tonumber(g) / 0xff
          self.b = tonumber(b) / 0xff
          return self
        elseif func == "rgba" then
          local r, g, b, a = values:match "([x.%x]+)[ ,]+([x.%x]+)[ ,]+([x.%x]+)[ ,]+([x.%x]+%%?)"
          assert(r and g and b and a)
          self.r = tonumber(r) / 0xff
          self.g = tonumber(g) / 0xff
          self.b = tonumber(b) / 0xff
          self.a = tonumPercent(a)
          return self
        elseif func == "hsv" then
          local h, s, v = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(h and s and v)
          return self:set {
            h = tonumber(h) / 360,
            s = tonumPercent(s),
            v = tonumPercent(v),
          }
        elseif func == "hsva" then
          local h, s, v, a = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(h and s and v and a)
          return self:set {
            h = tonumber(h) / 360,
            s = tonumPercent(s),
            v = tonumPercent(v),
            a = tonumPercent(a)
          }
        elseif func == "hsl" then
          local h, s, l = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(h and s and l)
          return self:set {
            h = tonumber(h) / 360,
            s = tonumPercent(s),
            l = tonumPercent(l),
          }
        elseif func == "hsla" then
          local h, s, l, a = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(h and s and l and a)
          return self:set {
            h = tonumber(h) / 360,
            s = tonumPercent(s),
            l = tonumPercent(l),
            a = tonumPercent(a)
          }
        elseif func == "hwb" then
          local h, w, b = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(h and w and b)
          return self:set {
            h = tonumber(h) / 360,
            w = tonumPercent(w),
            b = tonumPercent(b),
          }
        elseif func == "hwba" then
          local h, w, b, a = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(h and w and b and a)
          return self:set {
            h = tonumber(h) / 360,
            w = tonumPercent(w),
            b = tonumPercent(b),
            a = tonumPercent(a)
          }
        elseif func == "cmyk" then
          local c, m, y, k = values:match "([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
          assert(c and m and y and k)
          return self:set {
            c = tonumPercent(c),
            m = tonumPercent(m),
            y = tonumPercent(y),
            k = tonumPercent(k),
          }
        end
      else
        local col, dist, w, b, a = value:match "([RGBCMYrgbcmy])(%d*)[, ]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
        if col == nil then
          col, dist, w, b, a = value:match "([RGBCMYrgbcmy])(%d*)[, ]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)"
        end
        if col then
          col = col:lower()

          local h
          if     col == "r" then h = 0
          elseif col == "y" then h = 1/6
          elseif col == "g" then h = 2/6
          elseif col == "c" then h = 3/6
          elseif col == "b" then h = 4/6
          elseif col == "m" then h = 5/6 end

          if #dist > 0 then h = h + tonumber(dist) / 600 end

          return self:set {
            h = h,
            w = tonumPercent(w),
            b = tonumPercent(b),
            a = a and tonumPercent(a) or 1
          }
        end
      end
    else
      value = value:sub(2)
    end

    local pattern
    local div = 0xff
    if #value == 3 then
      pattern = "(%x)(%x)(%x)"
      div = 0xf
    elseif #value == 4 then
      pattern = "(%x)(%x)(%x)(%x)"
      div = 0xf
    elseif #value == 6 then
      pattern = "(%x%x)(%x%x)(%x%x)"
    elseif #value == 8 then
      pattern = "(%x%x)(%x%x)(%x%x)(%x%x)"
    else
      error "Not a valid color"
    end
    local r, g, b, a = value:match(pattern)
    assert(r ~= nil, "Not a valid color")
    self.r = tonumber(r, 16) / div
    self.g = tonumber(g, 16) / div
    self.b = tonumber(b, 16) / div
    self.a = a ~= nil and tonumber(a, 16) / div or 1

  -- table with rgb
  elseif value[1] ~= nil then
    self.r = value[1]
    self.g = value[2]
    self.b = value[3]
    self.a = value[4] or self.a or 1
  elseif value.r ~= nil then
    self.r = value.r
    self.g = value.g or self.g
    self.b = value.b or self.b
    self.a = value.a or self.a

  elseif value.c ~= nil then
    local k = 1 - value.k
    self.r = (1 - value.c) * k
    self.g = (1 - value.m) * k
    self.b = (1 - value.y) * k
    self.a = 1

  -- table with hs[vl]
  elseif value.h ~= nil then
    if value.w ~= nil then -- hwb
      value.v = 1 - value.b
      value.s = 1 - value.w / value.v
    end

    local hue, saturation = value.h, value.s
    assert(hue ~= nil, saturation ~= nil)

    local r, g, b = 0, 0, 0

    if value.v ~= nil then
      local v = value.v
      local chroma = saturation * v
      r, g, b = hcm_to_rgb(hue, chroma, v - chroma)

    elseif value.l ~= nil then
      local lightness = value.l
      local chroma = (1 - math.abs(2 * lightness - 1)) * saturation
      r, g, b = hcm_to_rgb(hue, chroma, lightness - chroma / 2)
    end

    self.r = r
    self.g = g
    self.b = b
    self.a = value.a or self.a or 1

  else -- Single set mode
    if value.red then self.r = value.red end
    if value.green then self.g = value.green end
    if value.blue then self.b = value.blue end
    if value.alpha then self.a = value.alpha end

    if value.lightness then
      local h, s, l = self:hsl()
      self:set {h= value.hue or h, s= value.saturation or s, l= value.lightness or l}
      value.hue = nil
      value.saturation = nil
    end

    if value.whiteness or value.blackness then
      local h, w, b = self:hwb()
      self:set {h= value.hue or h, w= value.whiteness or w, b= value.backness or b}
      value.hue = nil
    end

    if value.hue or value.saturation or value.value then
      local h, s, v = self:hsv()
      self:set {h= value.hue or h, s= value.saturation or s, v= value.value or v}
    end

    if value.cyan or value.magenta or value.yellow or value.key then
      local c, m, y, k = self:cmyk()
      self:set {
        c = value.cyan or c,
        m = value.magenta or m,
        y = value.yellow or y,
        k = value.key or k
      }
    end
  end

  local r, g, b, a =
    utils.clamp(self.r, 0, 1),
    utils.clamp(self.g, 0, 1),
    utils.clamp(self.b, 0, 1),
    utils.clamp(self.a, 0, 1)
  assert(r and g and b and a, "Color invalid")
  return self
end



--- Get rgb values.
--
-- @treturn number[0;1] red
-- @treturn number[0;1] green
-- @treturn number[0;1] blue
function Color:rgb()
  return self.r, self.g, self.b
end

--- Get rgba values.
--
-- @treturn number[0;1] red
-- @treturn number[0;1] green
-- @treturn number[0;1] blue
-- @treturn number[0;1] alpha
function Color:rgba()
  return self.r, self.g, self.b, self.a
end


function Color:_hsvm()
  local r, g, b = self.r, self.g, self.b

  local max, max_i = utils.max(r, g, b)
  local min = math.min(r, g, b)
  local chroma = max - min

  local hue
  if chroma == 0 then
    hue = 0
  elseif max_i == 1 then
    hue = (    (g - b) / chroma) / 6
  elseif max_i == 2 then
    hue = (2 + (b - r) / chroma) / 6
  elseif max_i == 3 then
    hue = (4 + (r - g) / chroma) / 6
  end

  local saturation = max == 0 and 0 or chroma / max

  return hue % 1, saturation, max, min
end

--- Get hsv values.
--
-- @treturn number[0;1] hue
-- @treturn number[0;1] saturation
-- @treturn number[0;1] value
function Color:hsv()
  local h, s, v = self:_hsvm()
  return h, s, v
end

--- Get hsv values.
--
-- @treturn number[0;1] hue
-- @treturn number[0;1] saturation
-- @treturn number[0;1] value
-- @treturn number[0;1] alpha
function Color:hsva()
  local h, s, v = self:_hsvm()
  return h, s, v, self.a
end


--- Get hsl values.
--
-- @treturn number[0;1] hue
-- @treturn number[0;1] saturation
-- @treturn number[0;1] lightness
function Color:hsl()
  local hue, _, max, min = self:_hsvm()
  local lightness = (max + min) / 2

  local saturation = lightness == 0 and 0
    or (max - lightness) / math.min(lightness, 1 - lightness)

  if saturation ~= saturation then
    saturation = 0
  end

  return hue, saturation, lightness
end

--- Get hsl values.
--
-- @treturn number[0;1] hue
-- @treturn number[0;1] saturation
-- @treturn number[0;1] lightness
-- @treturn number[0;1] alpha
function Color:hsla()
  local h, s, l = self:hsl()
  return h, s, l, self.a
end

--- Get hwb values.
--
-- @treturn number[0;1] hue
-- @treturn number[0;1] whiteness
-- @treturn number[0;1] blackness
function Color:hwb()
  local h, s, v = self:hsv()
  local w = (1 - s) * v
  local b = 1 - v
  return h, w, b
end

--- Get hwb values.
--
-- @treturn number[0;1] hue
-- @treturn number[0;1] whiteness
-- @treturn number[0;1] blackness
-- @treturn number[0;1] alpha
function Color:hwba()
  local h, w, b = self:hwb()
  return h, w, b, self.a
end

--- Get cmyk values.
--
-- @treturn number[0;1] cyan
-- @treturn number[0;1] magenta
-- @treturn number[0;1] yellow
-- @treturn number[0;1] key
function Color:cmyk()
  local r, g, b = self.r, self.g, self.b
  local K = math.max(r, g, b)
  if K == 0 then
     return 0.0, 0.0, 0.0, 1.0
  end
  local k = 1 - K
  local c = (K - r) / K
  local m = (K - g) / K
  local y = (K - b) / K
  return c, m, y, k
end



--- Rotate hue of color.
--
-- @tparam number[0;1]|table value Part of full turn or table containing degree or radians
--
-- @treturn Color self
--
-- @usage color:rotate(0.5)
-- @usage color:rotate {deg=180}
-- @usage color:rotate {rad=math.pi}
function Color:rotate(value)
  local r
  if type(value) == "number" then
    r = value
  elseif value.rad ~= nil then
    r = value.rad / (math.pi * 2)
  elseif value.deg ~= nil then
    r = value.deg / 360
  else
    error("No valid argument")
  end

  local h, s, v = self:hsv()
  h = (h + r) % 1
  self:set {h = h, s = s, v = v, a = self.a}

  return self
end

--- Invert the color.
--
-- @treturn Color self
function Color:invert()
  self.r = 1 - self.r
  self.g = 1 - self.g
  self.b = 1 - self.b
  return self
end

--- Reduce saturation to 0.
--
-- @treturn Color self
function Color:grey()
  local h, _, v = self:hsv()
  self:set {h=h, s=0, v=v, a=self.a}
  return self
end

--- Set to black or white depending on lightness.
--
-- @tparam ?number[0;1] lightness Cutoff point (Default: 0.5)
--
-- @treturn Color self
function Color:blackOrWhite(lightness)
  local _, _, l = self:hsl()
  local v = l > lightness and 1 or 0
  self.r = v
  self.g = v
  self.b = v
  return self
end

--- Mix two colors together.
--
-- @tparam Color other
-- @tparam ?number strength 0 results in self, 1 results in other (Default: 0.5)
--
-- @treturn Color self
function Color:mix(other, strength)
  if strength == nil then strength = 0.5 end
  self.r = self.r * (1 - strength) + other.r * strength
  self.g = self.g * (1 - strength) + other.g * strength
  self.b = self.b * (1 - strength) + other.b * strength
  self.a = self.a * (1 - strength) + other.a * strength
  return self
end



--- Generate complementary color.
--
-- @treturn Color
function Color:complement()
  return Color(self):rotate(0.5)
end

--- Generate analogous color scheme.
--
-- @treturn Color
-- @treturn Color self
-- @treturn Color
function Color:analogous()
  local h, s, v = self:hsv()
  return Color {h = (h - 1/12) % 1, s = s, v = v, a = self.a},
    self,
    Color {h = (h + 1/12) % 1, s = s, v = v, a = self.a}
end

--- Generate triadic color scheme.
--
-- @treturn Color self
-- @treturn Color
-- @treturn Color
function Color:triad()
  local h, s, v = self:hsv()
  return self,
    Color {h = (h + 1/3) % 1, s = s, v = v, a = self.a},
    Color {h = (h + 2/3) % 1, s = s, v = v, a = self.a}
end

--- Generate tetradic color scheme.
--
-- @treturn Color self
-- @treturn Color
-- @treturn Color
-- @treturn Color
function Color:tetrad()
  local h, s, v = self:hsv()
  return self,
    Color {h = (h + 1/4) % 1, s = s, v = v, a = self.a},
    Color {h = (h + 2/4) % 1, s = s, v = v, a = self.a},
    Color {h = (h + 3/4) % 1, s = s, v = v, a = self.a}
end

--- Generate compound color scheme.
--
-- @treturn Color
-- @treturn Color self
-- @treturn Color
function Color:compound()
  local ca, _, cb = self:complement():analogous()
  return ca, self, cb
end

--- Generate evenly spaced color scheme.
-- <br>
-- Generalization of `triad` and `tetrad`.
--
-- @tparam int     n Return n colors
-- @tparam ?number r Space colors over r rotations (Default: 1)
--
-- @treturn {Color,...} Table with n colors including self at index 1
function Color:evenlySpaced(n, r)
  assert(n > 0, "n needs to be greater than 0")
  r = r or 1

  local res = {self}

  local rot = r / n
  local h, s, v = self:hsv()
  local a = self.a

  for i = 1, n - 1 do
    h = (h + rot) % 1
    table.insert(res, Color {h=h, s=s, v=v, a=a})
  end

  return res
end



--- Get string representation of color.
--
-- If `format` is `nil`, `color:tostring()` is the same as `tostring(color)`.
--
-- @tparam ?string format One of: `#fff`, `#ffff`, `#ffffff`, `#ffffffff`,
--  rgb, rgba, hsv, hsva, hsl, hsla, hwb, hwba, ncol, cmyk
--
-- @treturn string
--
-- @see Color:__tostring
function Color:tostring(format)
  if format == nil then return tostring(self) end

  format = format:lower()

  if format:sub(1,1) == "#" then
    if #format == 4 then
      return string.format("#%x%x%x", utils.round(self.r * 0xf),
        utils.round(self.g * 0xf), utils.round(self.b * 0xf))
    elseif #format == 5 then
      return string.format("#%x%x%x%x", utils.round(self.r * 0xf),
        utils.round(self.g * 0xf), utils.round(self.b * 0xf), utils.round(self.a * 0xf))
    elseif #format == 7 then
      return string.format("#%02x%02x%02x", utils.round(self.r * 0xff),
        utils.round(self.g * 0xff), utils.round(self.b * 0xff))
    elseif #format == 9 then
      return string.format("#%02x%02x%02x%02x", utils.round(self.r * 0xff),
        utils.round(self.g * 0xff), utils.round(self.b * 0xff), utils.round(self.a * 0xff))
    end
  elseif format == "rgb" then
    return string.format("rgb(%d, %d, %d)",
      utils.round(self.r * 0xff),
      utils.round(self.g * 0xff),
      utils.round(self.b * 0xff))
  elseif format == "rgba" then
    return string.format("rgba(%d, %d, %d, %s)",
      utils.round(self.r * 0xff),
      utils.round(self.g * 0xff),
      utils.round(self.b * 0xff), self.a)
  elseif format == "hsv" then
    local h, s, v = self:hsv()
    return string.format("hsv(%d, %d%%, %d%%)",
      utils.round(h * 360),
      utils.round(s * 100),
      utils.round(v * 100))
  elseif format == "hsva" then
    local h, s, v, a = self:hsva()
    return string.format("hsva(%d, %d%%, %d%%, %s)",
      utils.round(h * 360),
      utils.round(s * 100),
      utils.round(v * 100), a)
  elseif format == "hsl" then
    local h, s, l = self:hsl()
    return string.format("hsl(%d, %d%%, %d%%)",
      utils.round(h * 360),
      utils.round(s * 100),
      utils.round(l * 100))
  elseif format == "hsla" then
    local h, s, l, a = self:hsla()
    return string.format("hsla(%d, %d%%, %d%%, %s)",
      utils.round(h * 360),
      utils.round(s * 100),
      utils.round(l * 100), a)
  elseif format == "hwb" then
    local h, w, b = self:hwb()
    return string.format("hwb(%d, %d%%, %d%%)",
      utils.round(h * 360),
      utils.round(w * 100),
      utils.round(b * 100))
  elseif format == "hwba" then
    local h, w, b, a = self:hwba()
    return string.format("hwba(%d, %d%%, %d%%, %s)",
      utils.round(h * 360),
      utils.round(w * 100),
      utils.round(b * 100), a)
  elseif format == "ncol" then
    local h, w, b = self:hwb()
    local h_maj, h_min = math.modf(h * 6)
    h_maj = h_maj % 6

    local col
    if h_maj == 0 then col = "R"
    elseif h_maj == 1 then col = "Y"
    elseif h_maj == 2 then col = "G"
    elseif h_maj == 3 then col = "C"
    elseif h_maj == 4 then col = "B"
    else col = "M" end

    return string.format("%s%d, %d%%, %d%%",
      col, utils.round(h_min * 100),
      utils.round(w * 100),
      utils.round(b * 100))
  elseif format == "cmyk" then
    local c, m, y, k = self:cmyk()
    return string.format("cymk(%d%%, %d%%, %d%%, %d%%)",
      utils.round(c * 100),
      utils.round(m * 100),
      utils.round(y * 100),
      utils.round(k * 100))
  end

  return tostring(self)
end



--- Get color in rgb hex notation.
-- <br>
-- only adds alpha value if `color.a < 1`
--
-- @treturn string `#rrggbb` | `#rrggbbaa`
--
-- @see Color:tostring
function Color:__tostring()
  if self.a < 1 then
    return string.format(
      "#%02x%02x%02x%02x",
      utils.round(self.r * 0xff),
      utils.round(self.g * 0xff),
      utils.round(self.b * 0xff),
      utils.round(self.a * 0xff)
    )
  else
    return string.format(
      "#%02x%02x%02x",
      utils.round(self.r * 0xff),
      utils.round(self.g * 0xff),
      utils.round(self.b * 0xff)
    )
  end
end

--- Check if colors are equal.
--
-- @tparam Color other
--
-- @treturn boolean all values are equal
function Color:__eq(other)
  return self.r == other.r
    and self.g == other.g
    and self.b == other.b
    and self.a == other.a
end

--- Checks whether color is darker.
--
-- @tparam Color other
--
-- @treturn boolean self is darker than other
function Color:__lt(other)
  local _, _, la = self:hsl()
  local _, _, lb = other:hsl()
  return la < lb
end

--- Checks whether color is as dark or darker.
--
-- @tparam Color other
--
-- @treturn boolean self is as dark or darker than other
function Color:__le(other)
  local _, _, la = self:hsl()
  local _, _, lb = other:hsl()
  return la <= lb
end

--- Iterate through color.
--
-- Iterates through r, g, b, and a.
function Color:__pairs()
  local function iter(tbl, k)
    if k == nil then
      return "r", self.r
    elseif k == "r" then
      return "g", self.g
    elseif k == "g" then
      return "b", self.b
    elseif k == "b" then
      return "a", self.a
    end
  end

  return iter, self, nil
end

--- Get inverted clone of color.
--
-- @treturn Color
function Color:__unm()
  return Color(self):invert()
end

--- Mix two colors evenly.
--
-- @tparam Color a first color
-- @tparam Color b second color
--
-- @treturn Color new color
--
-- @see Color:mix
function Color.__add(a, b)
  assert(Color.isColor(a) and Color.isColor(b), "Can only add two colors.")
  return Color(a):mix(b)
end

--- Complement of even mix.
--
-- @tparam Color a first color
-- @tparam Color b second color
--
-- @treturn Color new color
--
-- @see Color:mix
-- @see Color.__add
function Color.__sub(a, b)
  assert(Color.isColor(a) and Color.isColor(b), "Can only add two colors.")
  return Color(a):mix(b):rotate(0.5)
end

--- Apply rgb mask to color.
--
-- @tparam Color|number a color or mask
-- @tparam Color|number b color or mask (if a and b are colors b is used as mask)
--
-- @treturn Color new color
--
-- @usage local new_col = color & 0xff00ff -- get new color without the green channel
function Color.__band(a, b)
  local color, mask
  if Color.isColor(a) and type(b) == "number" then
    color = a
    mask = b
  elseif Color.isColor(b) and type(a) == "number" then
    color = b
    mask = a
  elseif Color.isColor(a) and Color.isColor(b) then
    color = a
    mask = bit_lshift(utils.round(b.r * 0xff), 16)
      + bit_lshift(utils.round(b.g * 0xff), 8)
      + utils.round(b.b * 0xff)
  else
    error("Required arguments: Color|number,Color|number Received: "..type(a)..","..type(b))
  end

  return Color {
    bit_and(utils.round(color.r * 0xff), bit_rshift(mask, 16)) / 0xff,
    bit_and(utils.round(color.g * 0xff), bit_rshift(mask,  8)) / 0xff,
    bit_and(utils.round(color.b * 0xff), mask                ) / 0xff,
    color.a
  }
end

--- Apply rgb mask to color, providing backwards compatibility for Lua 5.1 and LuaJIT 2.1.0-beta3 (e.g. inside Neovim), which don't provide native support for bitwise operators.
--
-- @tparam Color|number a color or mask
-- @tparam Color|number b color or mask (if a and b are colors b is used as mask)
--
-- @treturn Color new color
--
-- @usage local new_col = Color.band(color, 0xff00ff) -- get new color without the green channel
function Color.band(a, b)
    return Color.__band(a, b)
end


--- Check whether `color` is a Color.
--
-- @param color
--
-- @treturn boolean is a color
--
-- @usage if Color.isColor(color) then print "It's a color!" end
function Color.isColor(color)
  return color ~= nil and color.__is_color == true
end

-- Attempts to create a Color object using `str` color string.
-- 
-- @treturn boolean is a valid color string, Color resulting color object (or Error if it isn't).
function Color.tryColor(str)
  return pcall(Color, str)
end

Color.colorNames = colors_css


return Color