Support additive colors in color picker

This commit is contained in:
Emil Ernerfeldt 2021-01-03 18:03:11 +01:00
parent 5c8df6925d
commit 10a23d18e1
17 changed files with 239 additions and 106 deletions

View file

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed 🔧 ### Changed 🔧
* Renamed `Srgba` to `Color32`. * Renamed `Srgba` to `Color32`.
* All color contructions now starts with `from_`, e.g. `Color32::from_rgb`.
* Renamed `FontFamily::VariableWidth` to `FontFamily::Proportional`. * Renamed `FontFamily::VariableWidth` to `FontFamily::Proportional`.
* Removed `pixels_per_point` from `FontDefinitions`. * Removed `pixels_per_point` from `FontDefinitions`.

View file

@ -73,7 +73,7 @@ impl Frame {
Self { Self {
margin: Vec2::new(10.0, 10.0), margin: Vec2::new(10.0, 10.0),
corner_radius: 5.0, corner_radius: 5.0,
fill: Color32::black_alpha(250), fill: Color32::from_black_alpha(250),
stroke: style.visuals.widgets.noninteractive.bg_stroke, stroke: style.visuals.widgets.noninteractive.bg_stroke,
..Default::default() ..Default::default()
} }

View file

@ -70,19 +70,19 @@ impl Color32 {
Self([r, g, b, a]) Self([r, g, b, a])
} }
pub const fn gray(l: u8) -> Self { pub const fn from_gray(l: u8) -> Self {
Self([l, l, l, 255]) Self([l, l, l, 255])
} }
pub const fn black_alpha(a: u8) -> Self { pub const fn from_black_alpha(a: u8) -> Self {
Self([0, 0, 0, a]) Self([0, 0, 0, a])
} }
pub fn white_alpha(a: u8) -> Self { pub fn from_white_alpha(a: u8) -> Self {
Rgba::white_alpha(linear_from_alpha_byte(a)).into() Rgba::from_white_alpha(linear_from_alpha_byte(a)).into()
} }
pub const fn additive_luminance(l: u8) -> Self { pub const fn from_additive_luminance(l: u8) -> Self {
Self([l, l, l, 0]) Self([l, l, l, 0])
} }
@ -138,43 +138,49 @@ impl std::ops::IndexMut<usize> for Rgba {
} }
impl Rgba { impl Rgba {
pub const TRANSPARENT: Rgba = Rgba::new(0.0, 0.0, 0.0, 0.0); pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
pub const BLACK: Rgba = Rgba::new(0.0, 0.0, 0.0, 1.0); pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
pub const WHITE: Rgba = Rgba::new(1.0, 1.0, 1.0, 1.0); pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
pub const RED: Rgba = Rgba::new(1.0, 0.0, 0.0, 1.0); pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
pub const GREEN: Rgba = Rgba::new(0.0, 1.0, 0.0, 1.0); pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
pub const BLUE: Rgba = Rgba::new(0.0, 0.0, 1.0, 1.0); pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
Self([r, g, b, a]) Self([r, g, b, a])
} }
pub const fn rgb(r: f32, g: f32, b: f32) -> Self { pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
Self([r, g, b, 1.0]) Self([r, g, b, 1.0])
} }
pub const fn gray(l: f32) -> Self { pub const fn from_gray(l: f32) -> Self {
Self([l, l, l, 1.0]) Self([l, l, l, 1.0])
} }
pub fn luminance_alpha(l: f32, a: f32) -> Self { pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
debug_assert!(0.0 <= l && l <= 1.0); debug_assert!(0.0 <= l && l <= 1.0);
debug_assert!(0.0 <= a && a <= 1.0); debug_assert!(0.0 <= a && a <= 1.0);
Self([l * a, l * a, l * a, a]) Self([l * a, l * a, l * a, a])
} }
/// Transparent black /// Transparent black
pub fn black_alpha(a: f32) -> Self { pub fn from_black_alpha(a: f32) -> Self {
debug_assert!(0.0 <= a && a <= 1.0); debug_assert!(0.0 <= a && a <= 1.0);
Self([0.0, 0.0, 0.0, a]) Self([0.0, 0.0, 0.0, a])
} }
/// Transparent white /// Transparent white
pub fn white_alpha(a: f32) -> Self { pub fn from_white_alpha(a: f32) -> Self {
debug_assert!(0.0 <= a && a <= 1.0); debug_assert!(0.0 <= a && a <= 1.0);
Self([a, a, a, a]) Self([a, a, a, a])
} }
/// Return an additive version of this color (alpha = 0)
pub fn additive(self) -> Self {
let [r, g, b, _] = self.0;
Self([r, g, b, 0.0])
}
/// Multiply with e.g. 0.5 to make us half transparent /// Multiply with e.g. 0.5 to make us half transparent
pub fn multiply(self, alpha: f32) -> Self { pub fn multiply(self, alpha: f32) -> Self {
Self([ Self([
@ -206,11 +212,11 @@ impl Rgba {
/// Returns an opaque version of self /// Returns an opaque version of self
pub fn to_opaque(&self) -> Self { pub fn to_opaque(&self) -> Self {
if self.a() == 0.0 { if self.a() == 0.0 {
// additive or fully transparent // Additive or fully transparent black.
Self::new(self.r(), self.g(), self.b(), 1.0) Self::from_rgba_premultiplied(self.r(), self.g(), self.b(), 1.0)
} else { } else {
// un-multiply alpha // un-multiply alpha:
Self::new( Self::from_rgba_premultiplied(
self.r() / self.a(), self.r() / self.a(),
self.g() / self.a(), self.g() / self.a(),
self.b() / self.a(), self.b() / self.a(),
@ -345,7 +351,7 @@ pub struct Hsva {
pub s: f32, pub s: f32,
/// value 0-1 /// value 0-1
pub v: f32, pub v: f32,
/// alpha 0-1 /// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
pub a: f32, pub a: f32,
} }
@ -375,33 +381,79 @@ impl Hsva {
} }
/// From linear RGBA with premultiplied alpha /// From linear RGBA with premultiplied alpha
pub fn from_rgba_premultiplied(rgba: [f32; 4]) -> Self { pub fn from_rgba_premultiplied([r, g, b, a]: [f32; 4]) -> Self {
#![allow(clippy::many_single_char_names)] #![allow(clippy::many_single_char_names)]
let [r, g, b, a] = rgba;
if a == 0.0 { if a == 0.0 {
Hsva::default() if r == 0.0 && b == 0.0 && a == 0.0 {
Hsva::default()
} else {
Hsva::from_additive_rgb([r, g, b])
}
} else { } else {
let (h, s, v) = hsv_from_rgb((r / a, g / a, b / a)); let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
Hsva { h, s, v, a } Hsva { h, s, v, a }
} }
} }
/// From linear RGBA without premultiplied alpha /// From linear RGBA without premultiplied alpha
pub fn from_rgba_unmultiplied(rgba: [f32; 4]) -> Self { pub fn from_rgba_unmultiplied([r, g, b, a]: [f32; 4]) -> Self {
#![allow(clippy::many_single_char_names)] #![allow(clippy::many_single_char_names)]
let [r, g, b, a] = rgba; let (h, s, v) = hsv_from_rgb([r, g, b]);
let (h, s, v) = hsv_from_rgb((r, g, b));
Hsva { h, s, v, a } Hsva { h, s, v, a }
} }
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
let (h, s, v) = hsv_from_rgb(rgb);
Hsva {
h,
s,
v,
a: -0.5, // anything negative is treated as additive
}
}
pub fn from_rgb(rgb: [f32; 3]) -> Self {
let (h, s, v) = hsv_from_rgb(rgb);
Hsva { h, s, v, a: 1.0 }
}
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
Self::from_rgb([
linear_from_gamma_byte(r),
linear_from_gamma_byte(g),
linear_from_gamma_byte(b),
])
}
// ------------------------------------------------------------------------
pub fn to_rgb(&self) -> [f32; 3] {
rgb_from_hsv((self.h, self.s, self.v))
}
pub fn to_srgb(&self) -> [u8; 3] {
let [r, g, b] = self.to_rgb();
[
gamma_byte_from_linear(r),
gamma_byte_from_linear(g),
gamma_byte_from_linear(b),
]
}
pub fn to_rgba_premultiplied(&self) -> [f32; 4] { pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied(); let [r, g, b, a] = self.to_rgba_unmultiplied();
[a * r, a * g, a * b, a] let additive = a < 0.0;
if additive {
[r, g, b, 0.0]
} else {
[a * r, a * g, a * b, a]
}
} }
/// Represents additive colors using a negative alpha.
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
let Hsva { h, s, v, a } = *self; let Hsva { h, s, v, a } = *self;
let (r, g, b) = rgb_from_hsv((h, s, v)); let [r, g, b] = rgb_from_hsv((h, s, v));
[r, g, b, a] [r, g, b, a]
} }
@ -421,7 +473,7 @@ impl Hsva {
gamma_byte_from_linear(r), gamma_byte_from_linear(r),
gamma_byte_from_linear(g), gamma_byte_from_linear(g),
gamma_byte_from_linear(b), gamma_byte_from_linear(b),
alpha_byte_from_linear(a), alpha_byte_from_linear(a.abs()),
] ]
} }
} }
@ -449,7 +501,7 @@ impl From<Color32> for Hsva {
} }
/// All ranges in 0-1, rgb is linear. /// All ranges in 0-1, rgb is linear.
pub fn hsv_from_rgb((r, g, b): (f32, f32, f32)) -> (f32, f32, f32) { pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
#![allow(clippy::float_cmp)] #![allow(clippy::float_cmp)]
#![allow(clippy::many_single_char_names)] #![allow(clippy::many_single_char_names)]
let min = r.min(g.min(b)); let min = r.min(g.min(b));
@ -473,7 +525,7 @@ pub fn hsv_from_rgb((r, g, b): (f32, f32, f32)) -> (f32, f32, f32) {
} }
/// All ranges in 0-1, rgb is linear. /// All ranges in 0-1, rgb is linear.
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> (f32, f32, f32) { pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
#![allow(clippy::many_single_char_names)] #![allow(clippy::many_single_char_names)]
let h = (h.fract() + 1.0).fract(); // wrap let h = (h.fract() + 1.0).fract(); // wrap
let s = clamp(s, 0.0..=1.0); let s = clamp(s, 0.0..=1.0);
@ -484,12 +536,12 @@ pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> (f32, f32, f32) {
let t = v * (1.0 - (1.0 - f) * s); let t = v * (1.0 - (1.0 - f) * s);
match (h * 6.0).floor() as i32 % 6 { match (h * 6.0).floor() as i32 % 6 {
0 => (v, t, p), 0 => [v, t, p],
1 => (q, v, p), 1 => [q, v, p],
2 => (p, v, t), 2 => [p, v, t],
3 => (p, q, v), 3 => [p, q, v],
4 => (t, p, v), 4 => [t, p, v],
5 => (v, p, q), 5 => [v, p, q],
_ => unreachable!(), _ => unreachable!(),
} }
} }

