///|
/// An integer RGB value used for deterministic token analysis.
pub struct RgbColor {
red : Int
green : Int
blue : Int
} derive(Debug, Eq)
///|
pub fn RgbColor::new(red : Int, green : Int, blue : Int) -> RgbColor {
{
red: clamp_channel(red),
green: clamp_channel(green),
blue: clamp_channel(blue),
}
}
///|
pub fn RgbColor::black() -> RgbColor {
RgbColor::new(0, 0, 0)
}
///|
pub fn RgbColor::white() -> RgbColor {
RgbColor::new(255, 255, 255)
}
///|
pub fn RgbColor::from_hex(value : String) -> RgbColor? {
let text = value.trim().to_owned()
let body = if text.has_prefix("#") { text[1:].to_owned() } else { text }
if body.length() == 3 {
let r = rgb_hex_digit(body[0])
let g = rgb_hex_digit(body[1])
let b = rgb_hex_digit(body[2])
match (r, g, b) {
(Some(red), Some(green), Some(blue)) =>
Some(RgbColor::new(red * 17, green * 17, blue * 17))
_ => None
}
} else if body.length() == 6 {
let r1 = rgb_hex_digit(body[0])
let r2 = rgb_hex_digit(body[1])
let g1 = rgb_hex_digit(body[2])
let g2 = rgb_hex_digit(body[3])
let b1 = rgb_hex_digit(body[4])
let b2 = rgb_hex_digit(body[5])
match (r1, r2, g1, g2, b1, b2) {
(Some(a), Some(b), Some(c), Some(d), Some(e), Some(f)) =>
Some(RgbColor::new(a * 16 + b, c * 16 + d, e * 16 + f))
_ => None
}
} else {
None
}
}
///|
pub fn RgbColor::to_hex(self : RgbColor) -> String {
"#" + hex_pair(self.red) + hex_pair(self.green) + hex_pair(self.blue)
}
///|
pub fn RgbColor::to_css_rgb(self : RgbColor) -> String {
"rgb(" +
self.red.to_string() +
", " +
self.green.to_string() +
", " +
self.blue.to_string() +
")"
}
///|
pub fn RgbColor::channel(self : RgbColor, name : String) -> Int? {
match normalize_identifier(name) {
"red" => Some(self.red)
"green" => Some(self.green)
"blue" => Some(self.blue)
_ => None
}
}
///|
pub fn RgbColor::is_light(self : RgbColor) -> Bool {
self.luminance_x10000() >= 2500
}
///|
pub fn RgbColor::is_dark(self : RgbColor) -> Bool {
!self.is_light()
}
///|
pub fn RgbColor::is_grayscale(self : RgbColor) -> Bool {
self.red == self.green && self.green == self.blue
}
///|
pub fn RgbColor::inverted(self : RgbColor) -> RgbColor {
RgbColor::new(255 - self.red, 255 - self.green, 255 - self.blue)
}
///|
pub fn RgbColor::complementary(self : RgbColor) -> RgbColor {
self.inverted()
}
///|
pub fn RgbColor::lighten(self : RgbColor, percent : Int) -> RgbColor {
self.blend(RgbColor::white(), percent)
}
///|
pub fn RgbColor::darken(self : RgbColor, percent : Int) -> RgbColor {
self.blend(RgbColor::black(), percent)
}
///|
pub fn RgbColor::blend(
self : RgbColor,
other : RgbColor,
percent : Int,
) -> RgbColor {
let ratio = clamp_percent(percent)
let inverse = 100 - ratio
RgbColor::new(
(self.red * inverse + other.red * ratio + 50) / 100,
(self.green * inverse + other.green * ratio + 50) / 100,
(self.blue * inverse + other.blue * ratio + 50) / 100,
)
}
///|
pub fn RgbColor::average(self : RgbColor, other : RgbColor) -> RgbColor {
self.blend(other, 50)
}
///|
pub fn RgbColor::distance_to(self : RgbColor, other : RgbColor) -> Int {
let red = self.red - other.red
let green = self.green - other.green
let blue = self.blue - other.blue
integer_sqrt(red * red + green * green + blue * blue)
}
///|
pub fn RgbColor::luminance_x10000(self : RgbColor) -> Int {
(
2126 * linear_channel(self.red) +
7152 * linear_channel(self.green) +
722 * linear_channel(self.blue)
) /
10000
}
///|
pub fn RgbColor::contrast_ratio_x100(self : RgbColor, other : RgbColor) -> Int {
let first = self.luminance_x10000() + 1000
let second = other.luminance_x10000() + 1000
if first > second {
first * 100 / second
} else {
second * 100 / first
}
}
///|
pub fn RgbColor::with_red(self : RgbColor, red : Int) -> RgbColor {
RgbColor::new(red, self.green, self.blue)
}
///|
pub fn RgbColor::with_green(self : RgbColor, green : Int) -> RgbColor {
RgbColor::new(self.red, green, self.blue)
}
///|
pub fn RgbColor::with_blue(self : RgbColor, blue : Int) -> RgbColor {
RgbColor::new(self.red, self.green, blue)
}
///|
pub fn RgbColor::same_as(self : RgbColor, other : RgbColor) -> Bool {
self == other
}
///|
pub fn RgbColor::describe(self : RgbColor) -> String {
self.to_hex() + " (" + self.to_css_rgb() + ")"
}
///|
pub fn ColorToken::rgb(self : ColorToken) -> RgbColor? {
RgbColor::from_hex(self.value)
}
///|
pub fn ColorToken::lighten(self : ColorToken, percent : Int) -> ColorToken {
match self.rgb() {
Some(color) => { ..self, value: color.lighten(percent).to_hex() }
None => self
}
}
///|
pub fn ColorToken::darken(self : ColorToken, percent : Int) -> ColorToken {
match self.rgb() {
Some(color) => { ..self, value: color.darken(percent).to_hex() }
None => self
}
}
///|
pub fn ColorToken::blend_with(
self : ColorToken,
other : ColorToken,
percent : Int,
) -> ColorToken {
match (self.rgb(), other.rgb()) {
(Some(first), Some(second)) =>
{ ..self, value: first.blend(second, percent).to_hex() }
_ => self
}
}
///|
pub fn Palette::color_named(self : Palette, name : String) -> ColorToken? {
for color in self.colors {
if color.name == name {
return Some(color)
}
}
None
}
///|
pub fn Palette::nearest_color(self : Palette, value : RgbColor) -> ColorToken? {
let mut best : ColorToken? = None
let mut best_distance = 1000000
for color in self.colors {
match color.rgb() {
Some(candidate) => {
let distance = candidate.distance_to(value)
if distance < best_distance {
best_distance = distance
best = Some(color)
}
}
None => continue
}
}
best
}
///|
pub fn Palette::light_variant(self : Palette, percent : Int) -> Palette {
{
colors: self.colors.map(fn(color) { color.lighten(percent) }),
gradients: self.gradients,
}
}
///|
pub fn Palette::dark_variant(self : Palette, percent : Int) -> Palette {
{
colors: self.colors.map(fn(color) { color.darken(percent) }),
gradients: self.gradients,
}
}
///|
pub fn Palette::color_names(self : Palette) -> Array[String] {
self.colors.map(fn(color) { color.name })
}
///|
pub fn Palette::hex_values(self : Palette) -> Array[String] {
self.colors.map(fn(color) { color.value })
}
///|
pub fn Palette::light_colors(self : Palette) -> Array[ColorToken] {
self.colors.filter(fn(color) {
match color.rgb() {
Some(value) => value.is_light()
None => false
}
})
}
///|
pub fn Palette::dark_colors(self : Palette) -> Array[ColorToken] {
self.colors.filter(fn(color) {
match color.rgb() {
Some(value) => value.is_dark()
None => false
}
})
}
///|
fn clamp_channel(value : Int) -> Int {
if value < 0 {
0
} else if value > 255 {
255
} else {
value
}
}
///|
fn clamp_percent(value : Int) -> Int {
if value < 0 {
0
} else if value > 100 {
100
} else {
value
}
}
///|
fn rgb_hex_digit(ch : UInt16) -> Int? {
if ch >= '0' && ch <= '9' {
Some(ch.to_int() - ('0' : UInt16).to_int())
} else if ch >= 'A' && ch <= 'F' {
Some(ch.to_int() - ('A' : UInt16).to_int() + 10)
} else if ch >= 'a' && ch <= 'f' {
Some(ch.to_int() - ('a' : UInt16).to_int() + 10)
} else {
None
}
}
///|
fn hex_pair(value : Int) -> String {
let safe = clamp_channel(value)
hex_char(safe / 16) + hex_char(safe % 16)
}
///|
fn hex_char(value : Int) -> String {
match value {
0 => "0"
1 => "1"
2 => "2"
3 => "3"
4 => "4"
5 => "5"
6 => "6"
7 => "7"
8 => "8"
9 => "9"
10 => "A"
11 => "B"
12 => "C"
13 => "D"
14 => "E"
_ => "F"
}
}
///|
fn linear_channel(value : Int) -> Int {
let square = value * value
square * 10000 / (255 * 255)
}
///|
fn integer_sqrt(value : Int) -> Int {
if value <= 0 {
0
} else {
let mut guess = value
for _ in 0..<16 {
let next = (guess + value / guess) / 2
if next >= guess {
break
}
guess = next
}
guess
}
}