[color-picker] edit your own (s)RGBA arrays

Both with and without premultiplied alpha
This commit is contained in:
Emil Ernerfeldt 2020-09-09 11:23:32 +02:00
parent b9a3240ca3
commit bc0d6baefb
6 changed files with 340 additions and 39 deletions

View file

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

View file

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

View file

@ -208,7 +208,7 @@ impl From<Srgba> 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<Rgba> 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 }
}
/// 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<Hsva> 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` 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]),
])
}
}
impl From<Rgba> for Hsva {
fn from(rgba: Rgba) -> Hsva {
/// 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<Rgba> 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<Hsva> for Rgba {
fn from(hsva: Hsva) -> Rgba {
Rgba(hsva.to_rgba_premultiplied())
}
}
impl From<Rgba> for Hsva {
fn from(rgba: Rgba) -> Hsva {
Self::from_rgba_premultiplied(rgba.0)
}
}
impl From<Hsva> for Srgba {

View file

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

View file

@ -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<R>(

View file

@ -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<HsvaGamma> for Rgba {
fn from(hsvag: HsvaGamma) -> Rgba {
Hsva::from(hsvag).into()
}
}
impl From<HsvaGamma> for Srgba {
fn from(hsvag: HsvaGamma) -> Srgba {
Rgba::from(hsvag).into()
}
}
impl From<HsvaGamma> 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<Hsva> 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
}
}