diff --git a/crates/ecolor/README.md b/crates/ecolor/README.md index 4c84da01..98e3c372 100644 --- a/crates/ecolor/README.md +++ b/crates/ecolor/README.md @@ -8,4 +8,6 @@ A simple color storage and conversion library. -Made for [`egui`](https://github.com/emilk/egui/). +This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/). + +If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead. diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index e72a3b98..b5e05276 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -5,10 +5,24 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba}; /// Instead of manipulating this directly it is often better /// to first convert it to either [`Rgba`] or [`crate::Hsva`]. /// -/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha. -/// Alpha channel is in linear space. +/// Internally this uses 0-255 gamma space `sRGBA` color with _premultiplied alpha_. /// -/// The special value of alpha=0 means the color is to be treated as an additive color. +/// It's the non-linear ("gamma") values that are multiplied with the alpha. +/// +/// Premultiplied alpha means that the color values have been pre-multiplied with the alpha (opacity). +/// This is in contrast with "normal" RGBA, where the alpha is _separate_ (or "unmultiplied"). +/// Using premultiplied alpha has some advantages: +/// * It allows encoding additive colors +/// * It is the better way to blend colors, e.g. when filtering texture colors +/// * Because the above, it is the better way to encode colors in a GPU texture +/// +/// The color space is assumed to be [sRGB](https://en.wikipedia.org/wiki/SRGB). +/// +/// All operations on `Color32` are done in "gamma space" (see ). +/// This is not physically correct, but it is fast and sometimes more perceptually even than linear space. +/// If you instead want to perform these operations in linear-space color, use [`Rgba`]. +/// +/// An `alpha=0` means the color is to be treated as an additive color. #[repr(C)] #[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -16,6 +30,7 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba}; pub struct Color32(pub(crate) [u8; 4]); impl std::fmt::Debug for Color32 { + /// Prints the contents with premultiplied alpha! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let [r, g, b, a] = self.0; write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}") @@ -90,41 +105,49 @@ impl Color32 { #[deprecated = "Renamed to PLACEHOLDER"] pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER; + /// From RGB with alpha of 255 (opaque). #[inline] pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 255]) } + /// From RGB into an additive color (will make everything it blend with brighter). #[inline] pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 0]) } /// From `sRGBA` with premultiplied alpha. + /// + /// You likely want to use [`Self::from_rgba_unmultiplied`] instead. #[inline] pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { Self([r, g, b, a]) } - /// From `sRGBA` WITHOUT premultiplied alpha. + /// From `sRGBA` with separate alpha. + /// + /// This is a "normal" RGBA value that you would find in a color picker or a table somewhere. + /// + /// You can use [`Self::to_srgba_unmultiplied`] to get back these values, + /// but for transparent colors what you get back might be slightly different (rounding errors). #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { use std::sync::OnceLock; match a { - // common-case optimization + // common-case optimization: 0 => Self::TRANSPARENT, - // common-case optimization + + // common-case optimization: 255 => Self::from_rgb(r, g, b), + a => { static LOOKUP_TABLE: OnceLock> = OnceLock::new(); let lut = LOOKUP_TABLE.get_or_init(|| { - use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8}; (0..=u16::MAX) .map(|i| { let [value, alpha] = i.to_ne_bytes(); - let value_lin = linear_f32_from_gamma_u8(value); - let alpha_lin = linear_f32_from_linear_u8(alpha); - gamma_u8_from_linear_f32(value_lin * alpha_lin) + fast_round(value as f32 * linear_f32_from_linear_u8(alpha)) }) .collect() }); @@ -136,22 +159,26 @@ impl Color32 { } } + /// Opaque gray. #[doc(alias = "from_grey")] #[inline] pub const fn from_gray(l: u8) -> Self { Self([l, l, l, 255]) } + /// Black with the given opacity. #[inline] pub const fn from_black_alpha(a: u8) -> Self { Self([0, 0, 0, a]) } + /// White with the given opacity. #[inline] pub fn from_white_alpha(a: u8) -> Self { - Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into() + Self([a, a, a, a]) } + /// Additive white. #[inline] pub const fn from_additive_luminance(l: u8) -> Self { Self([l, l, l, 0]) @@ -162,21 +189,25 @@ impl Color32 { self.a() == 255 } + /// Red component multiplied by alpha. #[inline] pub const fn r(&self) -> u8 { self.0[0] } + /// Green component multiplied by alpha. #[inline] pub const fn g(&self) -> u8 { self.0[1] } + /// Blue component multiplied by alpha. #[inline] pub const fn b(&self) -> u8 { self.0[2] } + /// Alpha (opacity). #[inline] pub const fn a(&self) -> u8 { self.0[3] @@ -213,9 +244,26 @@ impl Color32 { (self.r(), self.g(), self.b(), self.a()) } + /// Convert to a normal "unmultiplied" RGBA color (i.e. with separate alpha). + /// + /// This will unmultiply the alpha. + /// + /// This is the inverse of [`Self::from_rgba_unmultiplied`], + /// but due to precision problems it may return slightly different values for transparent colors. #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - Rgba::from(*self).to_srgba_unmultiplied() + let [r, g, b, a] = self.to_array(); + match a { + // Common-case optimization. + 0 | 255 => self.to_array(), + a => { + let factor = 255.0 / a as f32; + let r = fast_round(factor * r as f32); + let g = fast_round(factor * g as f32); + let b = fast_round(factor * b as f32); + [r, g, b, a] + } + } } /// Multiply with 0.5 to make color half as opaque, perceptually. @@ -291,7 +339,7 @@ impl Color32 { ) } - /// Blend two colors, so that `self` is behind the argument. + /// Blend two colors in gamma space, so that `self` is behind the argument. pub fn blend(self, on_top: Self) -> Self { self.gamma_multiply_u8(255 - on_top.a()) + on_top } @@ -333,3 +381,131 @@ impl std::ops::Add for Color32 { ]) } } + +#[cfg(test)] +mod test { + use super::*; + + fn test_rgba() -> impl Iterator { + [ + [0, 0, 0, 0], + [0, 0, 0, 255], + [10, 0, 30, 0], + [10, 0, 30, 40], + [10, 100, 200, 0], + [10, 100, 200, 100], + [10, 100, 200, 200], + [10, 100, 200, 255], + [10, 100, 200, 40], + [10, 20, 0, 0], + [10, 20, 0, 255], + [10, 20, 30, 255], + [10, 20, 30, 40], + [255, 255, 255, 0], + [255, 255, 255, 255], + ] + .into_iter() + } + + #[test] + fn test_color32_additive() { + let opaque = Color32::from_rgb(40, 50, 60); + let additive = Color32::from_rgb(255, 127, 10).additive(); + assert_eq!(additive.blend(opaque), opaque, "opaque on top of additive"); + assert_eq!( + opaque.blend(additive), + Color32::from_rgb(255, 177, 70), + "additive on top of opaque" + ); + } + + #[test] + fn test_color32_blend_vs_gamma_blend() { + let opaque = Color32::from_rgb(0x60, 0x60, 0x60); + let transparent = Color32::from_rgba_unmultiplied(168, 65, 65, 79); + assert_eq!( + transparent.blend(opaque), + opaque, + "Opaque on top of transparent" + ); + // Blending in gamma-space is the de-facto standard almost everywhere. + // Browsers and most image editors do it, and so it is what users expect. + assert_eq!( + opaque.blend(transparent), + Color32::from_rgb( + blend(0x60, 168, 79), + blend(0x60, 65, 79), + blend(0x60, 65, 79) + ), + "Transparent on top of opaque" + ); + + fn blend(dest: u8, src: u8, alpha: u8) -> u8 { + let src = src as f32 / 255.0; + let dest = dest as f32 / 255.0; + let alpha = alpha as f32 / 255.0; + fast_round((src * alpha + dest * (1.0 - alpha)) * 255.0) + } + } + + #[test] + fn color32_unmultiplied_round_trip() { + for in_rgba in test_rgba() { + let [r, g, b, a] = in_rgba; + if a == 0 { + continue; + } + + let c = Color32::from_rgba_unmultiplied(r, g, b, a); + let out_rgba = c.to_srgba_unmultiplied(); + + if a == 255 { + assert_eq!(in_rgba, out_rgba); + } else { + // There will be small rounding errors whenever the alpha is not 0 or 255, + // because we multiply and then unmultiply the alpha. + for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) { + assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}"); + } + } + } + } + + #[test] + fn from_black_white_alpha() { + for a in 0..=255 { + assert_eq!( + Color32::from_white_alpha(a), + Color32::from_rgba_unmultiplied(255, 255, 255, a) + ); + assert_eq!( + Color32::from_white_alpha(a), + Color32::WHITE.gamma_multiply_u8(a) + ); + + assert_eq!( + Color32::from_black_alpha(a), + Color32::from_rgba_unmultiplied(0, 0, 0, a) + ); + assert_eq!( + Color32::from_black_alpha(a), + Color32::BLACK.gamma_multiply_u8(a) + ); + } + } + + #[test] + fn to_from_rgba() { + for [r, g, b, a] in test_rgba() { + let original = Color32::from_rgba_unmultiplied(r, g, b, a); + let rgba = Rgba::from(original); + let back = Color32::from(rgba); + assert_eq!(back, original); + } + + assert_eq!( + Color32::from(Rgba::from_rgba_unmultiplied(1.0, 0.0, 0.0, 0.5)), + Color32::from_rgba_unmultiplied(255, 0, 0, 128) + ); + } +} diff --git a/crates/ecolor/src/hex_color_runtime.rs b/crates/ecolor/src/hex_color_runtime.rs index f817bf4b..21e07ffc 100644 --- a/crates/ecolor/src/hex_color_runtime.rs +++ b/crates/ecolor/src/hex_color_runtime.rs @@ -208,17 +208,22 @@ mod tests { #[test] fn hex_string_round_trip() { - use Color32 as C; let cases = [ - C::from_rgba_unmultiplied(10, 20, 30, 0), - C::from_rgba_unmultiplied(10, 20, 30, 40), - C::from_rgba_unmultiplied(10, 20, 30, 255), - C::from_rgba_unmultiplied(0, 20, 30, 0), - C::from_rgba_unmultiplied(10, 0, 30, 40), - C::from_rgba_unmultiplied(10, 20, 0, 255), + [0, 20, 30, 0], + [10, 0, 30, 40], + [10, 100, 200, 0], + [10, 100, 200, 100], + [10, 100, 200, 200], + [10, 100, 200, 255], + [10, 100, 200, 40], + [10, 20, 0, 255], + [10, 20, 30, 0], + [10, 20, 30, 255], + [10, 20, 30, 40], ]; - for color in cases { - assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color)); + for [r, g, b, a] in cases { + let color = Color32::from_rgba_unmultiplied(r, g, b, a); + assert_eq!(Color32::from_hex(color.to_hex().as_str()), Ok(color)); } } } diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 5f5430cf..02ffd67d 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -1,6 +1,5 @@ use crate::{ - gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, - linear_u8_from_linear_f32, Color32, Rgba, + gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba, }; /// Hue, saturation, value, alpha. All in the range [0, 1]. @@ -29,30 +28,20 @@ impl Hsva { /// From `sRGBA` with premultiplied alpha #[inline] pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self { - Self::from_rgba_premultiplied( - linear_f32_from_gamma_u8(r), - linear_f32_from_gamma_u8(g), - linear_f32_from_gamma_u8(b), - linear_f32_from_linear_u8(a), - ) + Self::from(Color32::from_rgba_premultiplied(r, g, b, a)) } /// From `sRGBA` without premultiplied alpha #[inline] pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self { - Self::from_rgba_unmultiplied( - linear_f32_from_gamma_u8(r), - linear_f32_from_gamma_u8(g), - linear_f32_from_gamma_u8(b), - linear_f32_from_linear_u8(a), - ) + Self::from(Color32::from_rgba_unmultiplied(r, g, b, a)) } /// From linear RGBA with premultiplied alpha #[inline] pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { #![allow(clippy::many_single_char_names)] - if a == 0.0 { + if a <= 0.0 { if r == 0.0 && b == 0.0 && a == 0.0 { Self::default() } else { @@ -152,13 +141,7 @@ impl Hsva { #[inline] pub fn to_srgba_premultiplied(&self) -> [u8; 4] { - let [r, g, b, a] = self.to_rgba_premultiplied(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a), - ] + Color32::from(*self).to_array() } /// To gamma-space 0-255. diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index a9400d9d..9c4ac9b6 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -1,9 +1,20 @@ //! Color conversions and types. //! +//! This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/). +//! +//! If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead. +//! //! If you want a compact color representation, use [`Color32`]. -//! If you want to manipulate RGBA colors use [`Rgba`]. +//! If you want to manipulate RGBA colors in linear space use [`Rgba`]. //! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`]. //! +//! ## Conventions +//! The word "gamma" or "srgb" is used to refer to values in the non-linear space defined by +//! [the sRGB transfer function](https://en.wikipedia.org/wiki/SRGB). +//! We use `u8` for anything in the "gamma" space. +//! +//! We use `f32` in 0-1 range for anything in the linear space. +//! //! ## Feature flags #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! @@ -39,23 +50,46 @@ pub use hex_color_runtime::*; impl From for Rgba { fn from(srgba: Color32) -> Self { - Self([ - linear_f32_from_gamma_u8(srgba.0[0]), - linear_f32_from_gamma_u8(srgba.0[1]), - linear_f32_from_gamma_u8(srgba.0[2]), - linear_f32_from_linear_u8(srgba.0[3]), - ]) + let [r, g, b, a] = srgba.to_array(); + if a == 0 { + // Additive, or completely transparent + Self([ + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + 0.0, + ]) + } else { + let a = linear_f32_from_linear_u8(a); + Self([ + linear_from_gamma(r as f32 / (255.0 * a)) * a, + linear_from_gamma(g as f32 / (255.0 * a)) * a, + linear_from_gamma(b as f32 / (255.0 * a)) * a, + a, + ]) + } } } impl From for Color32 { fn from(rgba: Rgba) -> Self { - Self([ - gamma_u8_from_linear_f32(rgba.0[0]), - gamma_u8_from_linear_f32(rgba.0[1]), - gamma_u8_from_linear_f32(rgba.0[2]), - linear_u8_from_linear_f32(rgba.0[3]), - ]) + let [r, g, b, a] = rgba.to_array(); + if a == 0.0 { + // Additive, or completely transparent + Self([ + gamma_u8_from_linear_f32(r), + gamma_u8_from_linear_f32(g), + gamma_u8_from_linear_f32(b), + 0, + ]) + } else { + Self([ + fast_round(gamma_u8_from_linear_f32(r / a) as f32 * a), + fast_round(gamma_u8_from_linear_f32(g / a) as f32 * a), + fast_round(gamma_u8_from_linear_f32(b / a) as f32 * a), + linear_u8_from_linear_f32(a), + ]) + } } } diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 93cb41d8..85535bf3 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -1,9 +1,8 @@ -use crate::{ - gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, - linear_u8_from_linear_f32, -}; +use crate::Color32; /// 0-1 linear space `RGBA` color with premultiplied alpha. +/// +/// See [`crate::Color32`] for explanation of what "premultiplied alpha" means. #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -70,20 +69,12 @@ impl Rgba { #[inline] pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - let r = linear_f32_from_gamma_u8(r); - let g = linear_f32_from_gamma_u8(g); - let b = linear_f32_from_gamma_u8(b); - let a = linear_f32_from_linear_u8(a); - Self::from_rgba_premultiplied(r, g, b, a) + Self::from(Color32::from_rgba_premultiplied(r, g, b, a)) } #[inline] pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - let r = linear_f32_from_gamma_u8(r); - let g = linear_f32_from_gamma_u8(g); - let b = linear_f32_from_gamma_u8(b); - let a = linear_f32_from_linear_u8(a); - Self::from_rgba_premultiplied(r * a, g * a, b * a, a) + Self::from(Color32::from_rgba_unmultiplied(r, g, b, a)) } #[inline] @@ -211,13 +202,12 @@ impl Rgba { /// unmultiply the alpha #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - let [r, g, b, a] = self.to_rgba_unmultiplied(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a.abs()), - ] + crate::Color32::from(*self).to_srgba_unmultiplied() + } + + /// Blend two colors in linear space, so that `self` is behind the argument. + pub fn blend(self, on_top: Self) -> Self { + self.multiply(1.0 - on_top.a()) + on_top } } @@ -276,3 +266,72 @@ impl std::ops::Mul for f32 { ]) } } + +#[cfg(test)] +mod test { + + use super::*; + + fn test_rgba() -> impl Iterator { + [ + [0, 0, 0, 0], + [0, 0, 0, 255], + [10, 0, 30, 0], + [10, 0, 30, 40], + [10, 100, 200, 0], + [10, 100, 200, 100], + [10, 100, 200, 200], + [10, 100, 200, 255], + [10, 100, 200, 40], + [10, 20, 0, 0], + [10, 20, 0, 255], + [10, 20, 30, 255], + [10, 20, 30, 40], + [255, 255, 255, 0], + [255, 255, 255, 255], + ] + .into_iter() + } + + #[test] + fn test_rgba_blend() { + let opaque = Rgba::from_rgb(0.4, 0.5, 0.6); + let transparent = Rgba::from_rgb(1.0, 0.5, 0.0).multiply(0.3); + assert_eq!( + transparent.blend(opaque), + opaque, + "Opaque on top of transparent" + ); + assert_eq!( + opaque.blend(transparent), + Rgba::from_rgb( + 0.7 * 0.4 + 0.3 * 1.0, + 0.7 * 0.5 + 0.3 * 0.5, + 0.7 * 0.6 + 0.3 * 0.0 + ), + "Transparent on top of opaque" + ); + } + + #[test] + fn test_rgba_roundtrip() { + for in_rgba in test_rgba() { + let [r, g, b, a] = in_rgba; + if a == 0 { + continue; + } + let rgba = Rgba::from_srgba_unmultiplied(r, g, b, a); + let out_rgba = rgba.to_srgba_unmultiplied(); + + if a == 255 { + assert_eq!(in_rgba, out_rgba); + } else { + // There will be small rounding errors whenever the alpha is not 0 or 255, + // because we multiply and then unmultiply the alpha. + for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) { + assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}"); + } + } + } + } +} diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 6da01d9b..dad03681 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2240,18 +2240,21 @@ impl Ui { /// # Colors impl Ui { /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. pub fn color_edit_button_srgba(&mut self, srgba: &mut Color32) -> Response { color_picker::color_edit_button_srgba(self, srgba, color_picker::Alpha::BlendOrAdditive) } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. pub fn color_edit_button_hsva(&mut self, hsva: &mut Hsva) -> Response { color_picker::color_edit_button_hsva(self, hsva, color_picker::Alpha::BlendOrAdditive) } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in `sRGB` space. pub fn color_edit_button_srgb(&mut self, srgb: &mut [u8; 3]) -> Response { @@ -2259,6 +2262,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in linear RGB space. pub fn color_edit_button_rgb(&mut self, rgb: &mut [f32; 3]) -> Response { @@ -2266,6 +2270,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in `sRGBA` space with premultiplied alpha pub fn color_edit_button_srgba_premultiplied(&mut self, srgba: &mut [u8; 4]) -> Response { @@ -2276,6 +2281,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in `sRGBA` space without premultiplied alpha. /// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use. @@ -2288,6 +2294,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in linear RGBA space with premultiplied alpha pub fn color_edit_button_rgba_premultiplied(&mut self, rgba_premul: &mut [f32; 4]) -> Response { @@ -2307,6 +2314,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in linear RGBA space without premultiplied alpha. /// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use. diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 62624506..026ddf56 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85 -size 102929 +oid sha256:d17f0693c6288f87d4a0bb009ea03911e8a9baf3efa81445a3ed7849df0313e9 +size 102920 diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index e83e643b..879e2c7a 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -92,8 +92,6 @@ impl ColorTest { ui.label("Test that vertex color times texture color is done in gamma space:"); ui.scope(|ui| { - ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients - let tex_color = Color32::from_rgb(64, 128, 255); let vertex_color = Color32::from_rgb(128, 196, 196); let ground_truth = mul_color_gamma(tex_color, vertex_color); @@ -106,6 +104,9 @@ impl ColorTest { show_color(ui, vertex_color, color_size); ui.label(" vertex color ="); }); + + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + { let g = Gradient::one_color(ground_truth); self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); @@ -129,6 +130,34 @@ impl ColorTest { ui.separator(); + ui.label("Test that blending is done in gamma space:"); + ui.scope(|ui| { + let background = Color32::from_rgb(200, 60, 10); + let foreground = Color32::from_rgba_unmultiplied(108, 65, 200, 82); + let ground_truth = background.blend(foreground); + + ui.horizontal(|ui| { + let color_size = ui.spacing().interact_size; + ui.label("Background:"); + show_color(ui, background, color_size); + ui.label(", foreground: "); + show_color(ui, foreground, color_size); + }); + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + { + let g = Gradient::one_color(ground_truth); + self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); + self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g); + } + { + let g = Gradient::one_color(foreground); + self.vertex_gradient(ui, "Vertex blending", background, &g); + self.tex_gradient(ui, "Texture blending", background, &g); + } + }); + + ui.separator(); + // TODO(emilk): test color multiplication (image tint), // to make sure vertex and texture color multiplication is done in linear space. diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 190f7055..96eaf100 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90 -size 31780 +oid sha256:2faddafd5f6fc445d15ec39248326d607d14838692201503a178ae1da2c0127d +size 31675 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index bfba77f1..d4b9df8d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70 -size 24345 +oid sha256:f299fb3c7c66a0fde7a30916bb4be1bb14c43f5eb139268309aa8b46f86caede +size 24388 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index e7996836..a2416b30 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94 -size 35451 +oid sha256:b007380b5ce761ff5d23665dcaa2729e1795c5192efdf366007ddbcea0ed64a5 +size 35463 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index e1b600fe..8aacaa76 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03ee62427611101758958adf2650a4a0eea4e023f07c9ec4ebc63425233e8a04 -size 554949 +oid sha256:1b618443ba6e8483425972bd95fced23d2cd5ff4ad05277a6171eac14c255302 +size 572382 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index ab9c8b81..ecc877b5 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82ef265f0e22649c7fcdb9556879c1a30df582bd4e97c647258b3e5acc03d112 -size 771298 +oid sha256:00681d206ae05c2135dcc61e87a31f18248fc972804a01bc3440faf4fdd1a50e +size 796392 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index b2d8c311..d3186203 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cad71b486a479eb9c5339a93f4acc3df2d0b6b188ad023b9b044be7311b0ab72 -size 918775 +oid sha256:a21e2cc32a032ee44516c495c68aa5f6e168da1bea44396db6e67889d5714e7f +size 948339 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index 2056c3fa..aaae79e2 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc9ed4d29f4227b9d38b477ee8f546ea8597acda56a6909ba4826891ebdbea01 -size 1039263 +oid sha256:7907a5851f4b5a986cbd74b494b65cff7a469038af63ac957d55e848bb3391a8 +size 1072165 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 586916e5..8b1886e8 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9bf826bee811d8af345ec1281266fc9bef6d7c3782279516984a6c75130a929 -size 1130895 +oid sha256:b220801c49d7d1f4364cf6c4f2098123e34ce782bb1439b2896d8adf9215a0be +size 1166343 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index b764e7be..8eec2cc3 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9345de28f09e2891fd01db20bb0b94176ec3c89d8c2f344a6640d33e97ab5400 -size 1311417 +oid sha256:0ff8e54c66f64396b42bb962297eab966089318b1a75e65accef7abbeb7d6cee +size 1353321 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 87f13e8e..da499718 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2 -size 158553 +oid sha256:78335f9233990c5622d1f6f0f18a3b44e33b0e68061e865641b0b316072489ba +size 158496