View file

@ -13,7 +13,7 @@ impl Shadow {
pub fn small() -> Self { pub fn small() -> Self {
Self { Self {
extrusion: 8.0, extrusion: 8.0,
color: Color32::black_alpha(64), color: Color32::from_black_alpha(64),
} }
} }
@ -21,7 +21,7 @@ impl Shadow {
pub fn big() -> Self { pub fn big() -> Self {
Self { Self {
extrusion: 32.0, extrusion: 32.0,
color: Color32::black_alpha(96), color: Color32::from_black_alpha(96),
} }
} }

View file

@ -7,7 +7,7 @@ pub struct Texture {
pub version: u64, pub version: u64,
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
/// luminance and alpha, linear space 0-255 /// White color with the given alpha (linear space 0-255).
pub pixels: Vec<u8>, pub pixels: Vec<u8>,
} }
@ -15,7 +15,8 @@ impl Texture {
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom. /// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
pub fn srgba_pixels(&'_ self) -> impl Iterator<Item = super::Color32> + '_ { pub fn srgba_pixels(&'_ self) -> impl Iterator<Item = super::Color32> + '_ {
use super::Color32; use super::Color32;
let srgba_from_luminance_lut: Vec<Color32> = (0..=255).map(Color32::white_alpha).collect(); let srgba_from_luminance_lut: Vec<Color32> =
(0..=255).map(Color32::from_white_alpha).collect();
self.pixels self.pixels
.iter() .iter()
.map(move |&l| srgba_from_luminance_lut[l as usize]) .map(move |&l| srgba_from_luminance_lut[l as usize])
@ -47,7 +48,7 @@ impl std::ops::IndexMut<(usize, usize)> for Texture {
pub struct TextureAtlas { pub struct TextureAtlas {
texture: Texture, texture: Texture,
/// Used for when adding new rects /// Used for when allocating new rectangles.
cursor: (usize, usize), cursor: (usize, usize),
row_height: usize, row_height: usize,
} }
@ -78,7 +79,7 @@ impl TextureAtlas {
pub fn allocate(&mut self, (w, h): (usize, usize)) -> (usize, usize) { pub fn allocate(&mut self, (w, h): (usize, usize)) -> (usize, usize) {
/// On some low-precision GPUs (my old iPad) characters get muddled up /// On some low-precision GPUs (my old iPad) characters get muddled up
/// if we don't add some empty pixels between the characters. /// if we don't add some empty pixels between the characters.
/// On modern high-precision GPUs this is not be needed. /// On modern high-precision GPUs this is not needed.
const PADDING: usize = 1; const PADDING: usize = 1;
assert!(w <= self.texture.width); assert!(w <= self.texture.width);

View file

@ -147,7 +147,7 @@ impl Painter {
self.add(PaintCmd::Rect { self.add(PaintCmd::Rect {
rect: frame_rect, rect: frame_rect,
corner_radius: 0.0, corner_radius: 0.0,
fill: Color32::black_alpha(240), fill: Color32::from_black_alpha(240),
stroke: Stroke::new(1.0, Color32::RED), stroke: Stroke::new(1.0, Color32::RED),
}); });
self.galley(rect.min, galley, text_style, Color32::RED); self.galley(rect.min, galley, text_style, Color32::RED);

View file

@ -279,7 +279,7 @@ impl Default for Visuals {
override_text_color: None, override_text_color: None,
widgets: Default::default(), widgets: Default::default(),
selection: Default::default(), selection: Default::default(),
dark_bg_color: Color32::black_alpha(140), dark_bg_color: Color32::from_black_alpha(140),
hyperlink_color: Color32::from_rgb(90, 170, 255), hyperlink_color: Color32::from_rgb(90, 170, 255),
window_corner_radius: 10.0, window_corner_radius: 10.0,
window_shadow: Shadow::big(), window_shadow: Shadow::big(),
@ -296,8 +296,11 @@ impl Default for Visuals {
impl Default for Selection { impl Default for Selection {
fn default() -> Self { fn default() -> Self {
Self { Self {
bg_fill: Rgba::new(0.0, 0.5, 1.0, 0.0).multiply(0.15).into(), // additive! bg_fill: Rgba::from_rgb(0.0, 0.5, 1.0)
stroke: Stroke::new(1.0, Rgba::new(0.3, 0.6, 1.0, 1.0)), .additive()
.multiply(0.10)
.into(),
stroke: Stroke::new(1.0, Rgba::from_rgb(0.3, 0.6, 1.0)),
} }
} }
} }
@ -306,39 +309,39 @@ impl Default for Widgets {
fn default() -> Self { fn default() -> Self {
Self { Self {
active: WidgetVisuals { active: WidgetVisuals {
bg_fill: Rgba::luminance_alpha(0.10, 0.5).into(), bg_fill: Rgba::from_luminance_alpha(0.10, 0.5).into(),
bg_stroke: Stroke::new(2.0, Color32::WHITE), bg_stroke: Stroke::new(2.0, Color32::WHITE),
corner_radius: 4.0, corner_radius: 4.0,
fg_fill: Color32::from_rgb(120, 120, 200), fg_fill: Color32::from_rgb(120, 120, 200),
fg_stroke: Stroke::new(2.0, Color32::WHITE), fg_stroke: Stroke::new(2.0, Color32::WHITE),
}, },
hovered: WidgetVisuals { hovered: WidgetVisuals {
bg_fill: Rgba::luminance_alpha(0.06, 0.5).into(), bg_fill: Rgba::from_luminance_alpha(0.06, 0.5).into(),
bg_stroke: Stroke::new(1.0, Rgba::white_alpha(0.5)), bg_stroke: Stroke::new(1.0, Rgba::from_white_alpha(0.5)),
corner_radius: 4.0, corner_radius: 4.0,
fg_fill: Color32::from_rgb(100, 100, 150), fg_fill: Color32::from_rgb(100, 100, 150),
fg_stroke: Stroke::new(1.5, Color32::gray(240)), fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
}, },
inactive: WidgetVisuals { inactive: WidgetVisuals {
bg_fill: Rgba::luminance_alpha(0.04, 0.5).into(), bg_fill: Rgba::from_luminance_alpha(0.04, 0.5).into(),
bg_stroke: Stroke::new(1.0, Rgba::white_alpha(0.06)), // default window outline. Should be pretty readable bg_stroke: Stroke::new(1.0, Rgba::from_white_alpha(0.06)), // default window outline. Should be pretty readable
corner_radius: 4.0, corner_radius: 4.0,
fg_fill: Color32::from_rgb(60, 60, 80), fg_fill: Color32::from_rgb(60, 60, 80),
fg_stroke: Stroke::new(1.0, Color32::gray(200)), // Should NOT look grayed out! fg_stroke: Stroke::new(1.0, Color32::from_gray(200)), // Should NOT look grayed out!
}, },
disabled: WidgetVisuals { disabled: WidgetVisuals {
bg_fill: Rgba::luminance_alpha(0.02, 0.5).into(), bg_fill: Rgba::from_luminance_alpha(0.02, 0.5).into(),
bg_stroke: Stroke::new(0.5, Color32::gray(70)), bg_stroke: Stroke::new(0.5, Color32::from_gray(70)),
corner_radius: 4.0, corner_radius: 4.0,
fg_fill: Color32::from_rgb(50, 50, 50), fg_fill: Color32::from_rgb(50, 50, 50),
fg_stroke: Stroke::new(1.0, Color32::gray(140)), // Should look grayed out fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // Should look grayed out
}, },
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
bg_stroke: Stroke::new(1.0, Rgba::white_alpha(0.06)), bg_stroke: Stroke::new(1.0, Rgba::from_white_alpha(0.06)),
bg_fill: Rgba::luminance_alpha(0.010, 0.975).into(), // window background bg_fill: Rgba::from_luminance_alpha(0.010, 0.975).into(), // window background
corner_radius: 4.0, corner_radius: 4.0,
fg_fill: Default::default(), fg_fill: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::gray(160)), // text color fg_stroke: Stroke::new(1.0, Color32::from_gray(160)), // text color
}, },
} }
} }

View file

@ -814,13 +814,35 @@ impl Ui {
/// Shows a button with the given color. /// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown. /// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button_srgba(&mut self, srgba: &mut Color32) -> Response { pub fn color_edit_button_srgba(&mut self, srgba: &mut Color32) -> Response {
widgets::color_picker::color_edit_button_srgba(self, srgba) color_picker::color_edit_button_srgba(self, srgba, color_picker::Alpha::BlendOrAdditive)
} }
/// Shows a button with the given color. /// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown. /// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button_hsva(&mut self, hsva: &mut Hsva) -> Response { pub fn color_edit_button_hsva(&mut self, hsva: &mut Hsva) -> Response {
widgets::color_picker::color_edit_button_hsva(self, hsva) 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 {
let mut hsva = Hsva::from_srgb(*srgb);
let response =
color_picker::color_edit_button_hsva(self, &mut hsva, color_picker::Alpha::Opaque);
*srgb = hsva.to_srgb();
response
}
/// 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 {
let mut hsva = Hsva::from_rgb(*rgb);
let response =
color_picker::color_edit_button_hsva(self, &mut hsva, color_picker::Alpha::Opaque);
*rgb = hsva.to_rgb();
response
} }
/// Shows a button with the given color. /// Shows a button with the given color.
@ -839,7 +861,8 @@ impl Ui {
/// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use. /// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use.
pub fn color_edit_button_srgba_unmultiplied(&mut self, srgba: &mut [u8; 4]) -> Response { pub fn color_edit_button_srgba_unmultiplied(&mut self, srgba: &mut [u8; 4]) -> Response {
let mut hsva = Hsva::from_srgba_unmultiplied(*srgba); let mut hsva = Hsva::from_srgba_unmultiplied(*srgba);
let response = self.color_edit_button_hsva(&mut hsva); let response =
color_picker::color_edit_button_hsva(self, &mut hsva, color_picker::Alpha::OnlyBlend);
*srgba = hsva.to_srgba_unmultiplied(); *srgba = hsva.to_srgba_unmultiplied();
response response
} }
@ -849,7 +872,11 @@ impl Ui {
/// The given color is in linear RGBA space with premultiplied alpha /// The given color is in linear RGBA space with premultiplied alpha
pub fn color_edit_button_rgba_premultiplied(&mut self, rgba: &mut [f32; 4]) -> Response { pub fn color_edit_button_rgba_premultiplied(&mut self, rgba: &mut [f32; 4]) -> Response {
let mut hsva = Hsva::from_rgba_premultiplied(*rgba); let mut hsva = Hsva::from_rgba_premultiplied(*rgba);
let response = self.color_edit_button_hsva(&mut hsva); let response = color_picker::color_edit_button_hsva(
self,
&mut hsva,
color_picker::Alpha::BlendOrAdditive,
);
*rgba = hsva.to_rgba_premultiplied(); *rgba = hsva.to_rgba_premultiplied();
response response
} }
@ -860,7 +887,8 @@ impl Ui {
/// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use. /// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use.
pub fn color_edit_button_rgba_unmultiplied(&mut self, rgba: &mut [f32; 4]) -> Response { pub fn color_edit_button_rgba_unmultiplied(&mut self, rgba: &mut [f32; 4]) -> Response {
let mut hsva = Hsva::from_rgba_unmultiplied(*rgba); let mut hsva = Hsva::from_rgba_unmultiplied(*rgba);
let response = self.color_edit_button_hsva(&mut hsva); let response =
color_picker::color_edit_button_hsva(self, &mut hsva, color_picker::Alpha::OnlyBlend);
*rgba = hsva.to_rgba_unmultiplied(); *rgba = hsva.to_rgba_unmultiplied();
response response
} }

View file

@ -16,11 +16,11 @@ fn contrast_color(color: impl Into<Rgba>) -> Color32 {
/// Number of vertices per dimension in the color sliders. /// Number of vertices per dimension in the color sliders.
/// We need at least 6 for hues, and more for smooth 2D areas. /// We need at least 6 for hues, and more for smooth 2D areas.
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°). /// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
const N: u32 = 6 * 3; const N: u32 = 6 * 6;
fn background_checkers(painter: &Painter, rect: Rect) { fn background_checkers(painter: &Painter, rect: Rect) {
let mut top_color = Color32::gray(128); let mut top_color = Color32::from_gray(128);
let mut bottom_color = Color32::gray(32); let mut bottom_color = Color32::from_gray(32);
let checker_size = Vec2::splat(rect.height() / 2.0); let checker_size = Vec2::splat(rect.height() / 2.0);
let n = (rect.width() / checker_size.x).round() as u32; let n = (rect.width() / checker_size.x).round() as u32;
@ -184,20 +184,68 @@ fn color_slider_2d(
response response
} }
fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma) { /// What options to show for alpha
#[derive(Clone, Copy, PartialEq)]
pub enum Alpha {
// Set alpha to 1.0, and show no option for it.
Opaque,
// Only show normal blend options for it.
OnlyBlend,
// Show both blend and additive options.
BlendOrAdditive,
}
fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) {
ui.vertical(|ui| { ui.vertical(|ui| {
let current_color_size = vec2( let current_color_size = vec2(
ui.style().spacing.slider_width, ui.style().spacing.slider_width,
ui.style().spacing.interact_size.y * 2.0, ui.style().spacing.interact_size.y * 2.0,
); );
show_color(ui, *hsva, current_color_size).on_hover_text("Current color");
show_color(ui, HsvaGamma { a: 1.0, ..*hsva }, current_color_size)
.on_hover_text("Current color (opaque)");
let opaque = HsvaGamma { a: 1.0, ..*hsva }; let opaque = HsvaGamma { a: 1.0, ..*hsva };
let HsvaGamma { h, s, v, a } = hsva;
if alpha == Alpha::Opaque {
hsva.a = 1.0;
show_color(ui, *hsva, current_color_size).on_hover_text("Current color");
} else {
let a = &mut hsva.a;
// We signal additive blending by storing a negative alpha (a bit ironic).
let mut additive = *a < 0.0;
if alpha == Alpha::OnlyBlend {
if additive {
*a = 0.5;
}
color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
} else {
ui.horizontal(|ui| {
ui.label("Blending:");
ui.radio_value(&mut additive, false, "Normal");
ui.radio_value(&mut additive, true, "Additive");
if additive {
*a = -a.abs();
}
if !additive {
*a = a.abs();
}
});
if !additive {
color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into())
.on_hover_text("Alpha");
}
}
show_color(ui, *hsva, current_color_size).on_hover_text("Current color");
show_color(ui, opaque, current_color_size).on_hover_text("Current color (opaque)");
}
let HsvaGamma { h, s, v, a: _ } = hsva;
color_slider_2d(ui, h, s, |h, s| HsvaGamma::new(h, s, 1.0, 1.0).into()) color_slider_2d(ui, h, s, |h, s| HsvaGamma::new(h, s, 1.0, 1.0).into())
.on_hover_text("Hue - Saturation"); .on_hover_text("Hue - Saturation");
color_slider_2d(ui, v, s, |v, s| HsvaGamma { v, s, ..opaque }.into()) color_slider_2d(ui, v, s, |v, s| HsvaGamma { v, s, ..opaque }.into())
@ -205,17 +253,16 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma) {
color_slider_1d(ui, h, |h| HsvaGamma { h, ..opaque }.into()).on_hover_text("Hue"); color_slider_1d(ui, h, |h| HsvaGamma { h, ..opaque }.into()).on_hover_text("Hue");
color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation"); color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation");
color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value"); color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value");
color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
}); });
} }
fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva) { fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) {
let mut hsvag = HsvaGamma::from(*hsva); let mut hsvag = HsvaGamma::from(*hsva);
color_picker_hsvag_2d(ui, &mut hsvag); color_picker_hsvag_2d(ui, &mut hsvag, alpha);
*hsva = Hsva::from(hsvag); *hsva = Hsva::from(hsvag);
} }
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva) -> Response { pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let pupup_id = ui.auto_id_with("popup"); let pupup_id = ui.auto_id_with("popup");
let button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color"); let button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color");
@ -228,8 +275,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva) -> Response {
.order(Order::Foreground) .order(Order::Foreground)
.default_pos(button_response.rect.max) .default_pos(button_response.rect.max)
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
ui.style_mut().spacing.slider_width = 256.0;
Frame::popup(ui.style()).show(ui, |ui| { Frame::popup(ui.style()).show(ui, |ui| {
color_picker_hsva_2d(ui, hsva); color_picker_hsva_2d(ui, hsva, alpha);
}) })
}); });
@ -246,7 +294,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva) -> Response {
/// Shows a button with the given color. /// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown. /// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32) -> Response { pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> Response {
// To ensure we keep hue slider when `srgba` is grey we store the // To ensure we keep hue slider when `srgba` is grey we store the
// full `Hsva` in a cache: // full `Hsva` in a cache:
@ -258,7 +306,7 @@ pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32) -> Response {
.cloned() .cloned()
.unwrap_or_else(|| Hsva::from(*srgba)); .unwrap_or_else(|| Hsva::from(*srgba));
let response = color_edit_button_hsva(ui, &mut hsva); let response = color_edit_button_hsva(ui, &mut hsva, alpha);
*srgba = Color32::from(hsva); *srgba = Color32::from(hsva);
@ -279,7 +327,7 @@ struct HsvaGamma {
pub s: f32, pub s: f32,
/// value 0-1, in gamma-space (perceptually even) /// value 0-1, in gamma-space (perceptually even)
pub v: f32, pub v: f32,
/// alpha 0-1 /// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
pub a: f32, pub a: f32,
} }

