From bc0d6baefb705ff1d48021101616010ddf59b8e8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 9 Sep 2020 11:23:32 +0200 Subject: [PATCH] [color-picker] edit your own (s)RGBA arrays Both with and without premultiplied alpha --- TODO.md | 6 +- egui/src/demos/app.rs | 82 +++++++++++++++++++- egui/src/paint/color.rs | 96 ++++++++++++++++++++--- egui/src/style.rs | 4 +- egui/src/ui.rs | 63 +++++++++++++-- egui/src/widgets/color_picker.rs | 128 +++++++++++++++++++++++++++---- 6 files changed, 340 insertions(+), 39 deletions(-) diff --git a/TODO.md b/TODO.md index 7afd0874..1d798b8e 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,11 @@ TODO-list for the Egui project. If you looking for something to do, look here. * [x] linear rgb <-> sRGB * [x] HSV * [x] Color edit button with popup color picker - * [ ] Easily edit users own (s)RGBA quadruplets (`&mut [u8;4]`/`[f32;4]`) + * [x] Gamma for value (brightness) slider + * [x] Easily edit users own (s)RGBA quadruplets (`&mut [u8;4]`/`[f32;4]`) + * [ ] RGB editing without alpha + * [ ] Additive blending aware color picker + * [ ] Premultiplied alpha is a bit of a pain in the ass. Maybe rethink this a bit. * Containers * [ ] Scroll areas * [x] Vertical scrolling diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index cb524857..fc7a1987 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -312,6 +312,7 @@ pub struct DemoWindow { num_columns: usize, widgets: Widgets, + colors: ColorWidgets, layout: LayoutDemo, tree: Tree, box_painting: BoxPainting, @@ -324,6 +325,7 @@ impl Default for DemoWindow { num_columns: 2, widgets: Default::default(), + colors: Default::default(), layout: Default::default(), tree: Tree::demo(), box_painting: Default::default(), @@ -351,6 +353,12 @@ impl DemoWindow { self.widgets.ui(ui); }); + CollapsingHeader::new("Colors") + .default_open(true) + .show(ui, |ui| { + self.colors.ui(ui); + }); + CollapsingHeader::new("Layout") .default_open(false) .show(ui, |ui| self.layout.ui(ui)); @@ -531,7 +539,7 @@ impl Widgets { ui.horizontal_centered(|ui| { ui.add(Label::new("Click to select a different text color: ").text_color(self.color)); - ui.color_edit_button(&mut self.color); + ui.color_edit_button_srgba(&mut self.color); }); ui.separator(); @@ -552,6 +560,78 @@ impl Widgets { // ---------------------------------------------------------------------------- +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +struct ColorWidgets { + srgba_unmul: [u8; 4], + srgba_premul: [u8; 4], + rgba_unmul: [f32; 4], + rgba_premul: [f32; 4], +} + +impl Default for ColorWidgets { + fn default() -> Self { + // Approximately the same color. + ColorWidgets { + srgba_unmul: [0, 255, 183, 127], + srgba_premul: [0, 187, 140, 127], + rgba_unmul: [0.0, 1.0, 0.5, 0.5], + rgba_premul: [0.0, 0.5, 0.25, 0.5], + } + } +} + +impl ColorWidgets { + fn ui(&mut self, ui: &mut Ui) { + if ui.button("Reset").clicked { + *self = Default::default(); + } + + ui.label("Egui lets you edit colors stored as either sRGBA or linear RGBA and with or without premultiplied alpha"); + + let Self { + srgba_unmul, + srgba_premul, + rgba_unmul, + rgba_premul, + } = self; + + ui.horizontal_centered(|ui| { + ui.color_edit_button_srgba_unmultiplied(srgba_unmul); + ui.label(format!( + "sRGBA: {} {} {} {}", + srgba_unmul[0], srgba_unmul[1], srgba_unmul[2], srgba_unmul[3], + )); + }); + + ui.horizontal_centered(|ui| { + ui.color_edit_button_srgba_premultiplied(srgba_premul); + ui.label(format!( + "sRGBA with premultiplied alpha: {} {} {} {}", + srgba_premul[0], srgba_premul[1], srgba_premul[2], srgba_premul[3], + )); + }); + + ui.horizontal_centered(|ui| { + ui.color_edit_button_rgba_unmultiplied(rgba_unmul); + ui.label(format!( + "Linear RGBA: {:.02} {:.02} {:.02} {:.02}", + rgba_unmul[0], rgba_unmul[1], rgba_unmul[2], rgba_unmul[3], + )); + }); + + ui.horizontal_centered(|ui| { + ui.color_edit_button_rgba_premultiplied(rgba_premul); + ui.label(format!( + "Linear RGBA with premultiplied alpha: {:.02} {:.02} {:.02} {:.02}", + rgba_premul[0], rgba_premul[1], rgba_premul[2], rgba_premul[3], + )); + }); + } +} + +// ---------------------------------------------------------------------------- + #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] struct BoxPainting { diff --git a/egui/src/paint/color.rs b/egui/src/paint/color.rs index e60a603d..3e2b23f7 100644 --- a/egui/src/paint/color.rs +++ b/egui/src/paint/color.rs @@ -208,7 +208,7 @@ impl From for Rgba { linear_from_srgb_byte(srgba[0]), linear_from_srgb_byte(srgba[1]), linear_from_srgb_byte(srgba[2]), - srgba[3] as f32 / 255.0, + linear_from_alpha_byte(srgba[3]), ]) } } @@ -219,11 +219,12 @@ impl From for Srgba { srgb_byte_from_linear(rgba[0]), srgb_byte_from_linear(rgba[1]), srgb_byte_from_linear(rgba[2]), - clamp(rgba[3] * 255.0, 0.0..=255.0).round() as u8, + alpha_byte_from_linear(rgba[3]), ]) } } +/// [0, 255] -> [0, 1] fn linear_from_srgb_byte(s: u8) -> f32 { if s <= 10 { s as f32 / 3294.6 @@ -232,6 +233,11 @@ fn linear_from_srgb_byte(s: u8) -> f32 { } } +fn linear_from_alpha_byte(a: u8) -> f32 { + a as f32 / 255.0 +} + +/// [0, 1] -> [0, 255] fn srgb_byte_from_linear(l: f32) -> u8 { if l <= 0.0 { 0 @@ -244,6 +250,10 @@ fn srgb_byte_from_linear(l: f32) -> u8 { } } +fn alpha_byte_from_linear(a: f32) -> u8 { + clamp(a * 255.0, 0.0..=255.0).round() as u8 +} + #[test] fn test_srgba_conversion() { #![allow(clippy::float_cmp)] @@ -274,19 +284,31 @@ impl Hsva { pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self { Self { h, s, v, a } } -} -impl From for Rgba { - fn from(hsva: Hsva) -> Rgba { - let Hsva { h, s, v, a } = hsva; - let (r, g, b) = rgb_from_hsv((h, s, v)); - Rgba::new(a * r, a * g, a * b, a) + /// From `sRGBA` with premultiplied alpha + pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self { + Self::from_rgba_premultiplied([ + linear_from_srgb_byte(srgba[0]), + linear_from_srgb_byte(srgba[1]), + linear_from_srgb_byte(srgba[2]), + linear_from_alpha_byte(srgba[3]), + ]) } -} -impl From for Hsva { - fn from(rgba: Rgba) -> Hsva { + + /// From `sRGBA` without premultiplied alpha + pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { + Self::from_rgba_unmultiplied([ + linear_from_srgb_byte(srgba[0]), + linear_from_srgb_byte(srgba[1]), + linear_from_srgb_byte(srgba[2]), + linear_from_alpha_byte(srgba[3]), + ]) + } + + /// From linear RGBA with premultiplied alpha + pub fn from_rgba_premultiplied(rgba: [f32; 4]) -> Self { #![allow(clippy::many_single_char_names)] - let Rgba([r, g, b, a]) = rgba; + let [r, g, b, a] = rgba; if a == 0.0 { Hsva::default() } else { @@ -294,6 +316,56 @@ impl From for Hsva { Hsva { h, s, v, a } } } + + /// From linear RGBA without premultiplied alpha + pub fn from_rgba_unmultiplied(rgba: [f32; 4]) -> Self { + #![allow(clippy::many_single_char_names)] + let [r, g, b, a] = rgba; + let (h, s, v) = hsv_from_rgb((r, g, b)); + Hsva { h, s, v, a } + } + + pub fn to_rgba_premultiplied(&self) -> [f32; 4] { + let [r, g, b, a] = self.to_rgba_unmultiplied(); + [a * r, a * g, a * b, a] + } + + pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { + let Hsva { h, s, v, a } = *self; + let (r, g, b) = rgb_from_hsv((h, s, v)); + [r, g, b, a] + } + + pub fn to_srgba_premultiplied(&self) -> [u8; 4] { + let [r, g, b, a] = self.to_rgba_premultiplied(); + [ + srgb_byte_from_linear(r), + srgb_byte_from_linear(g), + srgb_byte_from_linear(b), + alpha_byte_from_linear(a), + ] + } + + pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { + let [r, g, b, a] = self.to_rgba_unmultiplied(); + [ + srgb_byte_from_linear(r), + srgb_byte_from_linear(g), + srgb_byte_from_linear(b), + alpha_byte_from_linear(a), + ] + } +} + +impl From for Rgba { + fn from(hsva: Hsva) -> Rgba { + Rgba(hsva.to_rgba_premultiplied()) + } +} +impl From for Hsva { + fn from(rgba: Rgba) -> Hsva { + Self::from_rgba_premultiplied(rgba.0) + } } impl From for Srgba { diff --git a/egui/src/style.rs b/egui/src/style.rs index ad4ffc06..050f741d 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -437,7 +437,7 @@ impl Stroke { ui.label(format!("{}: ", text)); ui.add(DragValue::f32(width).speed(0.1).range(0.0..=5.0)) .tooltip_text("Width"); - ui.color_edit_button(color); + ui.color_edit_button_srgba(color); }); } } @@ -454,6 +454,6 @@ fn ui_slider_vec2(ui: &mut Ui, value: &mut Vec2, range: std::ops::RangeInclusive fn ui_color(ui: &mut Ui, srgba: &mut Srgba, text: &str) { ui.horizontal_centered(|ui| { ui.label(format!("{}: ", text)); - ui.color_edit_button(srgba); + ui.color_edit_button_srgba(srgba); }); } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 8f91d449..a6e1058e 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -481,12 +481,6 @@ impl Ui { response } - /// Shows a button with the given color. - /// If the user clicks the button, a full color picker is shown. - pub fn color_edit_button(&mut self, srgba: &mut Srgba) { - widgets::color_picker::color_edit_button(self, srgba) - } - /// Ask to allocate a certain amount of space and return a Painter for that region. /// /// You may get back a `Painter` with a smaller or larger size than what you desired, @@ -497,6 +491,63 @@ 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 Srgba) -> Response { + widgets::color_picker::color_edit_button_srgba(self, srgba) + } + + /// 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 { + widgets::color_picker::color_edit_button_hsva(self, hsva) + } + + /// 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 { + let mut color = Srgba(*srgba); + let response = self.color_edit_button_srgba(&mut color); + *srgba = color.0; + 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 `sRGBA` space without premultiplied alpha. + /// 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 { + let mut hsva = Hsva::from_srgba_unmultiplied(*srgba); + let response = self.color_edit_button_hsva(&mut hsva); + *srgba = hsva.to_srgba_unmultiplied(); + 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 RGBA space with premultiplied alpha + pub fn color_edit_button_rgba_premultiplied(&mut self, rgba: &mut [f32; 4]) -> Response { + let mut hsva = Hsva::from_rgba_premultiplied(*rgba); + let response = self.color_edit_button_hsva(&mut hsva); + *rgba = hsva.to_rgba_premultiplied(); + 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 RGBA space without premultiplied alpha. + /// 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 { + let mut hsva = Hsva::from_rgba_unmultiplied(*rgba); + let response = self.color_edit_button_hsva(&mut hsva); + *rgba = hsva.to_rgba_unmultiplied(); + response + } +} + /// # Adding Containers / Sub-uis: impl Ui { pub fn collapsing( diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index 27e399b7..041cf814 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -38,7 +38,7 @@ fn background_checkers(painter: &Painter, rect: Rect) { painter.add(PaintCmd::Triangles(triangles)); } -fn show_color(ui: &mut Ui, color: Srgba, desired_size: Vec2) -> Rect { +fn show_color(ui: &mut Ui, color: Srgba, desired_size: Vec2) -> Response { let rect = ui.allocate_space(desired_size); background_checkers(ui.painter(), rect); ui.painter().add(PaintCmd::Rect { @@ -47,7 +47,7 @@ fn show_color(ui: &mut Ui, color: Srgba, desired_size: Vec2) -> Rect { fill: color, stroke: Stroke::new(3.0, color.to_opaque()), }); - rect + ui.interact_hover(rect) } fn color_button(ui: &mut Ui, color: Srgba) -> Response { @@ -184,29 +184,38 @@ fn color_slider_2d( response } -fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva) { +fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma) { ui.vertical_centered(|ui| { let current_color_size = vec2( ui.style().spacing.slider_width, ui.style().spacing.clickable_diameter * 2.0, ); - let current_color_rect = show_color(ui, (*hsva).into(), current_color_size); - if ui.hovered(current_color_rect) { - show_tooltip_text(ui.ctx(), "Current color"); - } - let opaque = Hsva { a: 1.0, ..*hsva }; - let Hsva { h, s, v, a } = hsva; - color_slider_2d(ui, h, s, |h, s| Hsva::new(h, s, 1.0, 1.0).into()) + show_color(ui, (*hsva).into(), current_color_size).tooltip_text("Current color"); + + show_color(ui, HsvaGamma { a: 1.0, ..*hsva }.into(), current_color_size) + .tooltip_text("Current color (opaque)"); + + let opaque = HsvaGamma { a: 1.0, ..*hsva }; + let HsvaGamma { h, s, v, a } = hsva; + color_slider_2d(ui, h, s, |h, s| HsvaGamma::new(h, s, 1.0, 1.0).into()) .tooltip_text("Hue - Saturation"); - color_slider_2d(ui, v, s, |v, s| Hsva { v, s, ..opaque }.into()) + color_slider_2d(ui, v, s, |v, s| HsvaGamma { v, s, ..opaque }.into()) .tooltip_text("Value - Saturation"); - ui.label("Alpha:"); - color_slider_1d(ui, a, |a| Hsva { a, ..opaque }.into()).tooltip_text("Alpha"); + color_slider_1d(ui, h, |h| HsvaGamma { h, ..opaque }.into()).tooltip_text("Hue"); + color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).tooltip_text("Saturation"); + color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).tooltip_text("Value"); + color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).tooltip_text("Alpha"); }); } -fn color_picker_hsva(ui: &mut Ui, hsva: &mut Hsva) { +fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva) { + let mut hsvag = HsvaGamma::from(*hsva); + color_picker_hsvag_2d(ui, &mut hsvag); + *hsva = Hsva::from(hsvag); +} + +pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva) -> Response { let id = ui.make_position_id().with("foo"); let button_response = color_button(ui, (*hsva).into()).tooltip_text("Click to edit color"); @@ -231,12 +240,13 @@ fn color_picker_hsva(ui: &mut Ui, hsva: &mut Hsva) { } } } + + button_response } -// TODO: return Response so user can show a tooltip /// Shows a button with the given color. /// If the user clicks the button, a full color picker is shown. -pub fn color_edit_button(ui: &mut Ui, srgba: &mut Srgba) { +pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Srgba) -> Response { // To ensure we keep hue slider when `srgba` is grey we store the // full `Hsva` in a cache: @@ -248,9 +258,93 @@ pub fn color_edit_button(ui: &mut Ui, srgba: &mut Srgba) { .cloned() .unwrap_or_else(|| Hsva::from(*srgba)); - color_picker_hsva(ui, &mut hsva); + let response = color_edit_button_hsva(ui, &mut hsva); *srgba = Srgba::from(hsva); ui.ctx().memory().color_cache.set(*srgba, hsva); + + response +} + +// ---------------------------------------------------------------------------- + +/// Like Hsva but with the `v` (value/brightness) being gamma corrected +/// so that it is perceptually even in sliders. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +struct HsvaGamma { + /// hue 0-1 + pub h: f32, + /// saturation 0-1 + pub s: f32, + /// value 0-1, in gamma-space (perceptually even) + pub v: f32, + /// alpha 0-1 + pub a: f32, +} + +impl HsvaGamma { + pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self { + Self { h, s, v, a } + } +} + +// const GAMMA: f32 = 2.2; + +impl From for Rgba { + fn from(hsvag: HsvaGamma) -> Rgba { + Hsva::from(hsvag).into() + } +} + +impl From for Srgba { + fn from(hsvag: HsvaGamma) -> Srgba { + Rgba::from(hsvag).into() + } +} + +impl From for Hsva { + fn from(hsvag: HsvaGamma) -> Hsva { + let HsvaGamma { h, s, v, a } = hsvag; + Hsva { + h, + s, + v: linear_from_srgb(v), + a, + } + } +} + +impl From for HsvaGamma { + fn from(hsva: Hsva) -> HsvaGamma { + let Hsva { h, s, v, a } = hsva; + HsvaGamma { + h, + s, + v: srgb_from_linear(v), + a, + } + } +} + +/// [0, 1] -> [0, 1] +fn linear_from_srgb(s: f32) -> f32 { + if s < 0.0 { + -linear_from_srgb(-s) + } else if s <= 0.04045 { + s / 12.92 + } else { + ((s + 0.055) / 1.055).powf(2.4) + } +} + +/// [0, 1] -> [0, 1] +fn srgb_from_linear(l: f32) -> f32 { + if l < 0.0 { + -srgb_from_linear(-l) + } else if l <= 0.0031308 { + 12.92 * l + } else { + 1.055 * l.powf(1.0 / 2.4) - 0.055 + } }