use crate::geometry::Scale;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlurStrength(f32);
impl BlurStrength {
pub fn new(points: f32) -> Self {
Self(if points.is_finite() {
points.max(0.0)
} else {
0.0
})
}
pub fn points(self) -> f32 {
self.0
}
pub(crate) fn to_physical_radius(self, scale: Scale) -> f32 {
self.0 * scale.factor()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LinearRgba {
r: f32,
g: f32,
b: f32,
a: f32,
}
impl LinearRgba {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self {
r: finite_or_zero(r),
g: finite_or_zero(g),
b: finite_or_zero(b),
a: finite_or_zero(a).clamp(0.0, 1.0),
}
}
pub fn from_srgb_unmultiplied(rgba: [u8; 4]) -> Self {
let [r, g, b, a] = rgba;
Self {
r: srgb_to_linear(f32::from(r) / 255.0),
g: srgb_to_linear(f32::from(g) / 255.0),
b: srgb_to_linear(f32::from(b) / 255.0),
a: f32::from(a) / 255.0,
}
}
pub fn r(self) -> f32 {
self.r
}
pub fn g(self) -> f32 {
self.g
}
pub fn b(self) -> f32 {
self.b
}
pub fn a(self) -> f32 {
self.a
}
}
fn srgb_to_linear(c: f32) -> f32 {
if c <= 0.040_45 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
fn finite_or_zero(x: f32) -> f32 {
if x.is_finite() { x } else { 0.0 }
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tint(LinearRgba);
impl Tint {
pub fn new(color: LinearRgba) -> Self {
Self(color)
}
pub fn from_srgb_unmultiplied(rgba: [u8; 4]) -> Self {
Self(LinearRgba::from_srgb_unmultiplied(rgba))
}
pub fn color(self) -> LinearRgba {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CornerRadius(f32);
impl CornerRadius {
pub fn new(points: f32) -> Self {
Self(if points.is_finite() {
points.max(0.0)
} else {
0.0
})
}
pub fn points(self) -> f32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Opacity(f32);
impl Opacity {
pub const FULL: Self = Self(1.0);
pub fn new(factor: f32) -> Self {
Self(if factor.is_finite() {
factor.clamp(0.0, 1.0)
} else {
1.0
})
}
pub fn value(self) -> f32 {
self.0
}
}
impl Default for Opacity {
fn default() -> Self {
Self::FULL
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn blur_strength_new_clamps_negative_to_zero() {
assert_eq!(BlurStrength::new(-3.0).points(), 0.0);
assert_eq!(BlurStrength::new(8.0).points(), 8.0);
}
#[test]
fn blur_strength_new_scrubs_non_finite_to_zero() {
assert_eq!(BlurStrength::new(f32::NAN).points(), 0.0);
assert_eq!(BlurStrength::new(f32::INFINITY).points(), 0.0);
assert_eq!(BlurStrength::new(f32::NEG_INFINITY).points(), 0.0);
}
#[test]
fn to_physical_radius_multiplies_by_scale() {
let r = BlurStrength::new(8.0).to_physical_radius(Scale::new(2.0));
assert!(close(r, 16.0));
}
#[test]
fn to_physical_radius_of_zero_strength_is_zero() {
let r = BlurStrength::new(0.0).to_physical_radius(Scale::new(3.0));
assert!(close(r, 0.0));
}
#[test]
fn corner_radius_new_clamps_negative_to_zero() {
assert_eq!(CornerRadius::new(-1.0).points(), 0.0);
assert_eq!(CornerRadius::new(12.0).points(), 12.0);
}
#[test]
fn from_srgb_unmultiplied_maps_endpoints_exactly() {
let black = LinearRgba::from_srgb_unmultiplied([0, 0, 0, 255]);
assert!(close(black.r(), 0.0) && close(black.g(), 0.0) && close(black.b(), 0.0));
assert!(close(black.a(), 1.0));
let white = LinearRgba::from_srgb_unmultiplied([255, 255, 255, 255]);
assert!(close(white.r(), 1.0) && close(white.g(), 1.0) && close(white.b(), 1.0));
}
#[test]
fn from_srgb_unmultiplied_decodes_midtone_through_eotf() {
let mid = LinearRgba::from_srgb_unmultiplied([188, 188, 188, 128]);
assert!(close(mid.r(), 0.502_886_5));
assert!(close(mid.a(), 128.0 / 255.0));
}
#[test]
fn from_srgb_unmultiplied_uses_linear_segment_near_black() {
let dark = LinearRgba::from_srgb_unmultiplied([2, 2, 2, 255]);
let expected = (2.0 / 255.0) / 12.92;
assert!(close(dark.r(), expected));
}
#[test]
fn new_scrubs_non_finite_channels_to_zero() {
let scrubbed = LinearRgba::new(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::NAN);
assert_eq!(scrubbed.r(), 0.0);
assert_eq!(scrubbed.g(), 0.0);
assert_eq!(scrubbed.b(), 0.0);
assert_eq!(scrubbed.a(), 0.0);
}
#[test]
fn new_clamps_alpha_but_keeps_hdr_rgb() {
let color = LinearRgba::new(4.0, 0.0, 0.0, 1.5);
assert!(close(color.r(), 4.0)); assert!(close(color.a(), 1.0)); }
#[test]
fn tint_from_srgb_decodes_its_wrapped_color() {
let tint = Tint::from_srgb_unmultiplied([255, 255, 255, 64]);
assert!(close(tint.color().r(), 1.0));
assert!(close(tint.color().a(), 64.0 / 255.0));
}
#[test]
fn opacity_new_clamps_into_unit_range() {
assert_eq!(Opacity::new(-1.0).value(), 0.0);
assert_eq!(Opacity::new(2.0).value(), 1.0);
assert!(close(Opacity::new(0.3).value(), 0.3));
}
#[test]
fn opacity_new_scrubs_non_finite_to_full() {
assert_eq!(Opacity::new(f32::NAN).value(), 1.0);
assert_eq!(Opacity::new(f32::INFINITY).value(), 1.0);
assert_eq!(Opacity::new(f32::NEG_INFINITY).value(), 1.0);
}
#[test]
fn opacity_default_and_full_are_one() {
assert_eq!(Opacity::default().value(), 1.0);
assert_eq!(Opacity::FULL.value(), 1.0);
}
}