View file

@ -39,8 +39,9 @@ impl epi::App for ColorTest {
if frame.is_web() { if frame.is_web() {
ui.colored_label( ui.colored_label(
RED, RED,
"NOTE: The current WebGL backend does NOT pass the color test!", "NOTE: The WebGL backend does NOT pass the color test."
); );
ui.small("This is because WebGL does not support a linear framebuffer blending (not even WebGL2!).\nMaybe when WebGL3 becomes mainstream in 2030 the web can finally get colors right?");
ui.separator(); ui.separator();
} }
ScrollArea::auto_sized().show(ui, |ui| { ScrollArea::auto_sized().show(ui, |ui| {
@ -56,8 +57,7 @@ impl ColorTest {
ui: &mut Ui, ui: &mut Ui,
mut tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>, mut tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
) { ) {
ui.label("This is made to test if your Egui painter backend is set up correctly"); ui.label("This is made to test that the Egui painter backend is set up correctly, so that all colors are interpolated and blended in linear space with premultiplied alpha.");
ui.label("It is meant to ensure you do proper sRGBA decoding of both texture and vertex colors, and blend using premultiplied alpha.");
ui.label("If everything is set up correctly, all groups of gradients will look uniform"); ui.label("If everything is set up correctly, all groups of gradients will look uniform");
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients"); ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
@ -85,8 +85,8 @@ impl ColorTest {
ui.wrap(|ui| { ui.wrap(|ui| {
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
let tex_color = Rgba::new(1.0, 0.25, 0.25, 1.0); let tex_color = Rgba::from_rgb(1.0, 0.25, 0.25);
let vertex_color = Rgba::new(0.5, 0.75, 0.75, 1.0); let vertex_color = Rgba::from_rgb(0.5, 0.75, 0.75);
ui.horizontal(|ui| { ui.horizontal(|ui| {
let color_size = ui.style().spacing.interact_size; let color_size = ui.style().spacing.interact_size;

View file

@ -57,7 +57,7 @@ impl super::View for DancingStrings {
let thickness = 10.0 / mode; let thickness = 10.0 / mode;
cmds.push(paint::PaintCmd::line( cmds.push(paint::PaintCmd::line(
points, points,
Stroke::new(thickness, Color32::additive_luminance(196)), Stroke::new(thickness, Color32::from_additive_luminance(196)),
)); ));
} }

View file

@ -93,7 +93,7 @@ impl DemoWindow {
let painter = ui.painter(); let painter = ui.painter();
let c = response.rect.center(); let c = response.rect.center();
let r = response.rect.width() / 2.0 - 1.0; let r = response.rect.width() / 2.0 - 1.0;
let color = Color32::gray(128); let color = Color32::from_gray(128);
let stroke = Stroke::new(1.0, color); let stroke = Stroke::new(1.0, color);
painter.circle_stroke(c, r, stroke); painter.circle_stroke(c, r, stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
@ -210,7 +210,7 @@ impl BoxPainting {
ui.painter().rect( ui.painter().rect(
response.rect, response.rect,
self.corner_radius, self.corner_radius,
Color32::gray(64), Color32::from_gray(64),
Stroke::new(self.stroke_width, Color32::WHITE), Stroke::new(self.stroke_width, Color32::WHITE),
); );
} }

View file

@ -40,8 +40,8 @@ pub fn toggle(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
// "how should something that is being interacted with be painted?". // "how should something that is being interacted with be painted?".
// This will, for instance, give us different colors when the widget is hovered or clicked. // This will, for instance, give us different colors when the widget is hovered or clicked.
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
let off_bg_fill = egui::Rgba::new(0.0, 0.0, 0.0, 0.0); let off_bg_fill = egui::Rgba::TRANSPARENT;
let on_bg_fill = egui::Rgba::new(0.0, 0.5, 0.25, 1.0); let on_bg_fill = egui::Rgba::from_rgb(0.0, 0.5, 0.25);
let bg_fill = egui::lerp(off_bg_fill..=on_bg_fill, how_on); let bg_fill = egui::lerp(off_bg_fill..=on_bg_fill, how_on);
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements. // All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
let rect = response.rect; let rect = response.rect;
@ -67,8 +67,8 @@ fn toggle_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
let how_on = ui.ctx().animate_bool(response.id, *on); let how_on = ui.ctx().animate_bool(response.id, *on);
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
let off_bg_fill = egui::Rgba::new(0.0, 0.0, 0.0, 0.0); let off_bg_fill = egui::Rgba::TRANSPARENT;
let on_bg_fill = egui::Rgba::new(0.0, 0.5, 0.25, 1.0); let on_bg_fill = egui::Rgba::from_rgb(0.0, 0.5, 0.25);
let bg_fill = egui::lerp(off_bg_fill..=on_bg_fill, how_on); let bg_fill = egui::lerp(off_bg_fill..=on_bg_fill, how_on);
let rect = response.rect; let rect = response.rect;
let radius = 0.5 * rect.height(); let radius = 0.5 * rect.height();

View file

@ -35,7 +35,7 @@ impl Default for Widgets {
count: 0, count: 0,
sliders: Default::default(), sliders: Default::default(),
angle: std::f32::consts::TAU / 3.0, angle: std::f32::consts::TAU / 3.0,
color: (Rgba::new(0.0, 1.0, 0.5, 1.0) * 0.75).into(), color: (Rgba::from_rgb(0.0, 1.0, 0.5) * 0.75).into(),
single_line_text_input: "Hello World!".to_owned(), single_line_text_input: "Hello World!".to_owned(),
multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(), multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
toggle_switch: false, toggle_switch: false,

View file

@ -58,7 +58,7 @@ impl FractalClock {
ui.expand_to_include_rect(painter.clip_rect()); ui.expand_to_include_rect(painter.clip_rect());
Frame::popup(ui.style()) Frame::popup(ui.style())
.fill(Rgba::luminance_alpha(0.02, 0.5).into()) .fill(Rgba::from_luminance_alpha(0.02, 0.5).into())
.stroke(Stroke::none()) .stroke(Stroke::none())
.show(ui, |ui| { .show(ui, |ui| {
ui.set_max_width(270.0); ui.set_max_width(270.0);
@ -160,7 +160,7 @@ impl FractalClock {
for (i, hand) in hands.iter().enumerate() { for (i, hand) in hands.iter().enumerate() {
let center = pos2(0.0, 0.0); let center = pos2(0.0, 0.0);
let end = center + hand.vec; let end = center + hand.vec;
paint_line([center, end], Color32::additive_luminance(255), width); paint_line([center, end], Color32::from_additive_luminance(255), width);
if i < 2 { if i < 2 {
nodes.push(Node { nodes.push(Node {
pos: end, pos: end,
@ -190,7 +190,7 @@ impl FractalClock {
}; };
paint_line( paint_line(
[a.pos, b.pos], [a.pos, b.pos],
Color32::additive_luminance(luminance_u8), Color32::from_additive_luminance(luminance_u8),
width, width,
); );
new_nodes.push(b); new_nodes.push(b);

View file

@ -78,7 +78,7 @@ impl FrameHistory {
}]; }];
let rect = rect.shrink(4.0); let rect = rect.shrink(4.0);
let line_stroke = Stroke::new(1.0, Color32::additive_luminance(128)); let line_stroke = Stroke::new(1.0, Color32::from_additive_luminance(128));
if let Some(mouse_pos) = ui.input().mouse.pos { if let Some(mouse_pos) = ui.input().mouse.pos {
if rect.contains(mouse_pos) { if rect.contains(mouse_pos) {
@ -100,7 +100,7 @@ impl FrameHistory {
} }
} }
let circle_color = Color32::additive_luminance(196); let circle_color = Color32::from_additive_luminance(196);
let radius = 2.0; let radius = 2.0;
let right_side_time = ui.input().time; // Time at right side of screen let right_side_time = ui.input().time; // Time at right side of screen

View file

@ -109,7 +109,7 @@ impl Painter {
.chunks(texture.width as usize) .chunks(texture.width as usize)
.map(|row| { .map(|row| {
row.iter() row.iter()
.map(|&a| Color32::white_alpha(a).to_tuple()) .map(|&a| Color32::from_white_alpha(a).to_tuple())
.collect() .collect()
}) })
.collect(); .collect();