mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 22:00:03 -04:00
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:
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user