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