//! Colour for EffectCraft: RGBA values, sRGB transfer, HSL/HSV, luminance, the label palette and //! the 38 After-Effects-compatible blend modes. //! //! Blend-mode formulas come from the W3C *Compositing and Blending Level 1* specification and the //! standard definitions of the "classic" photographic modes; they are applied to un-premultiplied //! colour and composited with Porter-Duff source-over on premultiplied pixels. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod blend; pub mod icc; pub mod space; pub use blend::{BlendMode, blend_pixel}; use serde::{Deserialize, Serialize}; pub use space::{ColorSpace, Conversion}; /// Straight (un-premultiplied) RGBA, 0..1 nominal (32 bpc may exceed). #[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Rgba { pub r: f32, pub g: f32, pub b: f32, pub a: f32, } pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba { Rgba { r, g, b, a } } impl Rgba { pub const BLACK: Rgba = rgba(0.0, 0.0, 0.0, 1.0); pub const WHITE: Rgba = rgba(1.0, 1.0, 1.0, 1.0); pub const TRANSPARENT: Rgba = rgba(0.0, 0.0, 0.0, 0.0); pub fn from_u8(r: u8, g: u8, b: u8) -> Rgba { rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0) } /// `#rrggbb` or `#rrggbbaa`. pub fn from_hex(s: &str) -> Option { let s = s.trim().trim_start_matches('#'); let p = |i: usize| { let pair = s.get(i..i + 2)?; if !pair.bytes().all(|b| b.is_ascii_hexdigit()) { return None; } u8::from_str_radix(pair, 16).ok() }; match s.len() { 6 => Some(Rgba::from_u8(p(0)?, p(2)?, p(4)?)), 8 => Some(Rgba { a: p(6)? as f32 / 255.0, ..Rgba::from_u8(p(0)?, p(2)?, p(4)?) }), _ => None, } } pub fn to_hex(&self) -> String { let c = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; format!("#{:02X}{:02X}{:02X}", c(self.r), c(self.g), c(self.b)) } pub fn premultiplied(&self) -> [f32; 4] { [self.r * self.a, self.g * self.a, self.b * self.a, self.a] } pub fn to_array(&self) -> [f32; 4] { [self.r, self.g, self.b, self.a] } pub fn from_array(a: [f32; 4]) -> Rgba { rgba(a[0], a[1], a[2], a[3]) } pub fn lerp(&self, o: &Rgba, t: f32) -> Rgba { rgba(self.r + (o.r - self.r) * t, self.g + (o.g - self.g) * t, self.b + (o.b - self.b) * t, self.a + (o.a - self.a) * t) } pub fn to_u8(&self) -> [u8; 4] { let c = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8; [c(self.r), c(self.g), c(self.b), c(self.a)] } } /// sRGB electro-optical transfer: encoded → linear. pub fn srgb_to_linear(v: f32) -> f32 { if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } } /// Linear → sRGB encoded. pub fn linear_to_srgb(v: f32) -> f32 { if v <= 0.003_130_8 { v * 12.92 } else { 1.055 * v.max(0.0).powf(1.0 / 2.4) - 0.055 } } /// Rec. 709 luma weights (AE's luminance for luma mattes/keys). pub fn luminance(r: f32, g: f32, b: f32) -> f32 { 0.2126 * r + 0.7152 * g + 0.0722 * b } /// RGB (0..1) → HSL with h in 0..1. pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) { let max = r.max(g).max(b); let min = r.min(g).min(b); let l = (max + min) * 0.5; if (max - min).abs() < 1e-7 { return (0.0, 0.0, l); } let d = max - min; let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) }; let h = if max == r { (((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0).rem_euclid(1.0) } else if max == g { ((b - r) / d + 2.0) / 6.0 } else { ((r - g) / d + 4.0) / 6.0 }; (h, s, l) } pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) { if s <= 0.0 { return (l, l, l); } let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }; let p = 2.0 * l - q; let f = |mut t: f32| { t = t.rem_euclid(1.0); if t < 1.0 / 6.0 { p + (q - p) * 6.0 * t } else if t < 0.5 { q } else if t < 2.0 / 3.0 { p + (q - p) * (2.0 / 3.0 - t) * 6.0 } else { p } }; (f(h + 1.0 / 3.0), f(h), f(h - 1.0 / 3.0)) } pub fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) { let max = r.max(g).max(b); let min = r.min(g).min(b); let d = max - min; let h = if d < 1e-7 { 0.0 } else if max == r { (((g - b) / d).rem_euclid(6.0) / 6.0).rem_euclid(1.0) } else if max == g { ((b - r) / d + 2.0) / 6.0 } else { ((r - g) / d + 4.0) / 6.0 }; let s = if max > 0.0 { d / max } else { 0.0 }; (h, s, max) } pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) { let mut h6 = h.rem_euclid(1.0) * 6.0; // In f32, a tiny negative hue wraps to exactly 1.0; treat it as 0. if h6 >= 6.0 { h6 = 0.0; } let i = h6.floor(); let f = h6 - i; let p = v * (1.0 - s); let q = v * (1.0 - s * f); let t = v * (1.0 - s * (1.0 - f)); match i as i32 { 0 => (v, t, p), 1 => (q, v, p), 2 => (p, v, t), 3 => (p, q, v), 4 => (t, p, v), _ => (v, p, q), } } /// Item/layer label colours (16 slots + None), defaults as in the public Labels preferences. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Label { None, #[default] Red, Yellow, Aqua, Pink, Lavender, Peach, SeaFoam, Blue, Green, Purple, Orange, Brown, Fuchsia, Cyan, Sandstone, DarkGreen, } impl Label { pub const ALL: [Label; 17] = [ Label::None, Label::Red, Label::Yellow, Label::Aqua, Label::Pink, Label::Lavender, Label::Peach, Label::SeaFoam, Label::Blue, Label::Green, Label::Purple, Label::Orange, Label::Brown, Label::Fuchsia, Label::Cyan, Label::Sandstone, Label::DarkGreen, ]; pub fn name(self) -> &'static str { match self { Label::None => "None", Label::Red => "Red", Label::Yellow => "Yellow", Label::Aqua => "Aqua", Label::Pink => "Pink", Label::Lavender => "Lavender", Label::Peach => "Peach", Label::SeaFoam => "Sea Foam", Label::Blue => "Blue", Label::Green => "Green", Label::Purple => "Purple", Label::Orange => "Orange", Label::Brown => "Brown", Label::Fuchsia => "Fuchsia", Label::Cyan => "Cyan", Label::Sandstone => "Sandstone", Label::DarkGreen => "Dark Green", } } pub fn from_name(s: &str) -> Option