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:
Emil Ernerfeldt 2025-03-21 10:45:25 +01:00 committed by GitHub
parent d54e29d375
commit 3f731ec794
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 399 additions and 103 deletions

View File

@ -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.

View File

@ -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)
);
}
}

View File

@ -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));
}
}
}

View File

@ -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.

View File

@ -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),
])
}
}
}

View File

@ -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:?}");
}
}
}
}
}

View File

@ -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.

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85
size 102929
oid sha256:d17f0693c6288f87d4a0bb009ea03911e8a9baf3efa81445a3ed7849df0313e9
size 102920

View File

@ -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.

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90
size 31780
oid sha256:2faddafd5f6fc445d15ec39248326d607d14838692201503a178ae1da2c0127d
size 31675

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70
size 24345
oid sha256:f299fb3c7c66a0fde7a30916bb4be1bb14c43f5eb139268309aa8b46f86caede
size 24388

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94
size 35451
oid sha256:b007380b5ce761ff5d23665dcaa2729e1795c5192efdf366007ddbcea0ed64a5
size 35463

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:03ee62427611101758958adf2650a4a0eea4e023f07c9ec4ebc63425233e8a04
size 554949
oid sha256:1b618443ba6e8483425972bd95fced23d2cd5ff4ad05277a6171eac14c255302
size 572382

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82ef265f0e22649c7fcdb9556879c1a30df582bd4e97c647258b3e5acc03d112
size 771298
oid sha256:00681d206ae05c2135dcc61e87a31f18248fc972804a01bc3440faf4fdd1a50e
size 796392

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cad71b486a479eb9c5339a93f4acc3df2d0b6b188ad023b9b044be7311b0ab72
size 918775
oid sha256:a21e2cc32a032ee44516c495c68aa5f6e168da1bea44396db6e67889d5714e7f
size 948339

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc9ed4d29f4227b9d38b477ee8f546ea8597acda56a6909ba4826891ebdbea01
size 1039263
oid sha256:7907a5851f4b5a986cbd74b494b65cff7a469038af63ac957d55e848bb3391a8
size 1072165

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9bf826bee811d8af345ec1281266fc9bef6d7c3782279516984a6c75130a929
size 1130895
oid sha256:b220801c49d7d1f4364cf6c4f2098123e34ce782bb1439b2896d8adf9215a0be
size 1166343

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9345de28f09e2891fd01db20bb0b94176ec3c89d8c2f344a6640d33e97ab5400
size 1311417
oid sha256:0ff8e54c66f64396b42bb962297eab966089318b1a75e65accef7abbeb7d6cee
size 1353321

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2
size 158553
oid sha256:78335f9233990c5622d1f6f0f18a3b44e33b0e68061e865641b0b316072489ba
size 158496