// Colours: parsing (d3-color 3.1), RGB interpolation and piecewise
// interpolators (d3-interpolate 3.0), as inlined in vega-scale (ISC license).
///|
/// An sRGB colour with fractional channels, as d3-color's `Rgb`.
pub struct Rgb {
r : Double
g : Double
b : Double
opacity : Double
}
///|
fn clampi(v : Double) -> Double {
// Math.max(0, Math.min(255, Math.round(value) || 0))
let r = @util.js_round(v)
let r2 = if r.is_nan() || r == 0.0 { 0.0 } else { r }
@util.js_max(0.0, @util.js_min(255.0, r2))
}
///|
fn clampa(opacity : Double) -> Double {
if opacity.is_nan() {
1.0
} else {
@util.js_max(0.0, @util.js_min(1.0, opacity))
}
}
///|
/// `rgb(r, g, b)` (or `rgba(r, g, b, a)`), channels rounded and clamped, as
/// d3-color's `formatRgb`.
pub fn Rgb::format_rgb(self : Rgb) -> String {
let a = clampa(self.opacity)
let ch = fn(v : Double) { @util.js_number_to_string(clampi(v)) }
if a == 1.0 {
"rgb(" + ch(self.r) + ", " + ch(self.g) + ", " + ch(self.b) + ")"
} else {
"rgba(" +
ch(self.r) +
", " +
ch(self.g) +
", " +
ch(self.b) +
", " +
@util.js_number_to_string(a) +
")"
}
}
///|
fn hex_digit(c : Char) -> Int32 {
if c >= '0' && c <= '9' {
c.to_int() - '0'.to_int()
} else if c >= 'a' && c <= 'f' {
c.to_int() - 'a'.to_int() + 10
} else {
-1
}
}
///|
fn rgb_of_int(n : Int32) -> Rgb {
{
r: ((n >> 16) & 0xff).to_double(),
g: ((n >> 8) & 0xff).to_double(),
b: (n & 0xff).to_double(),
opacity: 1.0,
}
}
///|
/// Parse a CSS colour: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb(r, g, b)`,
/// `rgba(r, g, b, a)` (integers or percentages) and the CSS colour keywords.
pub fn parse_color(text : String) -> Rgb? {
let s = text.trim().to_owned().to_lower()
if s.has_prefix("#") {
let cs = s.to_array()
let n = cs.length() - 1
let mut v = 0
for i in 1.. Some(rgb_of_int(v))
3 =>
Some({
r: (((v >> 8) & 0xf) * 17).to_double(),
g: (((v >> 4) & 0xf) * 17).to_double(),
b: ((v & 0xf) * 17).to_double(),
opacity: 1.0,
})
8 =>
Some({
r: ((v >> 24) & 0xff).to_double(),
g: ((v >> 16) & 0xff).to_double(),
b: ((v >> 8) & 0xff).to_double(),
opacity: (v & 0xff).to_double() / 255.0,
})
4 =>
Some({
r: (((v >> 12) & 0xf) * 17).to_double(),
g: (((v >> 8) & 0xf) * 17).to_double(),
b: (((v >> 4) & 0xf) * 17).to_double(),
opacity: ((v & 0xf) * 17).to_double() / 255.0,
})
_ => None
}
}
if s.has_prefix("rgb(") || s.has_prefix("rgba(") {
return parse_rgb_function(s)
}
if s == "transparent" {
return Some({
r: @double.not_a_number,
g: @double.not_a_number,
b: @double.not_a_number,
opacity: 0.0,
})
}
match named_color(s) {
Some(n) => Some(rgb_of_int(n))
None => None
}
}
///|
fn parse_rgb_function(s : String) -> Rgb? {
let open = s.find("(").unwrap_or(0)
if !s.has_suffix(")") {
return None
}
let inner = s.unsafe_substring(start=open + 1, end=s.length() - 1)
let parts : Array[String] = inner
.split(",")
.map(fn(p) { p.trim().to_owned() })
.collect()
let has_alpha = s.has_prefix("rgba(")
if parts.length() != (if has_alpha { 4 } else { 3 }) {
return None
}
let chans : Array[Double] = []
let mut percent = false
for i in 0..<3 {
let p = parts[i]
if p.has_suffix("%") {
percent = true
let v = @value.string_to_number(
p.unsafe_substring(start=0, end=p.length() - 1),
)
if v.is_nan() {
return None
}
chans.push(v * 255.0 / 100.0)
} else {
let v = @value.string_to_number(p)
if v.is_nan() {
return None
}
chans.push(v)
}
}
ignore(percent)
let a = if has_alpha {
let v = @value.string_to_number(parts[3])
if v.is_nan() {
return None
}
v
} else {
1.0
}
Some({ r: chans[0], g: chans[1], b: chans[2], opacity: a, })
}
///|
fn lerp_channel(a : Double, b : Double) -> (Double) -> Double {
let d = b - a
if d != 0.0 {
fn(t) { a + t * d }
} else {
let c = if a.is_nan() { b } else { a }
fn(_) { c }
}
}
///|
/// d3's `interpolateRgb(a, b)` (gamma 1): a function from t in [0, 1] to
/// an `rgb(...)` string. Unparsable colours interpolate as NaN channels.
pub fn interpolate_rgb(a : String, b : String) -> (Double) -> String {
let nan = @double.not_a_number
let ca = parse_color(a).unwrap_or({ r: nan, g: nan, b: nan, opacity: 1.0, })
let cb = parse_color(b).unwrap_or({ r: nan, g: nan, b: nan, opacity: 1.0, })
let r = lerp_channel(ca.r, cb.r)
let g = lerp_channel(ca.g, cb.g)
let bl = lerp_channel(ca.b, cb.b)
let o = lerp_channel(ca.opacity, cb.opacity)
fn(t) { { r: r(t), g: g(t), b: bl(t), opacity: o(t), }.format_rgb() }
}
///|
/// d3's `piecewise(interpolateRgb, colors)` as built by vega-scale's
/// `interpolateColors`: spreads the colours evenly over t in [0, 1].
pub fn interpolate_colors(colors : Array[String]) -> (Double) -> String {
let n = colors.length() - 1
let pieces : Array[(Double) -> String] = []
for i in 0.. String,
count : Int32,
) -> Array[String] {
let out : Array[String] = []
let n = (count + 1).to_double()
for i in 0.. hi).
pub fn interpolate_range(
interpolator : (Double) -> String,
lo : Double,
hi : Double,
) -> (Double) -> String {
let span = hi - lo
fn(i) { interpolator(lo + i * span) }
}