Fix semi-transparent colors appearing too bright (#5824)
The bug was in `Color32::from_rgba_unmultiplied` and by extension affects: * `Color32::from_rgba_unmultiplied` * `hex_color!` * `HexColor` * `ColorImage::from_rgba_unmultiplied` * All images with transparency (png, webp, …) * `Color32::from_white_alpha` The bug caused translucent colors to appear too bright. ## More Color is hard. When I started out egui I thought "linear space is objectively better, for everything!" and then I've been slowly walking that back for various reasons: * sRGB textures not available everywhere * gamma-space is more _perceptually_ even, so it makes sense to use for anti-aliasing * other applications do everything in gamma space, so that's what people expect (this PR) Similarly, pre-multiplied alpha _makes sense_ for blending colors. It also enables additive colors, which is nice. But it does complicate things. Especially when mixed with sRGB/gamma (As @karhu [points out](https://github.com/emilk/egui/pull/5824#issuecomment-2738099254)). ## Related * Closes https://github.com/emilk/egui/issues/5751 * Closes https://github.com/emilk/egui/issues/5771 ? (probably; hard to tell without a repro) * But not https://github.com/emilk/egui/issues/5810 ## TODO * [x] I broke the RGBA u8 color picker. Fix it --------- Co-authored-by: Andreas Reich <andreas@rerun.io>
This commit is contained in:
parent
d54e29d375
commit
3f731ec794
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 <https://en.wikipedia.org/wiki/SRGB>).
|
||||
/// 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<Box<[u8]>> = 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<Item = [u8; 4]> {
|
||||
[
|
||||
[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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<Color32> 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<Rgba> 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),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Rgba> for f32 {
|
|||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
|
||||
[
|
||||
[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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85
|
||||
size 102929
|
||||
oid sha256:d17f0693c6288f87d4a0bb009ea03911e8a9baf3efa81445a3ed7849df0313e9
|
||||
size 102920
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90
|
||||
size 31780
|
||||
oid sha256:2faddafd5f6fc445d15ec39248326d607d14838692201503a178ae1da2c0127d
|
||||
size 31675
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70
|
||||
size 24345
|
||||
oid sha256:f299fb3c7c66a0fde7a30916bb4be1bb14c43f5eb139268309aa8b46f86caede
|
||||
size 24388
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94
|
||||
size 35451
|
||||
oid sha256:b007380b5ce761ff5d23665dcaa2729e1795c5192efdf366007ddbcea0ed64a5
|
||||
size 35463
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:03ee62427611101758958adf2650a4a0eea4e023f07c9ec4ebc63425233e8a04
|
||||
size 554949
|
||||
oid sha256:1b618443ba6e8483425972bd95fced23d2cd5ff4ad05277a6171eac14c255302
|
||||
size 572382
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82ef265f0e22649c7fcdb9556879c1a30df582bd4e97c647258b3e5acc03d112
|
||||
size 771298
|
||||
oid sha256:00681d206ae05c2135dcc61e87a31f18248fc972804a01bc3440faf4fdd1a50e
|
||||
size 796392
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cad71b486a479eb9c5339a93f4acc3df2d0b6b188ad023b9b044be7311b0ab72
|
||||
size 918775
|
||||
oid sha256:a21e2cc32a032ee44516c495c68aa5f6e168da1bea44396db6e67889d5714e7f
|
||||
size 948339
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc9ed4d29f4227b9d38b477ee8f546ea8597acda56a6909ba4826891ebdbea01
|
||||
size 1039263
|
||||
oid sha256:7907a5851f4b5a986cbd74b494b65cff7a469038af63ac957d55e848bb3391a8
|
||||
size 1072165
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9bf826bee811d8af345ec1281266fc9bef6d7c3782279516984a6c75130a929
|
||||
size 1130895
|
||||
oid sha256:b220801c49d7d1f4364cf6c4f2098123e34ce782bb1439b2896d8adf9215a0be
|
||||
size 1166343
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9345de28f09e2891fd01db20bb0b94176ec3c89d8c2f344a6640d33e97ab5400
|
||||
size 1311417
|
||||
oid sha256:0ff8e54c66f64396b42bb962297eab966089318b1a75e65accef7abbeb7d6cee
|
||||
size 1353321
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2
|
||||
size 158553
|
||||
oid sha256:78335f9233990c5622d1f6f0f18a3b44e33b0e68061e865641b0b316072489ba
|
||||
size 158496
|
||||
|
|
|
|||
Loading…
Reference in New Issue