From 5effc68ba4f6dc0f9f9792193bb6e0eeca6dccef Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Tue, 6 Dec 2022 20:42:25 +0100 Subject: [PATCH] Split out ecolor crate (#2399) * split out ecolor crate * split up ecolor crate in lots of modules * add changelog notes * add readme to ecolor * put clippy::manual_range_contains on cranky allow list * fix hex color issues * doc fixes * more hex_color fixes * Document features * Rename hex_color module to avoid warning * Sort the feature names * fix link in CHANGELOG.md * better wording Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 14 +- Cargo.toml | 1 + Cranky.toml | 1 + crates/ecolor/CHANGELOG.md | 6 + crates/ecolor/Cargo.toml | 50 + crates/ecolor/README.md | 5 + crates/ecolor/src/cint_impl.rs | 161 +++ crates/ecolor/src/color32.rs | 181 +++ crates/ecolor/src/hex_color_macro.rs | 39 + crates/ecolor/src/hsva.rs | 231 ++++ crates/ecolor/src/hsva_gamma.rs | 66 + crates/ecolor/src/lib.rs | 173 +++ crates/ecolor/src/rgba.rs | 266 ++++ crates/egui/src/lib.rs | 12 +- crates/egui/src/painter.rs | 2 +- crates/egui/src/style.rs | 4 +- crates/egui/src/ui.rs | 2 +- crates/egui/src/widgets/color_picker.rs | 2 +- crates/egui/src/widgets/plot/mod.rs | 2 +- crates/egui_demo_lib/src/color_test.rs | 2 +- .../egui_demo_lib/src/demo/drag_and_drop.rs | 4 +- .../src/demo/misc_demo_window.rs | 2 +- crates/egui_demo_lib/src/demo/scrolling.rs | 2 +- crates/emath/src/lib.rs | 1 - crates/epaint/CHANGELOG.md | 1 + crates/epaint/Cargo.toml | 24 +- crates/epaint/src/color.rs | 1068 ----------------- crates/epaint/src/image.rs | 2 +- crates/epaint/src/lib.rs | 6 +- 29 files changed, 1229 insertions(+), 1101 deletions(-) create mode 100644 crates/ecolor/CHANGELOG.md create mode 100644 crates/ecolor/Cargo.toml create mode 100644 crates/ecolor/README.md create mode 100644 crates/ecolor/src/cint_impl.rs create mode 100644 crates/ecolor/src/color32.rs create mode 100644 crates/ecolor/src/hex_color_macro.rs create mode 100644 crates/ecolor/src/hsva.rs create mode 100644 crates/ecolor/src/hsva_gamma.rs create mode 100644 crates/ecolor/src/lib.rs create mode 100644 crates/ecolor/src/rgba.rs delete mode 100644 crates/epaint/src/color.rs diff --git a/Cargo.lock b/Cargo.lock index 51b28ec91..5e6ed1bb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1293,6 +1293,17 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" +[[package]] +name = "ecolor" +version = "0.19.0" +dependencies = [ + "bytemuck", + "cint", + "color-hex", + "document-features", + "serde", +] + [[package]] name = "eframe" version = "0.19.0" @@ -1556,10 +1567,9 @@ dependencies = [ "atomic_refcell", "backtrace", "bytemuck", - "cint", - "color-hex", "criterion", "document-features", + "ecolor", "emath", "nohash-hasher", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index eed3b6fc3..4de49458c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/ecolor", "crates/egui_demo_app", "crates/egui_demo_lib", "crates/egui_extras", diff --git a/Cranky.toml b/Cranky.toml index b2e0b313e..957bd477a 100644 --- a/Cranky.toml +++ b/Cranky.toml @@ -116,6 +116,7 @@ allow = [ # TODO(emilk): enable more lints "clippy::type_complexity", "clippy::undocumented_unsafe_blocks", + "clippy::manual_range_contains", "trivial_casts", "unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 "unused_qualifications", diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md new file mode 100644 index 000000000..6309f31ba --- /dev/null +++ b/crates/ecolor/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog for ecolor +All notable changes to the `ecolor` crate will be noted in this file. + + +## Unreleased +* Split out `ecolor` crate from `epaint` diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml new file mode 100644 index 000000000..bf50c0023 --- /dev/null +++ b/crates/ecolor/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "ecolor" +version = "0.19.0" +authors = [ + "Emil Ernerfeldt ", + "Andreas Reich ", +] +description = "Color structs and color conversion utilities" +edition = "2021" +rust-version = "1.65" +homepage = "https://github.com/emilk/egui" +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/emilk/egui" +categories = ["mathematics", "encoding", "images"] +keywords = ["gui", "color", "conversion", "gamedev", "images"] +include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] + +[package.metadata.docs.rs] +all-features = true + +[lib] + + +[features] +default = [] + +## Enable additional checks if debug assertions are enabled (debug builds). +extra_debug_asserts = [] +## Always enable additional checks. +extra_asserts = [] + + +[dependencies] +#! ### Optional dependencies + +## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast `emath` types to `&[u8]`. +bytemuck = { version = "1.7.2", optional = true, features = ["derive"] } + +## [`cint`](https://docs.rs/cint) enables interopability with other color libraries. +cint = { version = "0.3.1", optional = true } + +## Enable the [`hex_color`] macro. +color-hex = { version = "0.2.0", optional = true } + +## Enable this when generating docs. +document-features = { version = "0.2", optional = true } + +## Allow serialization using [`serde`](https://docs.rs/serde). +serde = { version = "1", optional = true, features = ["derive"] } diff --git a/crates/ecolor/README.md b/crates/ecolor/README.md new file mode 100644 index 000000000..5dee07769 --- /dev/null +++ b/crates/ecolor/README.md @@ -0,0 +1,5 @@ +# ecolor - egui color library + +A simple color storage and conversion library. + +Made for [`egui`](https://github.com/emilk/egui/). diff --git a/crates/ecolor/src/cint_impl.rs b/crates/ecolor/src/cint_impl.rs new file mode 100644 index 000000000..a730fda48 --- /dev/null +++ b/crates/ecolor/src/cint_impl.rs @@ -0,0 +1,161 @@ +use super::*; +use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha}; + +// ---- Color32 ---- + +impl From>> for Color32 { + fn from(srgba: Alpha>) -> Self { + let Alpha { + color: EncodedSrgb { r, g, b }, + alpha: a, + } = srgba; + + Color32::from_rgba_unmultiplied(r, g, b, a) + } +} + +// No From for Alpha<_> because Color32 is premultiplied + +impl From>> for Color32 { + fn from(srgba: PremultipliedAlpha>) -> Self { + let PremultipliedAlpha { + color: EncodedSrgb { r, g, b }, + alpha: a, + } = srgba; + + Color32::from_rgba_premultiplied(r, g, b, a) + } +} + +impl From for PremultipliedAlpha> { + fn from(col: Color32) -> Self { + let (r, g, b, a) = col.to_tuple(); + + PremultipliedAlpha { + color: EncodedSrgb { r, g, b }, + alpha: a, + } + } +} + +impl From>> for Color32 { + fn from(srgba: PremultipliedAlpha>) -> Self { + let PremultipliedAlpha { + color: EncodedSrgb { r, g, b }, + alpha: a, + } = srgba; + + // This is a bit of an abuse of the function name but it does what we want. + let r = linear_u8_from_linear_f32(r); + let g = linear_u8_from_linear_f32(g); + let b = linear_u8_from_linear_f32(b); + let a = linear_u8_from_linear_f32(a); + + Color32::from_rgba_premultiplied(r, g, b, a) + } +} + +impl From for PremultipliedAlpha> { + fn from(col: Color32) -> Self { + let (r, g, b, a) = col.to_tuple(); + + // This is a bit of an abuse of the function name but it does what we want. + let r = linear_f32_from_linear_u8(r); + let g = linear_f32_from_linear_u8(g); + let b = linear_f32_from_linear_u8(b); + let a = linear_f32_from_linear_u8(a); + + PremultipliedAlpha { + color: EncodedSrgb { r, g, b }, + alpha: a, + } + } +} + +impl ColorInterop for Color32 { + type CintTy = PremultipliedAlpha>; +} + +// ---- Rgba ---- + +impl From>> for Rgba { + fn from(srgba: PremultipliedAlpha>) -> Self { + let PremultipliedAlpha { + color: LinearSrgb { r, g, b }, + alpha: a, + } = srgba; + + Rgba([r, g, b, a]) + } +} + +impl From for PremultipliedAlpha> { + fn from(col: Rgba) -> Self { + let (r, g, b, a) = col.to_tuple(); + + PremultipliedAlpha { + color: LinearSrgb { r, g, b }, + alpha: a, + } + } +} + +impl ColorInterop for Rgba { + type CintTy = PremultipliedAlpha>; +} + +// ---- Hsva ---- + +impl From>> for Hsva { + fn from(srgba: Alpha>) -> Self { + let Alpha { + color: Hsv { h, s, v }, + alpha: a, + } = srgba; + + Hsva::new(h, s, v, a) + } +} + +impl From for Alpha> { + fn from(col: Hsva) -> Self { + let Hsva { h, s, v, a } = col; + + Alpha { + color: Hsv { h, s, v }, + alpha: a, + } + } +} + +impl ColorInterop for Hsva { + type CintTy = Alpha>; +} + +// ---- HsvaGamma ---- + +impl ColorInterop for HsvaGamma { + type CintTy = Alpha>; +} + +impl From>> for HsvaGamma { + fn from(srgba: Alpha>) -> Self { + let Alpha { + color: Hsv { h, s, v }, + alpha: a, + } = srgba; + + Hsva::new(h, s, v, a).into() + } +} + +impl From for Alpha> { + fn from(col: HsvaGamma) -> Self { + let Hsva { h, s, v, a } = col.into(); + + Alpha { + color: Hsv { h, s, v }, + alpha: a, + } + } +} diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs new file mode 100644 index 000000000..ee22c4d04 --- /dev/null +++ b/crates/ecolor/src/color32.rs @@ -0,0 +1,181 @@ +use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba}; + +/// This format is used for space-efficient color representation (32 bits). +/// +/// Instead of manipulating this directly it is often better +/// to first convert it to either [`Rgba`] or [`crate::Hsva`]. +/// +/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha. +/// Alpha channel is in linear space. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] +pub struct Color32(pub(crate) [u8; 4]); + +impl std::ops::Index for Color32 { + type Output = u8; + + #[inline(always)] + fn index(&self, index: usize) -> &u8 { + &self.0[index] + } +} + +impl std::ops::IndexMut for Color32 { + #[inline(always)] + fn index_mut(&mut self, index: usize) -> &mut u8 { + &mut self.0[index] + } +} + +impl Color32 { + // Mostly follows CSS names: + + pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); + pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0); + pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96); + pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160); + pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220); + pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255); + + pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42); + pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0); + pub const RED: Color32 = Color32::from_rgb(255, 0, 0); + pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128); + + pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0); + pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0); + pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140); + + pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); + pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0); + pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90); + + pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B); + pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255); + pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6); + + pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0); + + pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128); + + /// An ugly color that is planned to be replaced before making it to the screen. + pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0); + + #[inline(always)] + pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Self([r, g, b, 255]) + } + + #[inline(always)] + pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self { + Self([r, g, b, 0]) + } + + /// From `sRGBA` with premultiplied alpha. + #[inline(always)] + pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { + Self([r, g, b, a]) + } + + /// From `sRGBA` WITHOUT premultiplied alpha. + pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { + if a == 255 { + Self::from_rgb(r, g, b) // common-case optimization + } else if a == 0 { + Self::TRANSPARENT // common-case optimization + } else { + let r_lin = linear_f32_from_gamma_u8(r); + let g_lin = linear_f32_from_gamma_u8(g); + let b_lin = linear_f32_from_gamma_u8(b); + let a_lin = linear_f32_from_linear_u8(a); + + let r = gamma_u8_from_linear_f32(r_lin * a_lin); + let g = gamma_u8_from_linear_f32(g_lin * a_lin); + let b = gamma_u8_from_linear_f32(b_lin * a_lin); + + Self::from_rgba_premultiplied(r, g, b, a) + } + } + + #[inline(always)] + pub const fn from_gray(l: u8) -> Self { + Self([l, l, l, 255]) + } + + #[inline(always)] + pub const fn from_black_alpha(a: u8) -> Self { + Self([0, 0, 0, a]) + } + + pub fn from_white_alpha(a: u8) -> Self { + Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into() + } + + #[inline(always)] + pub const fn from_additive_luminance(l: u8) -> Self { + Self([l, l, l, 0]) + } + + #[inline(always)] + pub const fn is_opaque(&self) -> bool { + self.a() == 255 + } + + #[inline(always)] + pub const fn r(&self) -> u8 { + self.0[0] + } + + #[inline(always)] + pub const fn g(&self) -> u8 { + self.0[1] + } + + #[inline(always)] + pub const fn b(&self) -> u8 { + self.0[2] + } + + #[inline(always)] + pub const fn a(&self) -> u8 { + self.0[3] + } + + /// Returns an opaque version of self + pub fn to_opaque(self) -> Self { + Rgba::from(self).to_opaque().into() + } + + /// Returns an additive version of self + #[inline(always)] + pub const fn additive(self) -> Self { + let [r, g, b, _] = self.to_array(); + Self([r, g, b, 0]) + } + + /// Premultiplied RGBA + #[inline(always)] + pub const fn to_array(&self) -> [u8; 4] { + [self.r(), self.g(), self.b(), self.a()] + } + + /// Premultiplied RGBA + #[inline(always)] + pub const fn to_tuple(&self) -> (u8, u8, u8, u8) { + (self.r(), self.g(), self.b(), self.a()) + } + + pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { + Rgba::from(*self).to_srgba_unmultiplied() + } + + /// Multiply with 0.5 to make color half as opaque. + pub fn linear_multiply(self, factor: f32) -> Color32 { + crate::ecolor_assert!(0.0 <= factor && factor <= 1.0); + // As an unfortunate side-effect of using premultiplied alpha + // we need a somewhat expensive conversion to linear space and back. + Rgba::from(self).multiply(factor).into() + } +} diff --git a/crates/ecolor/src/hex_color_macro.rs b/crates/ecolor/src/hex_color_macro.rs new file mode 100644 index 000000000..450e6a869 --- /dev/null +++ b/crates/ecolor/src/hex_color_macro.rs @@ -0,0 +1,39 @@ +/// Construct a [`crate::Color32`] from a hex RGB or RGBA string. +/// +/// ``` +/// # use ecolor::{hex_color, Color32}; +/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22)); +/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12)); +/// ``` +#[macro_export] +macro_rules! hex_color { + ($s:literal) => {{ + let array = color_hex::color_from_hex!($s); + if array.len() == 3 { + $crate::Color32::from_rgb(array[0], array[1], array[2]) + } else { + #[allow(unconditional_panic)] + $crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3]) + } + }}; +} + +#[test] +fn test_from_rgb_hex() { + assert_eq!( + crate::Color32::from_rgb(0x20, 0x21, 0x22), + hex_color!("#202122") + ); + assert_eq!( + crate::Color32::from_rgb_additive(0x20, 0x21, 0x22), + hex_color!("#202122").additive() + ); +} + +#[test] +fn test_from_rgba_hex() { + assert_eq!( + crate::Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50), + hex_color!("20212250") + ); +} diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs new file mode 100644 index 000000000..8a68cb935 --- /dev/null +++ b/crates/ecolor/src/hsva.rs @@ -0,0 +1,231 @@ +use crate::{ + gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, + linear_u8_from_linear_f32, Color32, Rgba, +}; + +/// Hue, saturation, value, alpha. All in the range [0, 1]. +/// No premultiplied alpha. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Hsva { + /// hue 0-1 + pub h: f32, + + /// saturation 0-1 + pub s: f32, + + /// value 0-1 + pub v: f32, + + /// alpha 0-1. A negative value signifies an additive color (and alpha is ignored). + pub a: f32, +} + +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_f32_from_gamma_u8(srgba[0]), + linear_f32_from_gamma_u8(srgba[1]), + linear_f32_from_gamma_u8(srgba[2]), + linear_f32_from_linear_u8(srgba[3]), + ) + } + + /// From `sRGBA` without premultiplied alpha + pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { + Self::from_rgba_unmultiplied( + linear_f32_from_gamma_u8(srgba[0]), + linear_f32_from_gamma_u8(srgba[1]), + linear_f32_from_gamma_u8(srgba[2]), + linear_f32_from_linear_u8(srgba[3]), + ) + } + + /// From linear RGBA with premultiplied alpha + pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { + #![allow(clippy::many_single_char_names)] + if a == 0.0 { + if r == 0.0 && b == 0.0 && a == 0.0 { + Hsva::default() + } else { + Hsva::from_additive_rgb([r, g, b]) + } + } else { + let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]); + Hsva { h, s, v, a } + } + } + + /// From linear RGBA without premultiplied alpha + pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { + #![allow(clippy::many_single_char_names)] + let (h, s, v) = hsv_from_rgb([r, g, b]); + 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_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + ]) + } + + // ------------------------------------------------------------------------ + + pub fn to_opaque(self) -> Self { + Self { a: 1.0, ..self } + } + + 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_u8_from_linear_f32(r), + gamma_u8_from_linear_f32(g), + gamma_u8_from_linear_f32(b), + ] + } + + 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] + } + } + + /// Represents additive colors using a negative alpha. + 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(); + [ + gamma_u8_from_linear_f32(r), + gamma_u8_from_linear_f32(g), + gamma_u8_from_linear_f32(b), + linear_u8_from_linear_f32(a), + ] + } + + pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { + let [r, g, b, a] = self.to_rgba_unmultiplied(); + [ + gamma_u8_from_linear_f32(r), + gamma_u8_from_linear_f32(g), + gamma_u8_from_linear_f32(b), + linear_u8_from_linear_f32(a.abs()), + ] + } +} + +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[0], rgba.0[1], rgba.0[2], rgba.0[3]) + } +} + +impl From for Color32 { + fn from(hsva: Hsva) -> Color32 { + Color32::from(Rgba::from(hsva)) + } +} + +impl From for Hsva { + fn from(srgba: Color32) -> Hsva { + Hsva::from(Rgba::from(srgba)) + } +} + +/// All ranges in 0-1, rgb is linear. +pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { + #![allow(clippy::many_single_char_names)] + let min = r.min(g.min(b)); + let max = r.max(g.max(b)); // value + + let range = max - min; + + let h = if max == min { + 0.0 // hue is undefined + } else if max == r { + (g - b) / (6.0 * range) + } else if max == g { + (b - r) / (6.0 * range) + 1.0 / 3.0 + } else { + // max == b + (r - g) / (6.0 * range) + 2.0 / 3.0 + }; + let h = (h + 1.0).fract(); // wrap + let s = if max == 0.0 { 0.0 } else { 1.0 - min / max }; + (h, s, max) +} + +/// All ranges in 0-1, rgb is linear. +pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] { + #![allow(clippy::many_single_char_names)] + let h = (h.fract() + 1.0).fract(); // wrap + let s = s.clamp(0.0, 1.0); + + let f = h * 6.0 - (h * 6.0).floor(); + let p = v * (1.0 - s); + let q = v * (1.0 - f * s); + let t = v * (1.0 - (1.0 - f) * s); + + match (h * 6.0).floor() as i32 % 6 { + 0 => [v, t, p], + 1 => [q, v, p], + 2 => [p, v, t], + 3 => [p, q, v], + 4 => [t, p, v], + 5 => [v, p, q], + _ => unreachable!(), + } +} + +#[test] +#[ignore] // a bit expensive +fn test_hsv_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 hsva = Hsva::from(srgba); + assert_eq!(srgba, Color32::from(hsva)); + } + } + } +} diff --git a/crates/ecolor/src/hsva_gamma.rs b/crates/ecolor/src/hsva_gamma.rs new file mode 100644 index 000000000..3135ef100 --- /dev/null +++ b/crates/ecolor/src/hsva_gamma.rs @@ -0,0 +1,66 @@ +use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba}; + +/// Like Hsva but with the `v` value (brightness) being gamma corrected +/// so that it is somewhat perceptually even. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub 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. A negative value signifies an additive color (and alpha is ignored). + pub a: f32, +} + +impl From for Rgba { + fn from(hsvag: HsvaGamma) -> Rgba { + Hsva::from(hsvag).into() + } +} + +impl From for Color32 { + fn from(hsvag: HsvaGamma) -> Color32 { + 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_gamma(v), + a, + } + } +} + +impl From for HsvaGamma { + fn from(rgba: Rgba) -> HsvaGamma { + Hsva::from(rgba).into() + } +} + +impl From for HsvaGamma { + fn from(srgba: Color32) -> HsvaGamma { + Hsva::from(srgba).into() + } +} + +impl From for HsvaGamma { + fn from(hsva: Hsva) -> HsvaGamma { + let Hsva { h, s, v, a } = hsva; + HsvaGamma { + h, + s, + v: gamma_from_linear(v), + a, + } + } +} diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs new file mode 100644 index 000000000..9bc42c4cf --- /dev/null +++ b/crates/ecolor/src/lib.rs @@ -0,0 +1,173 @@ +//! Color conversions and types. +//! +//! If you want a compact color representation, use [`Color32`]. +//! If you want to manipulate RGBA colors use [`Rgba`]. +//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`]. +//! +//! ## Feature flags +#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +//! + +#![allow(clippy::wrong_self_convention)] + +#[cfg(feature = "cint")] +mod cint_impl; +#[cfg(feature = "cint")] +pub use cint_impl::*; + +mod color32; +pub use color32::*; + +mod hsva_gamma; +pub use hsva_gamma::*; + +mod hsva; +pub use hsva::*; + +#[cfg(feature = "color-hex")] +mod hex_color_macro; + +mod rgba; +pub use rgba::*; + +// ---------------------------------------------------------------------------- +// Color conversion: + +impl From for Rgba { + fn from(srgba: Color32) -> Rgba { + Rgba([ + linear_f32_from_gamma_u8(srgba.0[0]), + linear_f32_from_gamma_u8(srgba.0[1]), + linear_f32_from_gamma_u8(srgba.0[2]), + linear_f32_from_linear_u8(srgba.0[3]), + ]) + } +} + +impl From for Color32 { + fn from(rgba: Rgba) -> Color32 { + Color32([ + gamma_u8_from_linear_f32(rgba.0[0]), + gamma_u8_from_linear_f32(rgba.0[1]), + gamma_u8_from_linear_f32(rgba.0[2]), + linear_u8_from_linear_f32(rgba.0[3]), + ]) + } +} + +/// gamma [0, 255] -> linear [0, 1]. +pub fn linear_f32_from_gamma_u8(s: u8) -> f32 { + if s <= 10 { + s as f32 / 3294.6 + } else { + ((s as f32 + 14.025) / 269.025).powf(2.4) + } +} + +/// linear [0, 255] -> linear [0, 1]. +/// Useful for alpha-channel. +#[inline(always)] +pub fn linear_f32_from_linear_u8(a: u8) -> f32 { + a as f32 / 255.0 +} + +/// linear [0, 1] -> gamma [0, 255] (clamped). +/// Values outside this range will be clamped to the range. +pub fn gamma_u8_from_linear_f32(l: f32) -> u8 { + if l <= 0.0 { + 0 + } else if l <= 0.0031308 { + fast_round(3294.6 * l) + } else if l <= 1.0 { + fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025) + } else { + 255 + } +} + +/// linear [0, 1] -> linear [0, 255] (clamped). +/// Useful for alpha-channel. +#[inline(always)] +pub fn linear_u8_from_linear_f32(a: f32) -> u8 { + fast_round(a * 255.0) +} + +fn fast_round(r: f32) -> u8 { + (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 +} + +#[test] +pub fn test_srgba_conversion() { + for b in 0..=255 { + let l = linear_f32_from_gamma_u8(b); + assert!(0.0 <= l && l <= 1.0); + assert_eq!(gamma_u8_from_linear_f32(l), b); + } +} + +/// gamma [0, 1] -> linear [0, 1] (not clamped). +/// Works for numbers outside this range (e.g. negative numbers). +pub fn linear_from_gamma(gamma: f32) -> f32 { + if gamma < 0.0 { + -linear_from_gamma(-gamma) + } else if gamma <= 0.04045 { + gamma / 12.92 + } else { + ((gamma + 0.055) / 1.055).powf(2.4) + } +} + +/// linear [0, 1] -> gamma [0, 1] (not clamped). +/// Works for numbers outside this range (e.g. negative numbers). +pub fn gamma_from_linear(linear: f32) -> f32 { + if linear < 0.0 { + -gamma_from_linear(-linear) + } else if linear <= 0.0031308 { + 12.92 * linear + } else { + 1.055 * linear.powf(1.0 / 2.4) - 0.055 + } +} + +// ---------------------------------------------------------------------------- + +/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature +/// or with the `extra_debug_asserts` feature in debug builds. +#[macro_export] +macro_rules! ecolor_assert { + ($($arg: tt)*) => { + if cfg!(any( + feature = "extra_asserts", + all(feature = "extra_debug_asserts", debug_assertions), + )) { + assert!($($arg)*); + } + } +} + +// ---------------------------------------------------------------------------- + +/// Cheap and ugly. +/// Made for graying out disabled `Ui`s. +pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 { + let [mut r, mut g, mut b, mut a] = color.to_array(); + + if a == 0 { + r /= 2; + g /= 2; + b /= 2; + } else if a < 170 { + // Cheapish and looks ok. + // Works for e.g. grid stripes. + let div = (2 * 255 / a as i32) as u8; + r = r / 2 + target.r() / div; + g = g / 2 + target.g() / div; + b = b / 2 + target.b() / div; + a /= 2; + } else { + r = r / 2 + target.r() / 2; + g = g / 2 + target.g() / 2; + b = b / 2 + target.b() / 2; + } + Color32::from_rgba_premultiplied(r, g, b, a) +} diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs new file mode 100644 index 000000000..f9d671fd5 --- /dev/null +++ b/crates/ecolor/src/rgba.rs @@ -0,0 +1,266 @@ +use crate::{ + gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, + linear_u8_from_linear_f32, +}; + +/// 0-1 linear space `RGBA` color with premultiplied alpha. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] +pub struct Rgba(pub(crate) [f32; 4]); + +impl std::ops::Index for Rgba { + type Output = f32; + + #[inline(always)] + fn index(&self, index: usize) -> &f32 { + &self.0[index] + } +} + +impl std::ops::IndexMut for Rgba { + #[inline(always)] + fn index_mut(&mut self, index: usize) -> &mut f32 { + &mut self.0[index] + } +} + +#[inline(always)] +pub(crate) fn f32_hash(state: &mut H, f: f32) { + if f == 0.0 { + state.write_u8(0); + } else if f.is_nan() { + state.write_u8(1); + } else { + use std::hash::Hash; + f.to_bits().hash(state); + } +} + +#[allow(clippy::derive_hash_xor_eq)] +impl std::hash::Hash for Rgba { + #[inline] + fn hash(&self, state: &mut H) { + crate::f32_hash(state, self.0[0]); + crate::f32_hash(state, self.0[1]); + crate::f32_hash(state, self.0[2]); + crate::f32_hash(state, self.0[3]); + } +} + +impl Rgba { + pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0); + pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0); + pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0); + pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0); + pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0); + pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0); + + #[inline(always)] + pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { + Self([r, g, b, a]) + } + + #[inline(always)] + pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { + Self([r * a, g * a, b * a, a]) + } + + #[inline(always)] + pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { + let r = linear_f32_from_gamma_u8(r); + let g = linear_f32_from_gamma_u8(g); + let b = linear_f32_from_gamma_u8(b); + let a = linear_f32_from_linear_u8(a); + Self::from_rgba_premultiplied(r, g, b, a) + } + + #[inline(always)] + pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { + let r = linear_f32_from_gamma_u8(r); + let g = linear_f32_from_gamma_u8(g); + let b = linear_f32_from_gamma_u8(b); + let a = linear_f32_from_linear_u8(a); + Self::from_rgba_premultiplied(r * a, g * a, b * a, a) + } + + #[inline(always)] + pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self { + Self([r, g, b, 1.0]) + } + + #[inline(always)] + pub const fn from_gray(l: f32) -> Self { + Self([l, l, l, 1.0]) + } + + pub fn from_luminance_alpha(l: f32, a: f32) -> Self { + crate::ecolor_assert!(0.0 <= l && l <= 1.0); + crate::ecolor_assert!(0.0 <= a && a <= 1.0); + Self([l * a, l * a, l * a, a]) + } + + /// Transparent black + #[inline(always)] + pub fn from_black_alpha(a: f32) -> Self { + crate::ecolor_assert!(0.0 <= a && a <= 1.0); + Self([0.0, 0.0, 0.0, a]) + } + + /// Transparent white + #[inline(always)] + pub fn from_white_alpha(a: f32) -> Self { + crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a); + Self([a, a, a, a]) + } + + /// Return an additive version of this color (alpha = 0) + #[inline(always)] + 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 + #[inline(always)] + pub fn multiply(self, alpha: f32) -> Self { + Self([ + alpha * self[0], + alpha * self[1], + alpha * self[2], + alpha * self[3], + ]) + } + + #[inline(always)] + pub fn r(&self) -> f32 { + self.0[0] + } + + #[inline(always)] + pub fn g(&self) -> f32 { + self.0[1] + } + + #[inline(always)] + pub fn b(&self) -> f32 { + self.0[2] + } + + #[inline(always)] + pub fn a(&self) -> f32 { + self.0[3] + } + + /// How perceptually intense (bright) is the color? + #[inline] + pub fn intensity(&self) -> f32 { + 0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b() + } + + /// Returns an opaque version of self + pub fn to_opaque(&self) -> Self { + if self.a() == 0.0 { + // Additive or fully transparent black. + Self::from_rgb(self.r(), self.g(), self.b()) + } else { + // un-multiply alpha: + Self::from_rgb( + self.r() / self.a(), + self.g() / self.a(), + self.b() / self.a(), + ) + } + } + + /// Premultiplied RGBA + #[inline(always)] + pub fn to_array(&self) -> [f32; 4] { + [self.r(), self.g(), self.b(), self.a()] + } + + /// Premultiplied RGBA + #[inline(always)] + pub fn to_tuple(&self) -> (f32, f32, f32, f32) { + (self.r(), self.g(), self.b(), self.a()) + } + + /// unmultiply the alpha + pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { + let a = self.a(); + if a == 0.0 { + // Additive, let's assume we are black + self.0 + } else { + [self.r() / a, self.g() / a, self.b() / a, a] + } + } + + /// unmultiply the alpha + pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { + let [r, g, b, a] = self.to_rgba_unmultiplied(); + [ + gamma_u8_from_linear_f32(r), + gamma_u8_from_linear_f32(g), + gamma_u8_from_linear_f32(b), + linear_u8_from_linear_f32(a.abs()), + ] + } +} + +impl std::ops::Add for Rgba { + type Output = Rgba; + + #[inline(always)] + fn add(self, rhs: Rgba) -> Rgba { + Rgba([ + self[0] + rhs[0], + self[1] + rhs[1], + self[2] + rhs[2], + self[3] + rhs[3], + ]) + } +} + +impl std::ops::Mul for Rgba { + type Output = Rgba; + + #[inline(always)] + fn mul(self, other: Rgba) -> Rgba { + Rgba([ + self[0] * other[0], + self[1] * other[1], + self[2] * other[2], + self[3] * other[3], + ]) + } +} + +impl std::ops::Mul for Rgba { + type Output = Rgba; + + #[inline(always)] + fn mul(self, factor: f32) -> Rgba { + Rgba([ + self[0] * factor, + self[1] * factor, + self[2] * factor, + self[3] * factor, + ]) + } +} + +impl std::ops::Mul for f32 { + type Output = Rgba; + + #[inline(always)] + fn mul(self, rgba: Rgba) -> Rgba { + Rgba([ + self * rgba[0], + self * rgba[1], + self * rgba[2], + self * rgba[3], + ]) + } +} diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c447ebaa3..a76753f11 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -328,17 +328,19 @@ pub mod widgets; pub use accesskit; pub use epaint; +pub use epaint::ecolor; pub use epaint::emath; -pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2}; #[cfg(feature = "color-hex")] -pub use epaint::hex_color; +pub use ecolor::hex_color; +pub use ecolor::{Color32, Rgba}; +pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2}; pub use epaint::{ - color, mutex, + mutex, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, textures::{TextureFilter, TextureOptions, TexturesDelta}, - ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback, - PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId, + ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo, + Rounding, Shape, Stroke, TextureHandle, TextureId, }; pub mod text { diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 76a6d6696..cbefea886 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -448,6 +448,6 @@ impl Painter { fn tint_shape_towards(shape: &mut Shape, target: Color32) { epaint::shape_transform::adjust_colors(shape, &|color| { - *color = crate::color::tint_color_towards(*color, target); + *color = crate::ecolor::tint_color_towards(*color, target); }); } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 493b94ca4..f6bd2c4f9 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2,7 +2,7 @@ #![allow(clippy::if_same_then_else)] -use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText}; +use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText}; use epaint::{Rounding, Shadow, Stroke}; use std::collections::BTreeMap; @@ -495,7 +495,7 @@ impl Visuals { } pub fn weak_text_color(&self) -> Color32 { - crate::color::tint_color_towards(self.text_color(), self.window_fill()) + crate::ecolor::tint_color_towards(self.text_color(), self.window_fill()) } #[inline(always)] diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 7147f976e..1e7cc1486 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use crate::{ - color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer, + containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer, widgets::*, *, }; diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index 37bbe2178..7777bec62 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -2,7 +2,7 @@ use crate::util::fixed_cache::FixedCache; use crate::*; -use epaint::{color::*, *}; +use epaint::{ecolor::*, *}; fn contrast_color(color: impl Into) -> Color32 { if color.into().intensity() < 0.5 { diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index a31b1dc7d..91c0fb666 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -7,8 +7,8 @@ use std::{ }; use crate::*; -use epaint::color::Hsva; use epaint::util::FloatOrd; +use epaint::Hsva; use items::PlotItem; use legend::LegendWidget; diff --git a/crates/egui_demo_lib/src/color_test.rs b/crates/egui_demo_lib/src/color_test.rs index a68466ea9..a4637f622 100644 --- a/crates/egui_demo_lib/src/color_test.rs +++ b/crates/egui_demo_lib/src/color_test.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use egui::{color::*, widgets::color_picker::show_color, TextureOptions, *}; +use egui::{widgets::color_picker::show_color, TextureOptions, *}; const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0); diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 89f662a53..c45e1aa5f 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -59,8 +59,8 @@ pub fn drop_target( let mut stroke = style.bg_stroke; if is_being_dragged && !can_accept_what_is_being_dragged { // gray out: - fill = color::tint_color_towards(fill, ui.visuals().window_fill()); - stroke.color = color::tint_color_towards(stroke.color, ui.visuals().window_fill()); + fill = ecolor::tint_color_towards(fill, ui.visuals().window_fill()); + stroke.color = ecolor::tint_color_towards(stroke.color, ui.visuals().window_fill()); } ui.painter().set( diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index f4307db95..349708292 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -1,6 +1,6 @@ use super::*; use crate::LOREM_IPSUM; -use egui::{color::*, epaint::text::TextWrapping, *}; +use egui::{epaint::text::TextWrapping, *}; /// Showcase some ui code #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 8eda41521..84dec79b4 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -1,4 +1,4 @@ -use egui::{color::*, *}; +use egui::*; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 7b679cbf7..f5990d8ff 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -20,7 +20,6 @@ //! #![allow(clippy::float_cmp)] -#![allow(clippy::manual_range_contains)] use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index e8d7466bc..e9529fb39 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the epaint crate will be documented in this file. * Improve mixed CJK/Latin line-breaking ([#1986](https://github.com/emilk/egui/pull/1986)). * Added `Fonts::has_glyph(s)` for querying if a glyph is supported ([#2202](https://github.com/emilk/egui/pull/2202)). * Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space). +* Split out color into its own crate, `ecolor` ([#2399](https://github.com/emilk/egui/pull/2399)). ## 0.19.0 - 2022-08-20 diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index dda4127ed..4f7023e35 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -32,7 +32,13 @@ all-features = true default = ["default_fonts"] ## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`Vertex`] to `&[u8]`. -bytemuck = ["dep:bytemuck", "emath/bytemuck"] +bytemuck = ["dep:bytemuck", "emath/bytemuck", "ecolor/bytemuck"] + +## [`cint`](https://docs.rs/cint) enables interopability with other color libraries. +cint = ["ecolor/cint"] + +## Enable the [`hex_color`] macro. +color-hex = ["ecolor/color-hex"] ## This will automatically detect deadlocks due to double-locking on the same thread. ## If your app freezes, you may want to enable this! @@ -44,18 +50,22 @@ deadlock_detection = ["dep:backtrace"] default_fonts = [] ## Enable additional checks if debug assertions are enabled (debug builds). -extra_debug_asserts = ["emath/extra_debug_asserts"] +extra_debug_asserts = [ + "emath/extra_debug_asserts", + "ecolor/extra_debug_asserts", +] ## Always enable additional checks. -extra_asserts = ["emath/extra_asserts"] +extra_asserts = ["emath/extra_asserts", "ecolor/extra_asserts"] ## [`mint`](https://docs.rs/mint) enables interopability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra). mint = ["emath/mint"] ## Allow serialization using [`serde`](https://docs.rs/serde). -serde = ["dep:serde", "ahash/serde", "emath/serde"] +serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"] [dependencies] emath = { version = "0.19.0", path = "../emath" } +ecolor = { version = "0.19.0", path = "../ecolor" } ab_glyph = "0.2.11" ahash = { version = "0.8.1", default-features = false, features = [ @@ -67,12 +77,6 @@ nohash-hasher = "0.2" #! ### Optional dependencies bytemuck = { version = "1.7.2", optional = true, features = ["derive"] } -## [`cint`](https://docs.rs/cint) enables interopability with other color libraries. -cint = { version = "0.3.1", optional = true } - -## Enable the [`hex_color`] macro. -color-hex = { version = "0.2.0", optional = true } - ## Enable this when generating docs. document-features = { version = "0.2", optional = true } diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs deleted file mode 100644 index e6508f34b..000000000 --- a/crates/epaint/src/color.rs +++ /dev/null @@ -1,1068 +0,0 @@ -//! Color conversions and types. -//! -//! If you want a compact color representation, use [`Color32`]. -//! If you want to manipulate RGBA colors use [`Rgba`]. -//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`]. - -#![allow(clippy::wrong_self_convention)] - -/// This format is used for space-efficient color representation (32 bits). -/// -/// Instead of manipulating this directly it is often better -/// to first convert it to either [`Rgba`] or [`Hsva`]. -/// -/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha. -/// Alpha channel is in linear space. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] -pub struct Color32(pub(crate) [u8; 4]); - -impl std::ops::Index for Color32 { - type Output = u8; - - #[inline(always)] - fn index(&self, index: usize) -> &u8 { - &self.0[index] - } -} - -impl std::ops::IndexMut for Color32 { - #[inline(always)] - fn index_mut(&mut self, index: usize) -> &mut u8 { - &mut self.0[index] - } -} - -impl Color32 { - // Mostly follows CSS names: - - pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); - pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0); - pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96); - pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160); - pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220); - pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255); - - pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42); - pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0); - pub const RED: Color32 = Color32::from_rgb(255, 0, 0); - pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128); - - pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0); - pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0); - pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140); - - pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); - pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0); - pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90); - - pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B); - pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255); - pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6); - - pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0); - - pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128); - - /// An ugly color that is planned to be replaced before making it to the screen. - pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0); - - #[inline(always)] - pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { - Self([r, g, b, 255]) - } - - #[inline(always)] - pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self { - Self([r, g, b, 0]) - } - - /// From `sRGBA` with premultiplied alpha. - #[inline(always)] - pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - Self([r, g, b, a]) - } - - /// From `sRGBA` WITHOUT premultiplied alpha. - pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - if a == 255 { - Self::from_rgb(r, g, b) // common-case optimization - } else if a == 0 { - Self::TRANSPARENT // common-case optimization - } else { - let r_lin = linear_f32_from_gamma_u8(r); - let g_lin = linear_f32_from_gamma_u8(g); - let b_lin = linear_f32_from_gamma_u8(b); - let a_lin = linear_f32_from_linear_u8(a); - - let r = gamma_u8_from_linear_f32(r_lin * a_lin); - let g = gamma_u8_from_linear_f32(g_lin * a_lin); - let b = gamma_u8_from_linear_f32(b_lin * a_lin); - - Self::from_rgba_premultiplied(r, g, b, a) - } - } - - #[inline(always)] - pub const fn from_gray(l: u8) -> Self { - Self([l, l, l, 255]) - } - - #[inline(always)] - pub const fn from_black_alpha(a: u8) -> Self { - Self([0, 0, 0, a]) - } - - pub fn from_white_alpha(a: u8) -> Self { - Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into() - } - - #[inline(always)] - pub const fn from_additive_luminance(l: u8) -> Self { - Self([l, l, l, 0]) - } - - #[inline(always)] - pub const fn is_opaque(&self) -> bool { - self.a() == 255 - } - - #[inline(always)] - pub const fn r(&self) -> u8 { - self.0[0] - } - - #[inline(always)] - pub const fn g(&self) -> u8 { - self.0[1] - } - - #[inline(always)] - pub const fn b(&self) -> u8 { - self.0[2] - } - - #[inline(always)] - pub const fn a(&self) -> u8 { - self.0[3] - } - - /// Returns an opaque version of self - pub fn to_opaque(self) -> Self { - Rgba::from(self).to_opaque().into() - } - - /// Returns an additive version of self - #[inline(always)] - pub const fn additive(self) -> Self { - let [r, g, b, _] = self.to_array(); - Self([r, g, b, 0]) - } - - /// Premultiplied RGBA - #[inline(always)] - pub const fn to_array(&self) -> [u8; 4] { - [self.r(), self.g(), self.b(), self.a()] - } - - /// Premultiplied RGBA - #[inline(always)] - pub const fn to_tuple(&self) -> (u8, u8, u8, u8) { - (self.r(), self.g(), self.b(), self.a()) - } - - pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - Rgba::from(*self).to_srgba_unmultiplied() - } - - /// Multiply with 0.5 to make color half as opaque. - pub fn linear_multiply(self, factor: f32) -> Color32 { - crate::epaint_assert!(0.0 <= factor && factor <= 1.0); - // As an unfortunate side-effect of using premultiplied alpha - // we need a somewhat expensive conversion to linear space and back. - Rgba::from(self).multiply(factor).into() - } -} - -// ---------------------------------------------------------------------------- - -/// Construct a [`Color32`] from a hex RGB or RGBA string. -/// -/// ``` -/// # use epaint::{hex_color, Color32}; -/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22)); -/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12)); -/// ``` -#[cfg(feature = "color-hex")] -#[macro_export] -macro_rules! hex_color { - ($s:literal) => {{ - let array = $crate::color_hex::color_from_hex!($s); - if array.len() == 3 { - $crate::Color32::from_rgb(array[0], array[1], array[2]) - } else { - #[allow(unconditional_panic)] - $crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3]) - } - }}; -} - -#[cfg(feature = "color-hex")] -#[test] -fn test_from_rgb_hex() { - assert_eq!(Color32::from_rgb(0x20, 0x21, 0x22), hex_color!("#202122")); - assert_eq!( - Color32::from_rgb_additive(0x20, 0x21, 0x22), - hex_color!("#202122").additive() - ); -} - -#[cfg(feature = "color-hex")] -#[test] -fn test_from_rgba_hex() { - assert_eq!( - Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50), - hex_color!("20212250") - ); -} - -// ---------------------------------------------------------------------------- - -/// 0-1 linear space `RGBA` color with premultiplied alpha. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] -pub struct Rgba(pub(crate) [f32; 4]); - -impl std::ops::Index for Rgba { - type Output = f32; - - #[inline(always)] - fn index(&self, index: usize) -> &f32 { - &self.0[index] - } -} - -impl std::ops::IndexMut for Rgba { - #[inline(always)] - fn index_mut(&mut self, index: usize) -> &mut f32 { - &mut self.0[index] - } -} - -#[allow(clippy::derive_hash_xor_eq)] -impl std::hash::Hash for Rgba { - #[inline] - fn hash(&self, state: &mut H) { - crate::f32_hash(state, self.0[0]); - crate::f32_hash(state, self.0[1]); - crate::f32_hash(state, self.0[2]); - crate::f32_hash(state, self.0[3]); - } -} - -impl Rgba { - pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0); - pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0); - pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0); - pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0); - pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0); - pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0); - - #[inline(always)] - pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - Self([r, g, b, a]) - } - - #[inline(always)] - pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - Self([r * a, g * a, b * a, a]) - } - - #[inline(always)] - pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - let r = linear_f32_from_gamma_u8(r); - let g = linear_f32_from_gamma_u8(g); - let b = linear_f32_from_gamma_u8(b); - let a = linear_f32_from_linear_u8(a); - Self::from_rgba_premultiplied(r, g, b, a) - } - - #[inline(always)] - pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - let r = linear_f32_from_gamma_u8(r); - let g = linear_f32_from_gamma_u8(g); - let b = linear_f32_from_gamma_u8(b); - let a = linear_f32_from_linear_u8(a); - Self::from_rgba_premultiplied(r * a, g * a, b * a, a) - } - - #[inline(always)] - pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self { - Self([r, g, b, 1.0]) - } - - #[inline(always)] - pub const fn from_gray(l: f32) -> Self { - Self([l, l, l, 1.0]) - } - - pub fn from_luminance_alpha(l: f32, a: f32) -> Self { - crate::epaint_assert!(0.0 <= l && l <= 1.0); - crate::epaint_assert!(0.0 <= a && a <= 1.0); - Self([l * a, l * a, l * a, a]) - } - - /// Transparent black - #[inline(always)] - pub fn from_black_alpha(a: f32) -> Self { - crate::epaint_assert!(0.0 <= a && a <= 1.0); - Self([0.0, 0.0, 0.0, a]) - } - - /// Transparent white - #[inline(always)] - pub fn from_white_alpha(a: f32) -> Self { - crate::epaint_assert!(0.0 <= a && a <= 1.0, "a: {}", a); - Self([a, a, a, a]) - } - - /// Return an additive version of this color (alpha = 0) - #[inline(always)] - 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 - #[inline(always)] - pub fn multiply(self, alpha: f32) -> Self { - Self([ - alpha * self[0], - alpha * self[1], - alpha * self[2], - alpha * self[3], - ]) - } - - #[inline(always)] - pub fn r(&self) -> f32 { - self.0[0] - } - - #[inline(always)] - pub fn g(&self) -> f32 { - self.0[1] - } - - #[inline(always)] - pub fn b(&self) -> f32 { - self.0[2] - } - - #[inline(always)] - pub fn a(&self) -> f32 { - self.0[3] - } - - /// How perceptually intense (bright) is the color? - #[inline] - pub fn intensity(&self) -> f32 { - 0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b() - } - - /// Returns an opaque version of self - pub fn to_opaque(&self) -> Self { - if self.a() == 0.0 { - // Additive or fully transparent black. - Self::from_rgb(self.r(), self.g(), self.b()) - } else { - // un-multiply alpha: - Self::from_rgb( - self.r() / self.a(), - self.g() / self.a(), - self.b() / self.a(), - ) - } - } - - /// Premultiplied RGBA - #[inline(always)] - pub fn to_array(&self) -> [f32; 4] { - [self.r(), self.g(), self.b(), self.a()] - } - - /// Premultiplied RGBA - #[inline(always)] - pub fn to_tuple(&self) -> (f32, f32, f32, f32) { - (self.r(), self.g(), self.b(), self.a()) - } - - /// unmultiply the alpha - pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { - let a = self.a(); - if a == 0.0 { - // Additive, let's assume we are black - self.0 - } else { - [self.r() / a, self.g() / a, self.b() / a, a] - } - } - - /// unmultiply the alpha - pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - let [r, g, b, a] = self.to_rgba_unmultiplied(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a.abs()), - ] - } -} - -impl std::ops::Add for Rgba { - type Output = Rgba; - - #[inline(always)] - fn add(self, rhs: Rgba) -> Rgba { - Rgba([ - self[0] + rhs[0], - self[1] + rhs[1], - self[2] + rhs[2], - self[3] + rhs[3], - ]) - } -} - -impl std::ops::Mul for Rgba { - type Output = Rgba; - - #[inline(always)] - fn mul(self, other: Rgba) -> Rgba { - Rgba([ - self[0] * other[0], - self[1] * other[1], - self[2] * other[2], - self[3] * other[3], - ]) - } -} - -impl std::ops::Mul for Rgba { - type Output = Rgba; - - #[inline(always)] - fn mul(self, factor: f32) -> Rgba { - Rgba([ - self[0] * factor, - self[1] * factor, - self[2] * factor, - self[3] * factor, - ]) - } -} - -impl std::ops::Mul for f32 { - type Output = Rgba; - - #[inline(always)] - fn mul(self, rgba: Rgba) -> Rgba { - Rgba([ - self * rgba[0], - self * rgba[1], - self * rgba[2], - self * rgba[3], - ]) - } -} - -// ---------------------------------------------------------------------------- -// Color conversion: - -impl From for Rgba { - fn from(srgba: Color32) -> Rgba { - Rgba([ - linear_f32_from_gamma_u8(srgba.0[0]), - linear_f32_from_gamma_u8(srgba.0[1]), - linear_f32_from_gamma_u8(srgba.0[2]), - linear_f32_from_linear_u8(srgba.0[3]), - ]) - } -} - -impl From for Color32 { - fn from(rgba: Rgba) -> Color32 { - Color32([ - gamma_u8_from_linear_f32(rgba.0[0]), - gamma_u8_from_linear_f32(rgba.0[1]), - gamma_u8_from_linear_f32(rgba.0[2]), - linear_u8_from_linear_f32(rgba.0[3]), - ]) - } -} - -/// gamma [0, 255] -> linear [0, 1]. -pub fn linear_f32_from_gamma_u8(s: u8) -> f32 { - if s <= 10 { - s as f32 / 3294.6 - } else { - ((s as f32 + 14.025) / 269.025).powf(2.4) - } -} - -/// linear [0, 255] -> linear [0, 1]. -/// Useful for alpha-channel. -#[inline(always)] -pub fn linear_f32_from_linear_u8(a: u8) -> f32 { - a as f32 / 255.0 -} - -/// linear [0, 1] -> gamma [0, 255] (clamped). -/// Values outside this range will be clamped to the range. -pub fn gamma_u8_from_linear_f32(l: f32) -> u8 { - if l <= 0.0 { - 0 - } else if l <= 0.0031308 { - fast_round(3294.6 * l) - } else if l <= 1.0 { - fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025) - } else { - 255 - } -} - -/// linear [0, 1] -> linear [0, 255] (clamped). -/// Useful for alpha-channel. -#[inline(always)] -pub fn linear_u8_from_linear_f32(a: f32) -> u8 { - fast_round(a * 255.0) -} - -fn fast_round(r: f32) -> u8 { - (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 -} - -#[test] -pub fn test_srgba_conversion() { - for b in 0..=255 { - let l = linear_f32_from_gamma_u8(b); - assert!(0.0 <= l && l <= 1.0); - assert_eq!(gamma_u8_from_linear_f32(l), b); - } -} - -/// gamma [0, 1] -> linear [0, 1] (not clamped). -/// Works for numbers outside this range (e.g. negative numbers). -pub fn linear_from_gamma(gamma: f32) -> f32 { - if gamma < 0.0 { - -linear_from_gamma(-gamma) - } else if gamma <= 0.04045 { - gamma / 12.92 - } else { - ((gamma + 0.055) / 1.055).powf(2.4) - } -} - -/// linear [0, 1] -> gamma [0, 1] (not clamped). -/// Works for numbers outside this range (e.g. negative numbers). -pub fn gamma_from_linear(linear: f32) -> f32 { - if linear < 0.0 { - -gamma_from_linear(-linear) - } else if linear <= 0.0031308 { - 12.92 * linear - } else { - 1.055 * linear.powf(1.0 / 2.4) - 0.055 - } -} - -// ---------------------------------------------------------------------------- - -/// Hue, saturation, value, alpha. All in the range [0, 1]. -/// No premultiplied alpha. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Hsva { - /// hue 0-1 - pub h: f32, - - /// saturation 0-1 - pub s: f32, - - /// value 0-1 - pub v: f32, - - /// alpha 0-1. A negative value signifies an additive color (and alpha is ignored). - pub a: f32, -} - -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_f32_from_gamma_u8(srgba[0]), - linear_f32_from_gamma_u8(srgba[1]), - linear_f32_from_gamma_u8(srgba[2]), - linear_f32_from_linear_u8(srgba[3]), - ) - } - - /// From `sRGBA` without premultiplied alpha - pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { - Self::from_rgba_unmultiplied( - linear_f32_from_gamma_u8(srgba[0]), - linear_f32_from_gamma_u8(srgba[1]), - linear_f32_from_gamma_u8(srgba[2]), - linear_f32_from_linear_u8(srgba[3]), - ) - } - - /// From linear RGBA with premultiplied alpha - pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - #![allow(clippy::many_single_char_names)] - if a == 0.0 { - if r == 0.0 && b == 0.0 && a == 0.0 { - Hsva::default() - } else { - Hsva::from_additive_rgb([r, g, b]) - } - } else { - let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]); - Hsva { h, s, v, a } - } - } - - /// From linear RGBA without premultiplied alpha - pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - #![allow(clippy::many_single_char_names)] - let (h, s, v) = hsv_from_rgb([r, g, b]); - 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_f32_from_gamma_u8(r), - linear_f32_from_gamma_u8(g), - linear_f32_from_gamma_u8(b), - ]) - } - - // ------------------------------------------------------------------------ - - pub fn to_opaque(self) -> Self { - Self { a: 1.0, ..self } - } - - 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_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - ] - } - - 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] - } - } - - /// Represents additive colors using a negative alpha. - 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(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a), - ] - } - - pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - let [r, g, b, a] = self.to_rgba_unmultiplied(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a.abs()), - ] - } -} - -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[0], rgba.0[1], rgba.0[2], rgba.0[3]) - } -} - -impl From for Color32 { - fn from(hsva: Hsva) -> Color32 { - Color32::from(Rgba::from(hsva)) - } -} - -impl From for Hsva { - fn from(srgba: Color32) -> Hsva { - Hsva::from(Rgba::from(srgba)) - } -} - -/// All ranges in 0-1, rgb is linear. -pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { - #![allow(clippy::many_single_char_names)] - let min = r.min(g.min(b)); - let max = r.max(g.max(b)); // value - - let range = max - min; - - let h = if max == min { - 0.0 // hue is undefined - } else if max == r { - (g - b) / (6.0 * range) - } else if max == g { - (b - r) / (6.0 * range) + 1.0 / 3.0 - } else { - // max == b - (r - g) / (6.0 * range) + 2.0 / 3.0 - }; - let h = (h + 1.0).fract(); // wrap - let s = if max == 0.0 { 0.0 } else { 1.0 - min / max }; - (h, s, max) -} - -/// All ranges in 0-1, rgb is linear. -pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] { - #![allow(clippy::many_single_char_names)] - let h = (h.fract() + 1.0).fract(); // wrap - let s = s.clamp(0.0, 1.0); - - let f = h * 6.0 - (h * 6.0).floor(); - let p = v * (1.0 - s); - let q = v * (1.0 - f * s); - let t = v * (1.0 - (1.0 - f) * s); - - match (h * 6.0).floor() as i32 % 6 { - 0 => [v, t, p], - 1 => [q, v, p], - 2 => [p, v, t], - 3 => [p, q, v], - 4 => [t, p, v], - 5 => [v, p, q], - _ => unreachable!(), - } -} - -#[test] -#[ignore] // a bit expensive -fn test_hsv_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 hsva = Hsva::from(srgba); - assert_eq!(srgba, Color32::from(hsva)); - } - } - } -} - -// ---------------------------------------------------------------------------- - -/// Like Hsva but with the `v` value (brightness) being gamma corrected -/// so that it is somewhat perceptually even. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub 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. A negative value signifies an additive color (and alpha is ignored). - pub a: f32, -} - -impl From for Rgba { - fn from(hsvag: HsvaGamma) -> Rgba { - Hsva::from(hsvag).into() - } -} - -impl From for Color32 { - fn from(hsvag: HsvaGamma) -> Color32 { - 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_gamma(v), - a, - } - } -} - -impl From for HsvaGamma { - fn from(rgba: Rgba) -> HsvaGamma { - Hsva::from(rgba).into() - } -} - -impl From for HsvaGamma { - fn from(srgba: Color32) -> HsvaGamma { - Hsva::from(srgba).into() - } -} - -impl From for HsvaGamma { - fn from(hsva: Hsva) -> HsvaGamma { - let Hsva { h, s, v, a } = hsva; - HsvaGamma { - h, - s, - v: gamma_from_linear(v), - a, - } - } -} - -// ---------------------------------------------------------------------------- - -/// Cheap and ugly. -/// Made for graying out disabled `Ui`s. -pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 { - let [mut r, mut g, mut b, mut a] = color.to_array(); - - if a == 0 { - r /= 2; - g /= 2; - b /= 2; - } else if a < 170 { - // Cheapish and looks ok. - // Works for e.g. grid stripes. - let div = (2 * 255 / a as i32) as u8; - r = r / 2 + target.r() / div; - g = g / 2 + target.g() / div; - b = b / 2 + target.b() / div; - a /= 2; - } else { - r = r / 2 + target.r() / 2; - g = g / 2 + target.g() / 2; - b = b / 2 + target.b() / 2; - } - Color32::from_rgba_premultiplied(r, g, b, a) -} - -#[cfg(feature = "cint")] -mod impl_cint { - use super::*; - use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha}; - - // ---- Color32 ---- - - impl From>> for Color32 { - fn from(srgba: Alpha>) -> Self { - let Alpha { - color: EncodedSrgb { r, g, b }, - alpha: a, - } = srgba; - - Color32::from_rgba_unmultiplied(r, g, b, a) - } - } - - // No From for Alpha<_> because Color32 is premultiplied - - impl From>> for Color32 { - fn from(srgba: PremultipliedAlpha>) -> Self { - let PremultipliedAlpha { - color: EncodedSrgb { r, g, b }, - alpha: a, - } = srgba; - - Color32::from_rgba_premultiplied(r, g, b, a) - } - } - - impl From for PremultipliedAlpha> { - fn from(col: Color32) -> Self { - let (r, g, b, a) = col.to_tuple(); - - PremultipliedAlpha { - color: EncodedSrgb { r, g, b }, - alpha: a, - } - } - } - - impl From>> for Color32 { - fn from(srgba: PremultipliedAlpha>) -> Self { - let PremultipliedAlpha { - color: EncodedSrgb { r, g, b }, - alpha: a, - } = srgba; - - // This is a bit of an abuse of the function name but it does what we want. - let r = linear_u8_from_linear_f32(r); - let g = linear_u8_from_linear_f32(g); - let b = linear_u8_from_linear_f32(b); - let a = linear_u8_from_linear_f32(a); - - Color32::from_rgba_premultiplied(r, g, b, a) - } - } - - impl From for PremultipliedAlpha> { - fn from(col: Color32) -> Self { - let (r, g, b, a) = col.to_tuple(); - - // This is a bit of an abuse of the function name but it does what we want. - let r = linear_f32_from_linear_u8(r); - let g = linear_f32_from_linear_u8(g); - let b = linear_f32_from_linear_u8(b); - let a = linear_f32_from_linear_u8(a); - - PremultipliedAlpha { - color: EncodedSrgb { r, g, b }, - alpha: a, - } - } - } - - impl ColorInterop for Color32 { - type CintTy = PremultipliedAlpha>; - } - - // ---- Rgba ---- - - impl From>> for Rgba { - fn from(srgba: PremultipliedAlpha>) -> Self { - let PremultipliedAlpha { - color: LinearSrgb { r, g, b }, - alpha: a, - } = srgba; - - Rgba([r, g, b, a]) - } - } - - impl From for PremultipliedAlpha> { - fn from(col: Rgba) -> Self { - let (r, g, b, a) = col.to_tuple(); - - PremultipliedAlpha { - color: LinearSrgb { r, g, b }, - alpha: a, - } - } - } - - impl ColorInterop for Rgba { - type CintTy = PremultipliedAlpha>; - } - - // ---- Hsva ---- - - impl From>> for Hsva { - fn from(srgba: Alpha>) -> Self { - let Alpha { - color: Hsv { h, s, v }, - alpha: a, - } = srgba; - - Hsva::new(h, s, v, a) - } - } - - impl From for Alpha> { - fn from(col: Hsva) -> Self { - let Hsva { h, s, v, a } = col; - - Alpha { - color: Hsv { h, s, v }, - alpha: a, - } - } - } - - impl ColorInterop for Hsva { - type CintTy = Alpha>; - } - - // ---- HsvaGamma ---- - - impl ColorInterop for HsvaGamma { - type CintTy = Alpha>; - } - - impl From>> for HsvaGamma { - fn from(srgba: Alpha>) -> Self { - let Alpha { - color: Hsv { h, s, v }, - alpha: a, - } = srgba; - - Hsva::new(h, s, v, a).into() - } - } - - impl From for Alpha> { - fn from(col: HsvaGamma) -> Self { - let Hsva { h, s, v, a } = col.into(); - - Alpha { - color: Hsv { h, s, v }, - alpha: a, - } - } - } -} diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index dcd945aab..c3d1a9524 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -127,7 +127,7 @@ impl ColorImage { let s = 1.0; let v = 1.0; let a = y as f32 / height as f32; - img[(x, y)] = crate::color::Hsva { h, s, v, a }.into(); + img[(x, y)] = crate::Hsva { h, s, v, a }.into(); } } img diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 794778a20..41abb4af7 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -13,7 +13,6 @@ #![allow(clippy::manual_range_contains)] mod bezier; -pub mod color; pub mod image; mod mesh; pub mod mutex; @@ -31,7 +30,6 @@ pub mod util; pub use { bezier::{CubicBezierShape, QuadraticBezierShape}, - color::{Color32, Rgba}, image::{ColorImage, FontImage, ImageData, ImageDelta}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, @@ -48,13 +46,15 @@ pub use { textures::TextureManager, }; +pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba}; pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; pub use ahash; +pub use ecolor; pub use emath; #[cfg(feature = "color-hex")] -pub use color_hex; +pub use ecolor::hex_color; /// The UV coordinate of a white region of the texture mesh. /// The default egui texture has the top-left corner pixel fully white.