Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5b30508686 |
5 changed files with 503 additions and 43 deletions
|
@ -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<Color32, Hsva>,
|
||||
pub(crate) color_cache: Cache<Color32, crate::color::Lcha>,
|
||||
|
||||
/// Which popup-window is open (if any)?
|
||||
/// Could be a combo box, color picker, menu etc.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,19 @@ fn contrast_color(color: impl Into<Rgba>) -> 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
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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<Oklab> 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<Oklch> 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<Oklab> for Color32 {
|
||||
// fn from(i: Oklab) -> Color32 {
|
||||
// let [r, g, b] = i.to_linear_rgb();
|
||||
// Rgba::from_rgb(r, g, b).into()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<Oklch> 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<Lcha> for Rgba {
|
||||
fn from(hsva: Lcha) -> Rgba {
|
||||
Rgba(hsva.to_rgba_premultiplied())
|
||||
}
|
||||
}
|
||||
impl From<Rgba> for Lcha {
|
||||
fn from(rgba: Rgba) -> Lcha {
|
||||
Self::from_rgba_premultiplied(rgba.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Lcha> for Color32 {
|
||||
fn from(hsva: Lcha) -> Color32 {
|
||||
Color32::from(Rgba::from(hsva))
|
||||
}
|
||||
}
|
||||
impl From<Color32> 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),);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue