///|
/// Converts a color to HSV components: hue in radians, saturation/value in [0, 1].
pub fn Color::to_hsv(self : Color) -> (Double, Double, Double) {
  let maximum = self.r.max(self.g).max(self.b)
  let minimum = self.r.min(self.g).min(self.b)
  let delta = maximum - minimum
  let hue = if delta == 0.0 {
    0.0
  } else if maximum == self.r {
    2.0 * @math.PI * ((self.g - self.b) / delta / 6.0)
  } else if maximum == self.g {
    2.0 * @math.PI * (1.0 / 3.0 + (self.b - self.r) / delta / 6.0)
  } else {
    2.0 * @math.PI * (2.0 / 3.0 + (self.r - self.g) / delta / 6.0)
  }
  let saturation = if maximum == 0.0 { 0.0 } else { delta / maximum }
  (hue, saturation, maximum)
}

///|
pub fn Color::hsv(
  hue : Double,
  saturation : Double,
  value : Double,
  alpha? : Double = 1.0,
) -> Color {
  let h = (hue / (2.0 * @math.PI)).abs()
  let sector = (h * 6.0).to_int()
  let fraction = h * 6.0 - sector.to_double()
  let p = value * (1.0 - saturation)
  let q = value * (1.0 - fraction * saturation)
  let t = value * (1.0 - (1.0 - fraction) * saturation)
  let rgb = match sector % 6 {
    0 => (value, t, p)
    1 => (q, value, p)
    2 => (p, value, t)
    3 => (p, q, value)
    4 => (t, p, value)
    _ => (value, p, q)
  }
  Color::rgba(rgb.0, rgb.1, rgb.2, a=alpha)
}

///|
pub fn Color::grayscale(self : Color) -> Color {
  let value = self.luminance()
  Color::rgba(value, value, value, a=self.a)
}

///|
pub fn Color::saturate(self : Color, amount : Double) -> Color {
  let (hue, saturation, value) = self.to_hsv()
  Color::hsv(hue, (saturation + amount).max(0.0).min(1.0), value, alpha=self.a)
}

///|
pub fn Color::brighten(self : Color, amount : Double) -> Color {
  let (hue, saturation, value) = self.to_hsv()
  Color::hsv(hue, saturation, (value + amount).max(0.0).min(1.0), alpha=self.a)
}

///|
pub fn Color::invert(self : Color) -> Color {
  Color::rgba(1.0 - self.r, 1.0 - self.g, 1.0 - self.b, a=self.a)
}

///|
pub fn triadic_palette(base : Color) -> Palette {
  let (hue, saturation, value) = base.to_hsv()
  Palette::new("triadic", [
    base,
    Color::hsv(hue + 2.0 * @math.PI / 3.0, saturation, value),
    Color::hsv(hue + 4.0 * @math.PI / 3.0, saturation, value),
  ])
}

///|
pub fn split_complementary_palette(base : Color) -> Palette {
  let (hue, saturation, value) = base.to_hsv()
  Palette::new("split-complementary", [
    base,
    Color::hsv(hue + 5.0 * @math.PI / 6.0, saturation, value),
    Color::hsv(hue + 7.0 * @math.PI / 6.0, saturation, value),
  ])
}

///|
pub fn thermal_palette(samples : Int) -> Palette {
  let colors = []
  for i in 0..