diff --git a/egui/src/memory.rs b/egui/src/memory.rs index f030b669..8f44e180 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -1,12 +1,8 @@ use std::collections::{HashMap, HashSet}; use crate::{ - area, collapsing_header, menu, - paint::color::{Color32, Hsva}, - resize, scroll_area, - util::Cache, - widgets::text_edit, - window, Id, LayerId, Pos2, Rect, Style, + area, collapsing_header, menu, paint::color::Color32, resize, scroll_area, util::Cache, + widgets::text_edit, window, Id, LayerId, Pos2, Rect, Style, }; // ---------------------------------------------------------------------------- @@ -47,7 +43,7 @@ pub struct Memory { /// Used by color picker #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) color_cache: Cache, + pub(crate) color_cache: Cache, /// Which popup-window is open (if any)? /// Could be a combo box, color picker, menu etc. diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 16c6b622..f131836c 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -799,6 +799,16 @@ impl Ui { 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_lcha(&mut self, lcha: &mut Lcha) -> Response { + widgets::color_picker::color_edit_button_lcha( + self, + lcha, + 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 { @@ -842,10 +852,10 @@ impl Ui { /// 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 mut lcha = Lcha::from_srgba_unmultiplied(*srgba); let response = - color_picker::color_edit_button_hsva(self, &mut hsva, color_picker::Alpha::OnlyBlend); - *srgba = hsva.to_srgba_unmultiplied(); + color_picker::color_edit_button_lcha(self, &mut lcha, color_picker::Alpha::OnlyBlend); + *srgba = lcha.to_srgba_unmultiplied(); response } @@ -853,13 +863,13 @@ impl Ui { /// 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 = color_picker::color_edit_button_hsva( + let mut lcha = Lcha::from_rgba_premultiplied(*rgba); + let response = color_picker::color_edit_button_lcha( self, - &mut hsva, + &mut lcha, color_picker::Alpha::BlendOrAdditive, ); - *rgba = hsva.to_rgba_premultiplied(); + *rgba = lcha.to_rgba_premultiplied(); response } @@ -868,10 +878,10 @@ impl Ui { /// 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 mut lcha = Lcha::from_rgba_unmultiplied(*rgba); let response = - color_picker::color_edit_button_hsva(self, &mut hsva, color_picker::Alpha::OnlyBlend); - *rgba = hsva.to_rgba_unmultiplied(); + color_picker::color_edit_button_lcha(self, &mut lcha, color_picker::Alpha::OnlyBlend); + *rgba = lcha.to_rgba_unmultiplied(); response } } diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index bb478e9c..0e3144fb 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -13,10 +13,19 @@ fn contrast_color(color: impl Into) -> Color32 { } } +fn out_of_gamut_as_transparent(rgba: Rgba) -> Color32 { + let (r, g, b) = (rgba.r(), rgba.g(), rgba.b()); + if r < 0.0 || g < 0.0 || b < 0.0 || r > 1.0 || g > 1.0 || b > 1.0 { + Color32::TRANSPARENT // out-of-gamut + } else { + Color32::from(rgba) + } +} + /// Number of vertices per dimension in the color sliders. /// 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°). -const N: u32 = 6 * 6; +const N: u32 = 6 * 32; fn background_checkers(painter: &Painter, rect: Rect) { let rect = rect.shrink(0.5); // Small hack to avoid the checkers from peeking through the sides @@ -85,7 +94,7 @@ fn color_button(ui: &mut Ui, color: Color32) -> Response { response } -fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response { +fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Rgba) -> Response { #![allow(clippy::identity_op)] let desired_size = vec2( @@ -107,7 +116,7 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color let mut mesh = Mesh::default(); for i in 0..=N { let t = i as f32 / (N as f32); - let color = color_at(t); + let color = out_of_gamut_as_transparent(color_at(t)); let x = lerp(rect.left()..=rect.right(), t); mesh.colored_vertex(pos2(x, rect.top()), color); mesh.colored_vertex(pos2(x, rect.bottom()), color); @@ -144,7 +153,7 @@ fn color_slider_2d( ui: &mut Ui, x_value: &mut f32, y_value: &mut f32, - color_at: impl Fn(f32, f32) -> Color32, + color_at: impl Fn(f32, f32) -> Rgba, ) -> Response { let desired_size = Vec2::splat(ui.style().spacing.slider_width); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag()); @@ -161,7 +170,8 @@ fn color_slider_2d( for yi in 0..=N { let xt = xi as f32 / (N as f32); let yt = yi as f32 / (N as f32); - let color = color_at(xt, yt); + let color = out_of_gamut_as_transparent(color_at(xt, yt)); + let x = lerp(rect.left()..=rect.right(), xt); let y = lerp(rect.bottom()..=rect.top(), yt); mesh.colored_vertex(pos2(x, y), color); @@ -186,7 +196,7 @@ fn color_slider_2d( ui.painter().add(Shape::Circle { center: pos2(x, y), radius: rect.width() / 12.0, - fill: picked_color, + fill: picked_color.into(), stroke: Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)), }); @@ -345,25 +355,149 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res button_response } +fn color_picker_lcha_2d(ui: &mut Ui, lcha: &mut Lcha, alpha: Alpha) { + let current_color_size = vec2( + ui.style().spacing.slider_width, + ui.style().spacing.interact_size.y * 2.0, + ); + + ui.horizontal(|ui| { + ui.vertical(|ui| { + let opaque = Lcha { a: 1.0, ..*lcha }; + + if alpha == Alpha::Opaque { + lcha.a = 1.0; + show_color(ui, *lcha, current_color_size).on_hover_text("Current color"); + } else { + let a = &mut lcha.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| Lcha { 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| Lcha { a, ..opaque }.into()) + .on_hover_text("Alpha"); + } + } + + show_color(ui, *lcha, current_color_size).on_hover_text("Current color"); + show_color(ui, opaque, current_color_size).on_hover_text("Current color (opaque)"); + } + + let opaque = Lcha { a: 1.0, ..*lcha }; + let Lcha { l, c, h, a: _ } = lcha; + + color_slider_1d(ui, h, |h| Lcha { h, ..opaque }.into()).on_hover_text("Hue"); + color_slider_1d(ui, c, |c| Lcha { c, ..opaque }.into()).on_hover_text("Chroma"); + color_slider_1d(ui, l, |l| Lcha { l, ..opaque }.into()).on_hover_text("Lightness"); + color_slider_2d(ui, l, c, |l, c| Lcha { l, c, ..opaque }.into()) + .on_hover_text("Lightness - Chroma"); + }); + + ui.vertical(|ui| { + let opaque = Lcha { a: 1.0, ..*lcha }; + let Lcha { l, c, h, a: _ } = lcha; + color_slider_2d(ui, h, l, |h, l| Lcha { h, l, ..opaque }.into()) + .on_hover_text("Hue - Lightness"); + color_slider_2d(ui, h, c, |h, c| Lcha { h, c, ..opaque }.into()) + .on_hover_text("Hue - Chroma"); + }); + }); +} + +pub fn color_edit_button_lcha(ui: &mut Ui, lcha: &mut Lcha, alpha: Alpha) -> Response { + let pupup_id = ui.auto_id_with("popup"); + let button_response = color_button(ui, (*lcha).into()).on_hover_text("Click to edit color"); + + if button_response.clicked() { + ui.memory().toggle_popup(pupup_id); + } + // TODO: make it easier to show a temporary popup that closes when you click outside it + if ui.memory().is_popup_open(pupup_id) { + let area_response = Area::new(pupup_id) + .order(Order::Foreground) + .default_pos(button_response.rect.max) + .show(ui.ctx(), |ui| { + Frame::popup(ui.style()).show(ui, |ui| { + ui.style_mut().spacing.slider_width = 256.0; + color_picker_lcha_2d(ui, lcha, alpha); + }) + }); + + if !button_response.clicked() { + let clicked_outside = ui.input().pointer.any_click() && !area_response.hovered; + if clicked_outside || ui.input().key_pressed(Key::Escape) { + ui.memory().close_popup(); + } + } + } + + button_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_srgba(ui: &mut Ui, srgba: &mut Color32) -> Response { +// // To ensure we keep hue slider when `srgba` is grey we store the +// // full `Hsva` in a cache: + +// let mut hsva = ui +// .ctx() +// .memory() +// .color_cache +// .get(srgba) +// .cloned() +// .unwrap_or_else(|| Hsva::from(*srgba)); + +// let response = color_edit_button_hsva(ui, &mut hsva); + +// *srgba = Color32::from(hsva); + +// ui.ctx().memory().color_cache.set(*srgba, hsva); + +// 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_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> Response { // To ensure we keep hue slider when `srgba` is grey we store the - // full `Hsva` in a cache: + // full `Lcha` in a cache: - let mut hsva = ui + let mut lcha = ui .ctx() .memory() .color_cache .get(srgba) .cloned() - .unwrap_or_else(|| Hsva::from(*srgba)); + .unwrap_or_else(|| Lcha::from(*srgba)); - let response = color_edit_button_hsva(ui, &mut hsva, alpha); + let response = color_edit_button_lcha(ui, &mut lcha, alpha); - *srgba = Color32::from(hsva); + *srgba = Color32::from(lcha); - ui.ctx().memory().color_cache.set(*srgba, hsva); + ui.ctx().memory().color_cache.set(*srgba, lcha); response } diff --git a/egui_demo_lib/src/apps/demo/demo_window.rs b/egui_demo_lib/src/apps/demo/demo_window.rs index b5bcc7f0..896740b0 100644 --- a/egui_demo_lib/src/apps/demo/demo_window.rs +++ b/egui_demo_lib/src/apps/demo/demo_window.rs @@ -100,6 +100,7 @@ struct ColorWidgets { srgba_premul: [u8; 4], rgba_unmul: [f32; 4], rgba_premul: [f32; 4], + lcha: egui::color::Lcha, } impl Default for ColorWidgets { @@ -110,6 +111,7 @@ impl Default for ColorWidgets { 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], + lcha: (Rgba::from(egui::Color32::BLUE) * 0.5).into(), } } } @@ -125,8 +127,14 @@ impl ColorWidgets { srgba_premul, rgba_unmul, rgba_premul, + lcha, } = self; + ui.horizontal(|ui| { + ui.color_edit_button_lcha(lcha); + ui.label(format!("LCHA: {} {} {} {}", lcha.l, lcha.c, lcha.h, lcha.a)); + }); + ui.horizontal(|ui| { ui.color_edit_button_srgba_unmultiplied(srgba_unmul); ui.label(format!( diff --git a/epaint/src/color.rs b/epaint/src/color.rs index fdba57e0..6876d17c 100644 --- a/epaint/src/color.rs +++ b/epaint/src/color.rs @@ -228,7 +228,8 @@ impl Rgba { /// How perceptually intense (bright) is the color? pub fn intensity(&self) -> f32 { - 0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b() + // 0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b() + Lcha::from_rgb([self.r(), self.g(), self.b()]).l } /// Returns an opaque version of self @@ -382,6 +383,19 @@ impl Hsva { Self { h, s, v, a } } + 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), + ]) + } + /// From `sRGBA` with premultiplied alpha pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self { Self::from_rgba_premultiplied([ @@ -434,19 +448,6 @@ impl Hsva { } } - 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_opaque(self) -> Self { @@ -585,3 +586,314 @@ fn test_hsv_roundtrip() { } } } + +// ---------------------------------------------------------------------------- + +// /// A simple perceptual color space. +// /// +// /// https://bottosson.github.io/posts/oklab/ +// #[derive(Clone, Copy, Debug, Default, PartialEq)] +// struct Oklab { +// /// Perceived lightness (0-1) +// pub l: f32, +// /// How green/red the color is ([-1, 1]) +// pub a: f32, +// /// How blue/yellow the color is ([-1, 1]) +// pub b: f32, +// } + +// impl Oklab { +// pub fn from_linear_rgb(r: f32, g: f32, b: f32) -> Oklab { +// let (l, a, b) = lab_from_rgb([r, g, b]); +// Oklab { l, a, b } +// } + +// pub fn to_linear_rgb(self) -> [f32; 3] { +// rgb_from_lab((self.l, self.a, self.b)) +// } +// } + +// /// Polar form of [`Oklab`], all coordinated in 0-1 range. +// #[derive(Clone, Copy, Debug, Default, PartialEq)] +// struct Oklch { +// /// Perceived lightness in [0, 1] range. +// pub l: f32, +// /// Chroma in [0, 1] range. +// pub c: f32, +// /// Hue in [0, 1] range. +// pub h: f32, +// } + +// impl From for Oklch { +// fn from(i: Oklab) -> Oklch { +// use std::f32::consts::TAU; +// Oklch { +// l: i.l, +// c: i.a.hypot(i.b), +// h: (i.b.atan2(i.a) + TAU) % TAU / TAU, +// } +// } +// } + +// impl From for Oklab { +// fn from(i: Oklch) -> Oklab { +// use std::f32::consts::TAU; +// let (sin_h, cos_h) = (i.h * TAU).sin_cos(); +// Oklab { +// l: i.l, +// a: i.c * cos_h, +// b: i.c * sin_h, +// } +// } +// } + +// impl From for Color32 { +// fn from(i: Oklab) -> Color32 { +// let [r, g, b] = i.to_linear_rgb(); +// Rgba::from_rgb(r, g, b).into() +// } +// } + +// impl From for Color32 { +// fn from(i: Oklch) -> Color32 { +// Oklab::from(i).into() +// } +// } + +// #[test] +// // #[ignore] // a bit expensive +// fn test_oklab_roundtrip() { +// for r in 0..=255 { +// for g in 0..=255 { +// for b in 0..=255 { +// let srgba = Color32::from_rgb(r, g, b); +// let rgba = Rgba::from(srgba); +// let oklab = Oklab::from_linear_rgb(rgba.r(), rgba.g(), rgba.b()); +// assert_eq!(srgba, Color32::from(oklab)); +// let oklch = Oklch::from(oklab); +// assert_eq!(srgba, Color32::from(oklch),); +// } +// } +// } +// } + +// ---------------------------------------------------------------------------- + +/// oklab from linear rgb +fn lab_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { + let x = 0.4121656120 * r + 0.5362752080 * g + 0.0514575653 * b; + let y = 0.2118591070 * r + 0.6807189584 * g + 0.1074065790 * b; + let z = 0.0883097947 * r + 0.2818474174 * g + 0.6302613616 * b; + + let x = x.cbrt(); + let y = y.cbrt(); + let z = z.cbrt(); + + ( + 0.2104542553 * x + 0.7936177850 * y - 0.0040720468 * z, + 1.9779984951 * x - 2.4285922050 * y + 0.4505937099 * z, + 0.0259040371 * x + 0.7827717662 * y - 0.8086757660 * z, + ) +} + +/// linear rgb from oklab +pub fn rgb_from_lab((l, a, b): (f32, f32, f32)) -> [f32; 3] { + let x = l + 0.3963377774 * a + 0.2158037573 * b; + let y = l - 0.1055613458 * a - 0.0638541728 * b; + let z = l - 0.0894841775 * a - 1.2914855480 * b; + + let x = x.powi(3); + let y = y.powi(3); + let z = z.powi(3); + + [ + 4.0767245293 * x - 3.3072168827 * y + 0.2307590544 * z, + -1.2681437731 * x + 2.6093323231 * y - 0.3411344290 * z, + -0.0041119885 * x - 0.7034763098 * y + 1.7068625689 * z, + ] +} + +/// 0-1 normalized lch from oklab. +fn lch_from_lab((l, a, b): (f32, f32, f32)) -> (f32, f32, f32) { + use std::f32::consts::TAU; + let c = a.hypot(b); + let h = (b.atan2(a) + TAU) % TAU / TAU; + (l, c, h) +} + +/// Oklab from 0-1 normalized lch. +fn lab_from_lch((l, c, h): (f32, f32, f32)) -> (f32, f32, f32) { + use std::f32::consts::TAU; + let (sin_h, cos_h) = (h * TAU).sin_cos(); + let a = c * cos_h; + let b = c * sin_h; + (l, a, b) +} + +/// 0-1 normalized lch from linear rgb +fn lch_from_rgb(rgb: [f32; 3]) -> (f32, f32, f32) { + lch_from_lab(lab_from_rgb(rgb)) +} +/// linear rgb from 0-1 normalized lch +fn rgb_from_lch(lch: (f32, f32, f32)) -> [f32; 3] { + rgb_from_lab(lab_from_lch(lch)) +} + +/// Lightness, chroma, hue and alpha. All in the range [0, 1]. +/// No premultiplied alpha. +/// Based on the the perceptual color space Oklab (https://bottosson.github.io/posts/oklab/). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Lcha { + /// Perceived lightness in [0, 1] range. + pub l: f32, + /// Chroma in [0, 1] range. + pub c: f32, + /// Hue in [0, 1] range. + pub h: f32, + /// Alpha in [0, 1] range. A negative value signifies an additive color (and alpha is ignored). + pub a: f32, +} + +impl Lcha { + pub fn new(l: f32, c: f32, h: f32, a: f32) -> Self { + Self { l, c, h, a } + } + + /// From linear RGB. + pub fn from_rgb(rgb: [f32; 3]) -> Self { + let (l, c, h) = lch_from_rgb(rgb); + Lcha { l, c, h, a: 1.0 } + } + + /// From `sRGBA` with premultiplied alpha + pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self { + Self::from_rgba_premultiplied([ + linear_from_gamma_byte(srgba[0]), + linear_from_gamma_byte(srgba[1]), + linear_from_gamma_byte(srgba[2]), + linear_from_alpha_byte(srgba[3]), + ]) + } + + /// From `sRGBA` without premultiplied alpha + pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { + Self::from_rgba_unmultiplied([ + linear_from_gamma_byte(srgba[0]), + linear_from_gamma_byte(srgba[1]), + linear_from_gamma_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 [r, g, b, a] = rgba; + if a == 0.0 { + if r == 0.0 && b == 0.0 && a == 0.0 { + Lcha::default() + } else { + Lcha::from_additive_rgb([r, g, b]) + } + } else { + let (l, c, h) = lch_from_rgb([r / a, g / a, b / a]); + Lcha { l, c, h, 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 (l, c, h) = lch_from_rgb([r, g, b]); + Lcha { l, c, h, a } + } + + pub fn from_additive_rgb(rgb: [f32; 3]) -> Self { + let (l, c, h) = lch_from_rgb(rgb); + Lcha { + l, + c, + h, + a: -0.5, // anything negative is treated as additive + } + } + // ------------------------------------------------------------------------ + + pub fn to_rgb(&self) -> [f32; 3] { + rgb_from_lch((self.l, self.c, self.h)) + } + + pub fn to_rgba_premultiplied(&self) -> [f32; 4] { + let [r, g, b, a] = self.to_rgba_unmultiplied(); + let additive = a < 0.0; + if additive { + [r, g, b, 0.0] + } else { + [a * r, a * g, a * b, a] + } + } + + pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { + let Lcha { l, c, h, a } = *self; + let [r, g, b] = rgb_from_lch((l, c, h)); + [r, g, b, a] + } + + pub fn to_srgba_premultiplied(&self) -> [u8; 4] { + let [r, g, b, a] = self.to_rgba_premultiplied(); + [ + gamma_byte_from_linear(r), + gamma_byte_from_linear(g), + gamma_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(); + [ + gamma_byte_from_linear(r), + gamma_byte_from_linear(g), + gamma_byte_from_linear(b), + alpha_byte_from_linear(a.abs()), + ] + } +} + +impl From for Rgba { + fn from(hsva: Lcha) -> Rgba { + Rgba(hsva.to_rgba_premultiplied()) + } +} +impl From for Lcha { + fn from(rgba: Rgba) -> Lcha { + Self::from_rgba_premultiplied(rgba.0) + } +} + +impl From for Color32 { + fn from(hsva: Lcha) -> Color32 { + Color32::from(Rgba::from(hsva)) + } +} +impl From for Lcha { + fn from(srgba: Color32) -> Lcha { + Lcha::from(Rgba::from(srgba)) + } +} + +#[test] +// #[ignore] // a bit expensive +fn test_lcha_roundtrip() { + for r in 0..=255 { + for g in 0..=255 { + for b in 0..=255 { + let srgba = Color32::from_rgb(r, g, b); + let lcha = Lcha::from(srgba); + assert_eq!(srgba, Color32::from(lcha),); + } + } + } +}