///|
/// RGBA color in non-premultiplied form.
/// r/g/b are integers in 0..=255, a is a Double in 0.0..=1.0.
pub struct Color {
r : Int
g : Int
b : Int
a : Double
} derive(Eq)
///|
pub impl Show for Color with fn output(self, logger) {
logger.write_string("Color { r: ")
logger.write_object(self.r)
logger.write_string(", g: ")
logger.write_object(self.g)
logger.write_string(", b: ")
logger.write_object(self.b)
logger.write_string(", a: ")
logger.write_object(self.a)
logger.write_string(" }")
}
///|
pub fn Color::rgb(r : Int, g : Int, b : Int) -> Color {
{ r, g, b, a: 1.0 }
}
///|
pub fn Color::rgba(r : Int, g : Int, b : Int, a : Double) -> Color {
{ r, g, b, a }
}
///|
pub fn Color::black() -> Color {
{ r: 0, g: 0, b: 0, a: 1.0 }
}
///|
pub fn Color::white() -> Color {
{ r: 255, g: 255, b: 255, a: 1.0 }
}
///|
pub fn Color::transparent() -> Color {
{ r: 0, g: 0, b: 0, a: 0.0 }
}
///|
pub suberror ColorParseError {
ColorParseError(String)
}
///|
pub impl Show for ColorParseError with fn output(self, logger) {
let ColorParseError(msg) = self
logger.write_string("ColorParseError(")
logger.write_string(msg)
logger.write_string(")")
}
///|
fn hex_nibble(ch : Char) -> Int raise ColorParseError {
let code = ch.to_int()
if code >= 0x30 && code <= 0x39 {
code - 0x30
} else if code >= 0x41 && code <= 0x46 {
code - 0x41 + 10
} else if code >= 0x61 && code <= 0x66 {
code - 0x61 + 10
} else {
raise ColorParseError("invalid hex digit: " + ch.to_string())
}
}
///|
fn char_at(src : String, i : Int) -> Char raise ColorParseError {
match src.get_char(i) {
Some(c) => c
None => raise ColorParseError("index out of bounds: " + src)
}
}
///|
fn parse_hex(src : String) -> Color raise ColorParseError {
// src begins with '#'. Supported lengths: 4 (#rgb), 5 (#rgba), 7 (#rrggbb), 9 (#rrggbbaa).
let len = src.length()
match len {
4 => {
let r = hex_nibble(char_at(src, 1)) * 17
let g = hex_nibble(char_at(src, 2)) * 17
let b = hex_nibble(char_at(src, 3)) * 17
{ r, g, b, a: 1.0 }
}
5 => {
let r = hex_nibble(char_at(src, 1)) * 17
let g = hex_nibble(char_at(src, 2)) * 17
let b = hex_nibble(char_at(src, 3)) * 17
let a_i = hex_nibble(char_at(src, 4)) * 17
{ r, g, b, a: a_i.to_double() / 255.0 }
}
7 => {
let r = hex_nibble(char_at(src, 1)) * 16 + hex_nibble(char_at(src, 2))
let g = hex_nibble(char_at(src, 3)) * 16 + hex_nibble(char_at(src, 4))
let b = hex_nibble(char_at(src, 5)) * 16 + hex_nibble(char_at(src, 6))
{ r, g, b, a: 1.0 }
}
9 => {
let r = hex_nibble(char_at(src, 1)) * 16 + hex_nibble(char_at(src, 2))
let g = hex_nibble(char_at(src, 3)) * 16 + hex_nibble(char_at(src, 4))
let b = hex_nibble(char_at(src, 5)) * 16 + hex_nibble(char_at(src, 6))
let a_i = hex_nibble(char_at(src, 7)) * 16 + hex_nibble(char_at(src, 8))
{ r, g, b, a: a_i.to_double() / 255.0 }
}
_ => raise ColorParseError("unsupported hex length: " + src)
}
}
///|
fn skip_ws(src : String, i : Int) -> Int {
let mut j = i
let len = src.length()
while j < len {
match src.get_char(j) {
Some(' ') | Some('\t') | Some('\n') | Some('\r') => j = j + 1
_ => break
}
}
j
}
///|
fn parse_double_token(
src : String,
start : Int,
) -> (Double, Int) raise ColorParseError {
let mut i = skip_ws(src, start)
let begin = i
let len = src.length()
while i < len {
match src.get_char(i) {
Some('0')
| Some('1')
| Some('2')
| Some('3')
| Some('4')
| Some('5')
| Some('6')
| Some('7')
| Some('8')
| Some('9')
| Some('.')
| Some('-')
| Some('+') => i = i + 1
_ => break
}
}
if begin == i {
raise ColorParseError("expected number at position " + begin.to_string())
}
try @string.parse_double(src[begin:i]) catch {
_ =>
raise ColorParseError("invalid number at position " + begin.to_string())
} noraise {
v => (v, skip_ws(src, i))
}
}
///|
fn expect_char(src : String, i : Int, ch : Char) -> Int raise ColorParseError {
let j = skip_ws(src, i)
match src.get_char(j) {
Some(c) if c == ch => skip_ws(src, j + 1)
_ =>
raise ColorParseError(
"expected '" + ch.to_string() + "' at position " + j.to_string(),
)
}
}
///|
fn parse_rgb_like(
src : String,
with_alpha : Bool,
) -> Color raise ColorParseError {
let len = src.length()
let mut i = 0
let mut found = false
while i < len {
match src.get_char(i) {
Some('(') => {
found = true
break
}
_ => i = i + 1
}
}
if !found {
raise ColorParseError("missing '(' in color: " + src)
}
i = i + 1
let (r, i1) = parse_double_token(src, i)
let i2 = expect_char(src, i1, ',')
let (g, i3) = parse_double_token(src, i2)
let i4 = expect_char(src, i3, ',')
let (b, i5) = parse_double_token(src, i4)
let (a, i_end) = if with_alpha {
let i6 = expect_char(src, i5, ',')
let (av, i7) = parse_double_token(src, i6)
(av, i7)
} else {
(1.0, i5)
}
let after_close = expect_char(src, i_end, ')')
if after_close != src.length() {
raise ColorParseError("trailing garbage after ')' in color: " + src)
}
{ r: r.to_int(), g: g.to_int(), b: b.to_int(), a }
}
///|
fn named_color(name : String) -> Color? {
match name {
"black" => Some({ r: 0, g: 0, b: 0, a: 1.0 })
"white" => Some({ r: 255, g: 255, b: 255, a: 1.0 })
"red" => Some({ r: 255, g: 0, b: 0, a: 1.0 })
"green" => Some({ r: 0, g: 128, b: 0, a: 1.0 })
"blue" => Some({ r: 0, g: 0, b: 255, a: 1.0 })
"yellow" => Some({ r: 255, g: 255, b: 0, a: 1.0 })
"cyan" => Some({ r: 0, g: 255, b: 255, a: 1.0 })
"magenta" => Some({ r: 255, g: 0, b: 255, a: 1.0 })
"gray" => Some({ r: 128, g: 128, b: 128, a: 1.0 })
"silver" => Some({ r: 192, g: 192, b: 192, a: 1.0 })
"maroon" => Some({ r: 128, g: 0, b: 0, a: 1.0 })
"olive" => Some({ r: 128, g: 128, b: 0, a: 1.0 })
"lime" => Some({ r: 0, g: 255, b: 0, a: 1.0 })
"teal" => Some({ r: 0, g: 128, b: 128, a: 1.0 })
"navy" => Some({ r: 0, g: 0, b: 128, a: 1.0 })
"purple" => Some({ r: 128, g: 0, b: 128, a: 1.0 })
"transparent" => Some({ r: 0, g: 0, b: 0, a: 0.0 })
_ => None
}
}
///|
pub fn Color::parse(css : String) -> Color raise ColorParseError {
match css.get_char(0) {
None => raise ColorParseError("empty color string")
Some('#') => parse_hex(css)
Some(_) =>
if css.has_prefix("rgba(") {
parse_rgb_like(css, true)
} else if css.has_prefix("rgb(") {
parse_rgb_like(css, false)
} else {
match named_color(css) {
Some(c) => c
None => raise ColorParseError("unsupported color: " + css)
}
}
}
}