use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8, }
impl Color {
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
pub const fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
#[deprecated(since = "0.6.0", note = "renamed: use `Color::from_rgb(r, g, b)`")]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self::from_rgb(r, g, b)
}
#[deprecated(since = "0.6.0", note = "renamed: use `Color::from_rgba(r, g, b, a)`")]
pub const fn new_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self::from_rgba(r, g, b, a)
}
pub fn hex(hex: &str) -> Option<Self> {
Self::from_hex(hex).ok()
}
pub fn named(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"red" => Some(Self::RED),
"green" => Some(Self::GREEN),
"blue" => Some(Self::BLUE),
"yellow" => Some(Self::YELLOW),
"orange" => Some(Self::ORANGE),
"purple" => Some(Self::PURPLE),
"cyan" => Some(Self::CYAN),
"magenta" => Some(Self::MAGENTA),
"black" => Some(Self::BLACK),
"white" => Some(Self::WHITE),
"gray" | "grey" => Some(Self::GRAY),
"lightgray" | "lightgrey" | "light_gray" | "light_grey" => Some(Self::LIGHT_GRAY),
"darkgray" | "darkgrey" | "dark_gray" | "dark_grey" => Some(Self::DARK_GRAY),
"pink" => Some(Self::from_rgb(255, 192, 203)),
"brown" => Some(Self::from_rgb(139, 69, 19)),
"lime" => Some(Self::from_rgb(0, 255, 0)),
"navy" => Some(Self::from_rgb(0, 0, 128)),
"teal" => Some(Self::from_rgb(0, 128, 128)),
"olive" => Some(Self::from_rgb(128, 128, 0)),
"maroon" => Some(Self::from_rgb(128, 0, 0)),
"aqua" => Some(Self::CYAN),
"fuchsia" => Some(Self::MAGENTA),
"silver" => Some(Self::from_rgb(192, 192, 192)),
"coral" => Some(Self::from_rgb(255, 127, 80)),
"salmon" => Some(Self::from_rgb(250, 128, 114)),
"gold" => Some(Self::from_rgb(255, 215, 0)),
"indigo" => Some(Self::from_rgb(75, 0, 130)),
"violet" => Some(Self::from_rgb(238, 130, 238)),
"crimson" => Some(Self::from_rgb(220, 20, 60)),
_ => None,
}
}
pub fn suggest_named(name: &str) -> Option<&'static str> {
let name_lower = name.to_lowercase();
let known_colors = [
"red", "green", "blue", "yellow", "orange", "purple", "cyan", "magenta", "black",
"white", "gray", "grey", "pink", "brown", "lime", "navy", "teal", "olive", "maroon",
"aqua", "fuchsia", "silver", "coral", "salmon", "gold", "indigo", "violet", "crimson",
];
for color in &known_colors {
if color.starts_with(&name_lower) && color.len() <= name_lower.len() + 2 {
return Some(color);
}
if name_lower.starts_with(color) && name_lower.len() <= color.len() + 2 {
return Some(color);
}
if name_lower.len() == color.len() {
let diff_count = name_lower
.chars()
.zip(color.chars())
.filter(|(a, b)| a != b)
.count();
if diff_count <= 1 {
return Some(color);
}
}
}
None
}
pub fn from_hex(hex: &str) -> Result<Self, ColorError> {
let hex = hex.trim_start_matches('#');
match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
.map_err(|_| ColorError::InvalidHex)?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
.map_err(|_| ColorError::InvalidHex)?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
.map_err(|_| ColorError::InvalidHex)?;
Ok(Self::from_rgb(r, g, b))
}
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| ColorError::InvalidHex)?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| ColorError::InvalidHex)?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| ColorError::InvalidHex)?;
Ok(Self::from_rgb(r, g, b))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| ColorError::InvalidHex)?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| ColorError::InvalidHex)?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| ColorError::InvalidHex)?;
let a = u8::from_str_radix(&hex[6..8], 16).map_err(|_| ColorError::InvalidHex)?;
Ok(Self::from_rgba(r, g, b, a))
}
_ => Err(ColorError::InvalidLength),
}
}
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.a = (alpha.clamp(0.0, 1.0) * 255.0) as u8;
self
}
pub fn to_tiny_skia_color(self) -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(self.r, self.g, self.b, self.a)
}
pub fn to_rgba_f32(self) -> (f32, f32, f32, f32) {
(
self.r as f32 / 255.0,
self.g as f32 / 255.0,
self.b as f32 / 255.0,
self.a as f32 / 255.0,
)
}
#[inline]
pub fn darken(self, factor: f32) -> Self {
let factor = factor.clamp(0.0, 1.0);
let mult = 1.0 - factor;
Self {
r: ((self.r as f32) * mult) as u8,
g: ((self.g as f32) * mult) as u8,
b: ((self.b as f32) * mult) as u8,
a: self.a,
}
}
#[inline]
pub fn lighten(self, factor: f32) -> Self {
let factor = factor.clamp(0.0, 1.0);
Self {
r: (self.r as f32 + (255.0 - self.r as f32) * factor) as u8,
g: (self.g as f32 + (255.0 - self.g as f32) * factor) as u8,
b: (self.b as f32 + (255.0 - self.b as f32) * factor) as u8,
a: self.a,
}
}
#[inline]
pub const fn from_gray(value: u8) -> Self {
Self {
r: value,
g: value,
b: value,
a: 255,
}
}
}
impl Color {
pub const BLACK: Color = Color {
r: 0,
g: 0,
b: 0,
a: 255,
};
pub const WHITE: Color = Color {
r: 255,
g: 255,
b: 255,
a: 255,
};
pub const RED: Color = Color {
r: 255,
g: 0,
b: 0,
a: 255,
};
pub const GREEN: Color = Color {
r: 0,
g: 128,
b: 0,
a: 255,
};
pub const BLUE: Color = Color {
r: 0,
g: 0,
b: 255,
a: 255,
};
pub const YELLOW: Color = Color {
r: 255,
g: 255,
b: 0,
a: 255,
};
pub const ORANGE: Color = Color {
r: 255,
g: 165,
b: 0,
a: 255,
};
pub const PURPLE: Color = Color {
r: 128,
g: 0,
b: 128,
a: 255,
};
pub const CYAN: Color = Color {
r: 0,
g: 255,
b: 255,
a: 255,
};
pub const MAGENTA: Color = Color {
r: 255,
g: 0,
b: 255,
a: 255,
};
pub const GRAY: Color = Color {
r: 128,
g: 128,
b: 128,
a: 255,
};
pub const LIGHT_GRAY: Color = Color {
r: 211,
g: 211,
b: 211,
a: 255,
};
pub const DARK_GRAY: Color = Color {
r: 64,
g: 64,
b: 64,
a: 255,
};
pub const TRANSPARENT: Color = Color {
r: 0,
g: 0,
b: 0,
a: 0,
};
}
#[inline]
pub(crate) fn mul_div_255(value: u8, alpha: u8) -> u8 {
(((value as u32 * alpha as u32) + 127) / 255) as u8
}
#[inline]
pub(crate) fn premultiply_rgba(r: u8, g: u8, b: u8, alpha: u8) -> [u8; 4] {
[
mul_div_255(r, alpha),
mul_div_255(g, alpha),
mul_div_255(b, alpha),
alpha,
]
}
#[inline]
pub(crate) fn scale_premultiplied_rgba(src: [u8; 4], alpha: u8) -> [u8; 4] {
[
mul_div_255(src[0], alpha),
mul_div_255(src[1], alpha),
mul_div_255(src[2], alpha),
mul_div_255(src[3], alpha),
]
}
#[inline]
pub(crate) fn source_over_premultiplied_rgba(dst: [u8; 4], src: [u8; 4]) -> [u8; 4] {
if src[3] == 0 {
return dst;
}
if src[3] == u8::MAX {
return src;
}
let inv_alpha = u8::MAX - src[3];
[
src[0].saturating_add(mul_div_255(dst[0], inv_alpha)),
src[1].saturating_add(mul_div_255(dst[1], inv_alpha)),
src[2].saturating_add(mul_div_255(dst[2], inv_alpha)),
src[3].saturating_add(mul_div_255(dst[3], inv_alpha)),
]
}
impl Color {
pub fn default_palette() -> &'static [Color] {
static PALETTE: &[Color] = &[
Color::from_rgb_u32(0x1f77b4), Color::from_rgb_u32(0xff7f0e), Color::from_rgb_u32(0x2ca02c), Color::from_rgb_u32(0xd62728), Color::from_rgb_u32(0x9467bd), Color::from_rgb_u32(0x8c564b), Color::from_rgb_u32(0xe377c2), Color::from_rgb_u32(0x7f7f7f), Color::from_rgb_u32(0xbcbd22), Color::from_rgb_u32(0x17becf), ];
PALETTE
}
pub fn from_palette(index: usize) -> Self {
let palette = Self::default_palette();
palette[index % palette.len()]
}
const fn from_rgb_u32(rgb: u32) -> Self {
Self {
r: ((rgb >> 16) & 0xFF) as u8,
g: ((rgb >> 8) & 0xFF) as u8,
b: (rgb & 0xFF) as u8,
a: 255,
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.a == 255 {
write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
} else {
write!(
f,
"#{:02x}{:02x}{:02x}{:02x}",
self.r, self.g, self.b, self.a
)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ColorError {
InvalidHex,
InvalidLength,
}
#[derive(Debug, Clone)]
pub struct ColorMap {
colors: Vec<Color>,
name: String,
}
impl ColorMap {
pub fn new(name: String, colors: Vec<Color>) -> Self {
Self { name, colors }
}
pub fn sample(&self, t: f64) -> Color {
let t = t.clamp(0.0, 1.0);
if self.colors.is_empty() {
return Color::BLACK;
}
if self.colors.len() == 1 {
return self.colors[0];
}
if t == 0.0 {
return self.colors[0];
}
if t == 1.0 {
return *self.colors.last().unwrap();
}
let scaled = t * (self.colors.len() - 1) as f64;
let index = scaled.floor() as usize;
let frac = scaled - index as f64;
if index >= self.colors.len() - 1 {
return *self.colors.last().unwrap();
}
let c1 = self.colors[index];
let c2 = self.colors[index + 1];
Color::from_rgba(
(c1.r as f64 + (c2.r as f64 - c1.r as f64) * frac) as u8,
(c1.g as f64 + (c2.g as f64 - c1.g as f64) * frac) as u8,
(c1.b as f64 + (c2.b as f64 - c1.b as f64) * frac) as u8,
(c1.a as f64 + (c2.a as f64 - c1.a as f64) * frac) as u8,
)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn len(&self) -> usize {
self.colors.len()
}
pub fn is_empty(&self) -> bool {
self.colors.is_empty()
}
#[cfg(feature = "3d")]
pub(crate) fn colors(&self) -> &[Color] {
&self.colors
}
}
impl ColorMap {
pub fn viridis() -> Self {
Self::new(
"viridis".to_string(),
vec![
Color::from_rgb_u32(0x440154), Color::from_rgb_u32(0x482777), Color::from_rgb_u32(0x3f4a8a), Color::from_rgb_u32(0x31678e), Color::from_rgb_u32(0x26838f), Color::from_rgb_u32(0x1f9d8a), Color::from_rgb_u32(0x6cce5a), Color::from_rgb_u32(0xb6de2b), Color::from_rgb_u32(0xfee825), ],
)
}
pub fn plasma() -> Self {
Self::new(
"plasma".to_string(),
vec![
Color::from_rgb_u32(0x0c0786), Color::from_rgb_u32(0x5b02a3), Color::from_rgb_u32(0x9a179b), Color::from_rgb_u32(0xd53e4f), Color::from_rgb_u32(0xf89441), Color::from_rgb_u32(0xfdbf6f), Color::from_rgb_u32(0xfeee65), ],
)
}
pub fn inferno() -> Self {
Self::new(
"inferno".to_string(),
vec![
Color::from_rgb_u32(0x000003), Color::from_rgb_u32(0x1f0c47), Color::from_rgb_u32(0x550f6d), Color::from_rgb_u32(0x88226a), Color::from_rgb_u32(0xb83655), Color::from_rgb_u32(0xe55c30), Color::from_rgb_u32(0xfb9b06), Color::from_rgb_u32(0xf7d746), Color::from_rgb_u32(0xfcfdbf), ],
)
}
pub fn magma() -> Self {
Self::new(
"magma".to_string(),
vec![
Color::from_rgb_u32(0x000003), Color::from_rgb_u32(0x1c1044), Color::from_rgb_u32(0x4f127b), Color::from_rgb_u32(0x812581), Color::from_rgb_u32(0xb5367a), Color::from_rgb_u32(0xe55964), Color::from_rgb_u32(0xfb8761), Color::from_rgb_u32(0xfec287), Color::from_rgb_u32(0xfbfcbf), ],
)
}
pub fn hot() -> Self {
Self::new(
"hot".to_string(),
vec![
Color::from_rgb(0, 0, 0), Color::from_rgb(128, 0, 0), Color::from_rgb(255, 0, 0), Color::from_rgb(255, 128, 0), Color::from_rgb(255, 255, 0), Color::from_rgb(255, 255, 128), Color::from_rgb(255, 255, 255), ],
)
}
pub fn cool() -> Self {
Self::new(
"cool".to_string(),
vec![
Color::from_rgb(0, 255, 255), Color::from_rgb(128, 128, 255), Color::from_rgb(255, 0, 255), ],
)
}
pub fn gray() -> Self {
Self::new(
"gray".to_string(),
vec![
Color::from_rgb(0, 0, 0), Color::from_rgb(64, 64, 64), Color::from_rgb(128, 128, 128), Color::from_rgb(192, 192, 192), Color::from_rgb(255, 255, 255), ],
)
}
pub fn jet() -> Self {
Self::new(
"jet".to_string(),
vec![
Color::from_rgb(0, 0, 128), Color::from_rgb(0, 0, 255), Color::from_rgb(0, 128, 255), Color::from_rgb(0, 255, 255), Color::from_rgb(128, 255, 128), Color::from_rgb(255, 255, 0), Color::from_rgb(255, 128, 0), Color::from_rgb(255, 0, 0), Color::from_rgb(128, 0, 0), ],
)
}
pub fn coolwarm() -> Self {
Self::new(
"coolwarm".to_string(),
vec![
Color::from_rgb_u32(0x3b4cc0), Color::from_rgb_u32(0x6788ee), Color::from_rgb_u32(0x9abbff), Color::from_rgb_u32(0xc9d7f0), Color::from_rgb_u32(0xf7f7f7), Color::from_rgb_u32(0xf6cfa5), Color::from_rgb_u32(0xf08a6d), Color::from_rgb_u32(0xd8412d), Color::from_rgb_u32(0xb40426), ],
)
}
pub fn rdbu() -> Self {
Self::new(
"rdbu".to_string(),
vec![
Color::from_rgb_u32(0x67001f), Color::from_rgb_u32(0xb2182b), Color::from_rgb_u32(0xd6604d), Color::from_rgb_u32(0xf4a582), Color::from_rgb_u32(0xfddbc7), Color::from_rgb_u32(0xf7f7f7), Color::from_rgb_u32(0xd1e5f0), Color::from_rgb_u32(0x92c5de), Color::from_rgb_u32(0x4393c3), Color::from_rgb_u32(0x2166ac), Color::from_rgb_u32(0x053061), ],
)
}
pub fn from_colors(colors: &[Color]) -> Self {
Self::new("custom".to_string(), colors.to_vec())
}
pub fn by_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"viridis" => Some(Self::viridis()),
"plasma" => Some(Self::plasma()),
"inferno" => Some(Self::inferno()),
"magma" => Some(Self::magma()),
"hot" => Some(Self::hot()),
"cool" => Some(Self::cool()),
"gray" | "grey" => Some(Self::gray()),
"jet" => Some(Self::jet()),
"coolwarm" => Some(Self::coolwarm()),
"rdbu" => Some(Self::rdbu()),
_ => None,
}
}
pub fn available_names() -> Vec<&'static str> {
vec![
"viridis", "plasma", "inferno", "magma", "hot", "cool", "gray", "jet", "coolwarm",
"rdbu",
]
}
}
#[derive(Debug, Clone)]
pub enum ColorMapSpec {
Named(String),
Map(ColorMap),
}
impl ColorMapSpec {
pub const FALLBACK: &'static str = "viridis";
pub fn name(&self) -> &str {
match self {
ColorMapSpec::Named(name) => name,
ColorMapSpec::Map(map) => map.name(),
}
}
pub fn resolve(&self) -> ColorMap {
match self {
ColorMapSpec::Named(name) => ColorMap::by_name(name).unwrap_or_else(ColorMap::viridis),
ColorMapSpec::Map(map) => map.clone(),
}
}
pub fn try_resolve(&self) -> Option<ColorMap> {
match self {
ColorMapSpec::Named(name) => ColorMap::by_name(name),
ColorMapSpec::Map(map) => Some(map.clone()),
}
}
pub fn into_name(self) -> String {
match self {
ColorMapSpec::Named(name) => name,
ColorMapSpec::Map(map) => map.name().to_string(),
}
}
}
impl Default for ColorMapSpec {
fn default() -> Self {
ColorMapSpec::Named(Self::FALLBACK.to_string())
}
}
impl From<ColorMap> for ColorMapSpec {
fn from(map: ColorMap) -> Self {
ColorMapSpec::Map(map)
}
}
impl From<&ColorMap> for ColorMapSpec {
fn from(map: &ColorMap) -> Self {
ColorMapSpec::Map(map.clone())
}
}
impl From<String> for ColorMapSpec {
fn from(name: String) -> Self {
ColorMapSpec::Named(name)
}
}
impl From<&str> for ColorMapSpec {
fn from(name: &str) -> Self {
ColorMapSpec::Named(name.to_string())
}
}
impl From<&String> for ColorMapSpec {
fn from(name: &String) -> Self {
ColorMapSpec::Named(name.clone())
}
}
impl From<ColorMapSpec> for ColorMap {
fn from(spec: ColorMapSpec) -> Self {
spec.resolve()
}
}
impl fmt::Display for ColorMapSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl fmt::Display for ColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ColorError::InvalidHex => write!(f, "Invalid hexadecimal color value"),
ColorError::InvalidLength => write!(
f,
"Invalid color string length (expected 3, 6, or 8 characters)"
),
}
}
}
impl std::error::Error for ColorError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_creation() {
let color = Color::from_rgb(255, 128, 64);
assert_eq!(color.r, 255);
assert_eq!(color.g, 128);
assert_eq!(color.b, 64);
assert_eq!(color.a, 255);
}
#[test]
fn test_color_with_alpha() {
let color = Color::RED.with_alpha(0.5);
assert_eq!(color.a, 127); }
#[test]
fn test_hex_parsing() {
assert_eq!(
Color::from_hex("#f0a").unwrap(),
Color::from_rgb(255, 0, 170)
);
assert_eq!(
Color::from_hex("#ff8040").unwrap(),
Color::from_rgb(255, 128, 64)
);
assert_eq!(
Color::from_hex("ff8040").unwrap(),
Color::from_rgb(255, 128, 64)
);
assert_eq!(
Color::from_hex("#ff804080").unwrap(),
Color::from_rgba(255, 128, 64, 128)
);
assert!(Color::from_hex("#12345").is_err());
assert!(Color::from_hex("#gghhii").is_err());
assert!(Color::from_hex("").is_err());
}
#[test]
fn test_predefined_colors() {
assert_eq!(Color::RED, Color::from_rgb(255, 0, 0));
assert_eq!(Color::BLUE, Color::from_rgb(0, 0, 255));
assert_eq!(Color::WHITE, Color::from_rgb(255, 255, 255));
assert_eq!(Color::TRANSPARENT.a, 0);
}
#[test]
fn test_color_palette() {
let palette = Color::default_palette();
assert_eq!(palette.len(), 10);
assert_eq!(Color::from_palette(0), palette[0]);
assert_eq!(Color::from_palette(10), palette[0]); assert_eq!(Color::from_palette(15), palette[5]);
}
#[test]
fn test_rgba_f32_conversion() {
let color = Color::from_rgba(255, 128, 64, 128);
let (r, g, b, a) = color.to_rgba_f32();
assert!((r - 1.0).abs() < f32::EPSILON);
assert!((g - 0.502).abs() < 0.01);
assert!((b - 0.251).abs() < 0.01);
assert!((a - 0.502).abs() < 0.01);
}
#[test]
fn test_color_display() {
assert_eq!(Color::RED.to_string(), "#ff0000");
assert_eq!(Color::from_rgba(255, 128, 64, 128).to_string(), "#ff804080");
}
#[test]
fn test_colormap_creation() {
let colors = vec![Color::RED, Color::GREEN, Color::BLUE];
let cmap = ColorMap::new("test".to_string(), colors);
assert_eq!(cmap.name(), "test");
assert_eq!(cmap.len(), 3);
}
#[test]
fn test_colormap_sampling() {
let cmap = ColorMap::new(
"test".to_string(),
vec![
Color::from_rgb(0, 0, 0), Color::from_rgb(255, 255, 255), ],
);
assert_eq!(cmap.sample(0.0), Color::from_rgb(0, 0, 0));
assert_eq!(cmap.sample(1.0), Color::from_rgb(255, 255, 255));
let mid = cmap.sample(0.5);
assert!(mid.r > 100 && mid.r < 200); }
#[test]
fn test_predefined_colormaps() {
let viridis = ColorMap::viridis();
assert_eq!(viridis.name(), "viridis");
assert!(!viridis.is_empty());
let plasma = ColorMap::plasma();
assert_eq!(plasma.name(), "plasma");
assert!(!plasma.is_empty());
}
#[test]
fn test_colormap_by_name() {
assert!(ColorMap::by_name("viridis").is_some());
assert!(ColorMap::by_name("plasma").is_some());
assert!(ColorMap::by_name("nonexistent").is_none());
assert!(ColorMap::by_name("VIRIDIS").is_some());
assert!(ColorMap::by_name("Plasma").is_some());
}
#[test]
fn test_colormap_edge_cases() {
let empty = ColorMap::new("empty".to_string(), vec![]);
assert_eq!(empty.sample(0.5), Color::BLACK);
let single = ColorMap::new("single".to_string(), vec![Color::RED]);
assert_eq!(single.sample(0.0), Color::RED);
assert_eq!(single.sample(0.5), Color::RED);
assert_eq!(single.sample(1.0), Color::RED);
}
#[test]
fn test_colormap_clamping() {
let cmap = ColorMap::viridis();
let below = cmap.sample(-0.5);
let above = cmap.sample(1.5);
let start = cmap.sample(0.0);
let end = cmap.sample(1.0);
assert_eq!(below, start);
assert_eq!(above, end);
}
#[test]
fn test_named_colors() {
assert_eq!(Color::named("red"), Some(Color::RED));
assert_eq!(Color::named("blue"), Some(Color::BLUE));
assert_eq!(Color::named("green"), Some(Color::GREEN));
assert_eq!(Color::named("RED"), Some(Color::RED));
assert_eq!(Color::named("Blue"), Some(Color::BLUE));
assert_eq!(Color::named("gray"), Some(Color::GRAY));
assert_eq!(Color::named("grey"), Some(Color::GRAY));
assert!(Color::named("coral").is_some());
assert!(Color::named("salmon").is_some());
assert!(Color::named("gold").is_some());
assert_eq!(Color::named("notacolor"), None);
}
#[test]
fn test_hex_convenience() {
assert!(Color::hex("#FF0000").is_some());
assert_eq!(Color::hex("#FF0000"), Some(Color::RED));
assert_eq!(Color::hex("invalid"), None);
assert_eq!(Color::hex("#GGGGGG"), None);
}
#[test]
fn test_color_suggestions() {
assert_eq!(Color::suggest_named("blu"), Some("blue"));
assert_eq!(Color::suggest_named("re"), Some("red"));
assert_eq!(Color::suggest_named("gree"), Some("green"));
assert_eq!(Color::suggest_named("bluee"), Some("blue"));
assert_eq!(Color::suggest_named("blua"), Some("blue"));
assert_eq!(Color::suggest_named("rad"), Some("red"));
assert_eq!(Color::suggest_named("xyz"), None);
}
#[test]
fn test_color_darken() {
let color = Color::from_rgb(100, 150, 200);
let same = color.darken(0.0);
assert_eq!(same.r, 100);
assert_eq!(same.g, 150);
assert_eq!(same.b, 200);
let darker = color.darken(0.3);
assert_eq!(darker.r, 70); assert_eq!(darker.g, 105); assert_eq!(darker.b, 140);
let black = color.darken(1.0);
assert_eq!(black.r, 0);
assert_eq!(black.g, 0);
assert_eq!(black.b, 0);
let with_alpha = Color::from_rgba(100, 150, 200, 128).darken(0.5);
assert_eq!(with_alpha.a, 128);
}
#[test]
fn test_color_lighten() {
let color = Color::from_rgb(100, 150, 200);
let same = color.lighten(0.0);
assert_eq!(same.r, 100);
assert_eq!(same.g, 150);
assert_eq!(same.b, 200);
let lighter = color.lighten(0.5);
assert_eq!(lighter.r, 177); assert_eq!(lighter.g, 202); assert_eq!(lighter.b, 227);
let white = color.lighten(1.0);
assert_eq!(white.r, 255);
assert_eq!(white.g, 255);
assert_eq!(white.b, 255);
let with_alpha = Color::from_rgba(100, 150, 200, 128).lighten(0.5);
assert_eq!(with_alpha.a, 128);
}
#[test]
fn premultiplied_source_over_preserves_destination_alpha() {
let coverage = 64;
let requested_alpha = 128;
let effective_alpha = mul_div_255(coverage, requested_alpha);
let source = premultiply_rgba(200, 100, 50, effective_alpha);
let destination = [20, 40, 60, 128];
assert_eq!(source, [25, 13, 6, 32]);
assert_eq!(
source_over_premultiplied_rgba(destination, source),
[42, 48, 58, 144]
);
}
#[test]
fn premultiplied_source_over_handles_transparent_and_opaque_sources() {
let destination = [20, 40, 60, 128];
assert_eq!(
source_over_premultiplied_rgba(destination, [0, 0, 0, 0]),
destination
);
assert_eq!(
source_over_premultiplied_rgba(destination, [10, 20, 30, 255]),
[10, 20, 30, 255]
);
}
#[test]
fn deprecated_constructors_forward_to_the_new_names() {
#[allow(deprecated)]
let old = Color::new(12, 34, 56);
assert_eq!(old, Color::from_rgb(12, 34, 56));
#[allow(deprecated)]
let old_rgba = Color::new_rgba(12, 34, 56, 78);
assert_eq!(old_rgba, Color::from_rgba(12, 34, 56, 78));
}
#[test]
fn colormap_spec_accepts_names_and_values() {
let from_str: ColorMapSpec = "plasma".into();
let from_string: ColorMapSpec = String::from("plasma").into();
let from_map: ColorMapSpec = ColorMap::plasma().into();
assert_eq!(from_str.name(), "plasma");
assert_eq!(from_string.name(), "plasma");
assert_eq!(from_map.name(), "plasma");
for spec in [from_str, from_string, from_map] {
assert_eq!(spec.resolve().sample(0.5), ColorMap::plasma().sample(0.5));
assert_eq!(spec.clone().into_name(), "plasma");
assert!(spec.try_resolve().is_some());
}
}
#[test]
fn colormap_spec_falls_back_for_unknown_names() {
let spec: ColorMapSpec = "not-a-colormap".into();
assert!(spec.try_resolve().is_none());
assert_eq!(spec.resolve().name(), ColorMapSpec::FALLBACK);
assert_eq!(ColorMapSpec::default().name(), ColorMapSpec::FALLBACK);
}
}