diff --git a/Cargo.toml b/Cargo.toml index 9c52ea8f..206409bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ debug = ["iced_winit/debug"] tokio = ["iced_futures/tokio"] # Enables `async-std` as the `executor::Default` on native platforms async-std = ["iced_futures/async-std"] +# Enables advanced color conversion via `palette` +palette = ["iced_core/palette"] [badges] maintenance = { status = "actively-developed" } @@ -39,6 +41,7 @@ members = [ "winit", "examples/bezier_tool", "examples/clock", + "examples/color_palette", "examples/counter", "examples/custom_widget", "examples/download_progress", @@ -57,6 +60,7 @@ members = [ ] [dependencies] +iced_core = { version = "0.2", path = "core" } iced_futures = { version = "0.1", path = "futures" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/core/Cargo.toml b/core/Cargo.toml index 837f6aae..b52bf315 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -8,3 +8,7 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] + +[dependencies.palette] +version = "0.5.0" +optional = true diff --git a/core/src/color.rs b/core/src/color.rs index db509b88..a4c3d87c 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,10 +1,16 @@ +#[cfg(feature = "palette")] +use palette::rgb::{Srgb, Srgba}; + /// A color in the sRGB color space. -#[derive(Debug, Clone, Copy, PartialEq)] -#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Color { + /// Red component, 0.0 - 1.0 pub r: f32, + /// Green component, 0.0 - 1.0 pub g: f32, + /// Blue component, 0.0 - 1.0 pub b: f32, + /// Transparency, 0.0 - 1.0 pub a: f32, } @@ -33,11 +39,45 @@ impl Color { a: 0.0, }; + /// Creates a new [`Color`]. + /// + /// In debug mode, it will panic if the values are not in the correct + /// range: 0.0 - 1.0 + /// + /// [`Color`]: struct.Color.html + pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color { + debug_assert!( + (0.0..=1.0).contains(&r), + "Red component must be on [0, 1]" + ); + debug_assert!( + (0.0..=1.0).contains(&g), + "Green component must be on [0, 1]" + ); + debug_assert!( + (0.0..=1.0).contains(&b), + "Blue component must be on [0, 1]" + ); + debug_assert!( + (0.0..=1.0).contains(&a), + "Alpha component must be on [0, 1]" + ); + + Color { r, g, b, a } + } + /// Creates a [`Color`] from its RGB components. /// /// [`Color`]: struct.Color.html pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color { - Color { r, g, b, a: 1.0 } + Color::from_rgba(r, g, b, 1.0f32) + } + + /// Creates a [`Color`] from its RGBA components. + /// + /// [`Color`]: struct.Color.html + pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color { + Color { r, g, b, a } } /// Creates a [`Color`] from its RGB8 components. @@ -80,16 +120,114 @@ impl Color { self.a, ] } + + /// Inverts the [`Color`] in-place. + /// + /// [`Color`]: struct.Color.html + pub fn invert(&mut self) { + self.r = 1.0f32 - self.r; + self.b = 1.0f32 - self.g; + self.g = 1.0f32 - self.b; + } + + /// Returns the inverted [`Color`]. + /// + /// [`Color`]: struct.Color.html + pub fn inverse(self) -> Color { + Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) + } } impl From<[f32; 3]> for Color { fn from([r, g, b]: [f32; 3]) -> Self { - Color { r, g, b, a: 1.0 } + Color::new(r, g, b, 1.0) } } impl From<[f32; 4]> for Color { fn from([r, g, b, a]: [f32; 4]) -> Self { - Color { r, g, b, a } + Color::new(r, g, b, a) + } +} + +#[cfg(feature = "palette")] +/// Converts from palette's `Srgba` type to a [`Color`]. +/// +/// [`Color`]: struct.Color.html +impl From for Color { + fn from(srgba: Srgba) -> Self { + Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha) + } +} + +#[cfg(feature = "palette")] +/// Converts from [`Color`] to palette's `Srgba` type. +/// +/// [`Color`]: struct.Color.html +impl From for Srgba { + fn from(c: Color) -> Self { + Srgba::new(c.r, c.g, c.b, c.a) + } +} + +#[cfg(feature = "palette")] +/// Converts from palette's `Srgb` type to a [`Color`]. +/// +/// [`Color`]: struct.Color.html +impl From for Color { + fn from(srgb: Srgb) -> Self { + Color::new(srgb.red, srgb.green, srgb.blue, 1.0) + } +} + +#[cfg(feature = "palette")] +/// Converts from [`Color`] to palette's `Srgb` type. +/// +/// [`Color`]: struct.Color.html +/// [`Srgb`]: ../palette/rgb/type.Srgb.html +impl From for Srgb { + fn from(c: Color) -> Self { + Srgb::new(c.r, c.g, c.b) + } +} + +#[cfg(feature = "palette")] +#[cfg(test)] +mod tests { + use super::*; + use palette::Blend; + + #[test] + fn srgba_traits() { + let c = Color::from_rgb(0.5, 0.4, 0.3); + // Round-trip conversion to the palette:Srgba type + let s: Srgba = c.into(); + let r: Color = s.into(); + assert_eq!(c, r); + } + + #[test] + fn color_manipulation() { + let c1 = Color::from_rgb(0.5, 0.4, 0.3); + let c2 = Color::from_rgb(0.2, 0.5, 0.3); + + // Convert to linear color for manipulation + let l1 = Srgba::from(c1).into_linear(); + let l2 = Srgba::from(c2).into_linear(); + + // Take the lighter of each of the RGB components + let lighter = l1.lighten(l2); + + // Convert back to our Color + let r: Color = Srgba::from_linear(lighter).into(); + assert_eq!( + r, + Color { + r: 0.5, + g: 0.5, + b: 0.3, + a: 1.0 + } + ); } } diff --git a/examples/README.md b/examples/README.md index 5aea51eb..f67a0dd2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -71,6 +71,7 @@ A bunch of simpler examples exist: - [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using [`lyon`]. - [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. +- [`color_palette`](color_palette), a color palette generator based on a user-defined root color. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). - [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle. - [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml new file mode 100644 index 00000000..00f33e20 --- /dev/null +++ b/examples/color_palette/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "color_palette" +version = "0.1.0" +authors = ["Clark Moody "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "palette"] } +palette = "0.5.0" diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md new file mode 100644 index 00000000..e70188f8 --- /dev/null +++ b/examples/color_palette/README.md @@ -0,0 +1,15 @@ +## Color palette + +A color palette generator, based on a user-defined root color. + + + +You can run it with `cargo run`: + +``` +cargo run --package color_palette +``` diff --git a/examples/color_palette/screenshot.png b/examples/color_palette/screenshot.png new file mode 100644 index 00000000..aa4772e0 Binary files /dev/null and b/examples/color_palette/screenshot.png differ diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs new file mode 100644 index 00000000..073a6734 --- /dev/null +++ b/examples/color_palette/src/main.rs @@ -0,0 +1,444 @@ +use iced::{ + canvas, slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, + Length, Point, Row, Sandbox, Settings, Size, Slider, Text, Vector, + VerticalAlignment, +}; +use palette::{self, Limited}; +use std::marker::PhantomData; +use std::ops::RangeInclusive; + +pub fn main() { + ColorPalette::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +pub struct ColorPalette { + theme: Theme, + rgb: ColorPicker, + hsl: ColorPicker, + hsv: ColorPicker, + hwb: ColorPicker, + lab: ColorPicker, + lch: ColorPicker, + canvas_layer: canvas::layer::Cache, +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + RgbColorChanged(Color), + HslColorChanged(palette::Hsl), + HsvColorChanged(palette::Hsv), + HwbColorChanged(palette::Hwb), + LabColorChanged(palette::Lab), + LchColorChanged(palette::Lch), +} + +impl Sandbox for ColorPalette { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Color palette - Iced") + } + + fn update(&mut self, message: Message) { + let srgb = match message { + Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), + Message::HslColorChanged(hsl) => palette::Srgb::from(hsl), + Message::HsvColorChanged(hsv) => palette::Srgb::from(hsv), + Message::HwbColorChanged(hwb) => palette::Srgb::from(hwb), + Message::LabColorChanged(lab) => palette::Srgb::from(lab), + Message::LchColorChanged(lch) => palette::Srgb::from(lch), + }; + + self.theme = Theme::new(srgb.clamp()); + self.canvas_layer.clear(); + } + + fn view(&mut self) -> Element { + let base = self.theme.base; + + let srgb = palette::Srgb::from(base); + let hsl = palette::Hsl::from(srgb); + let hsv = palette::Hsv::from(srgb); + let hwb = palette::Hwb::from(srgb); + let lab = palette::Lab::from(srgb); + let lch = palette::Lch::from(srgb); + + Column::new() + .padding(10) + .spacing(10) + .push(self.rgb.view(base).map(Message::RgbColorChanged)) + .push(self.hsl.view(hsl).map(Message::HslColorChanged)) + .push(self.hsv.view(hsv).map(Message::HsvColorChanged)) + .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) + .push(self.lab.view(lab).map(Message::LabColorChanged)) + .push(self.lch.view(lch).map(Message::LchColorChanged)) + .push( + Canvas::new() + .width(Length::Fill) + .height(Length::Fill) + .push(self.canvas_layer.with(&self.theme)), + ) + .into() + } +} + +#[derive(Debug)] +pub struct Theme { + lower: Vec, + base: Color, + higher: Vec, +} + +impl Theme { + pub fn new(base: impl Into) -> Theme { + use palette::{Hsl, Hue, Shade, Srgb}; + + let base = base.into(); + + // Convert to HSL color for manipulation + let hsl = Hsl::from(Srgb::from(base)); + + let lower = [ + hsl.shift_hue(-135.0).lighten(0.075), + hsl.shift_hue(-120.0), + hsl.shift_hue(-105.0).darken(0.075), + hsl.darken(0.075), + ]; + + let higher = [ + hsl.lighten(0.075), + hsl.shift_hue(105.0).darken(0.075), + hsl.shift_hue(120.0), + hsl.shift_hue(135.0).lighten(0.075), + ]; + + Theme { + lower: lower + .iter() + .map(|&color| Srgb::from(color).clamp().into()) + .collect(), + base, + higher: higher + .iter() + .map(|&color| Srgb::from(color).clamp().into()) + .collect(), + } + } + + pub fn len(&self) -> usize { + self.lower.len() + self.higher.len() + 1 + } + + pub fn colors(&self) -> impl Iterator { + self.lower + .iter() + .chain(std::iter::once(&self.base)) + .chain(self.higher.iter()) + } +} + +impl canvas::Drawable for Theme { + fn draw(&self, frame: &mut canvas::Frame) { + use canvas::Path; + use palette::{Hsl, Srgb}; + + let pad = 20.0; + + let box_size = Size { + width: frame.width() / self.len() as f32, + height: frame.height() / 2.0 - pad, + }; + + let triangle = Path::new(|path| { + path.move_to(Point { x: 0.0, y: -0.5 }); + path.line_to(Point { x: -0.5, y: 0.0 }); + path.line_to(Point { x: 0.5, y: 0.0 }); + path.close(); + }); + + let mut text = canvas::Text { + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Top, + size: 15.0, + ..canvas::Text::default() + }; + + for (i, &color) in self.colors().enumerate() { + let anchor = Point { + x: (i as f32) * box_size.width, + y: 0.0, + }; + let rect = Path::rectangle(anchor, box_size); + frame.fill(&rect, color); + + // We show a little indicator for the base color + if color == self.base { + let triangle_x = anchor.x + box_size.width / 2.0; + + frame.with_save(|frame| { + frame.translate(Vector::new(triangle_x, 0.0)); + frame.scale(10.0); + frame.rotate(std::f32::consts::PI); + + frame.fill(&triangle, Color::WHITE); + }); + + frame.with_save(|frame| { + frame.translate(Vector::new(triangle_x, box_size.height)); + frame.scale(10.0); + + frame.fill(&triangle, Color::WHITE); + }); + } + + frame.fill_text(canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height, + }, + ..text + }); + } + + text.vertical_alignment = VerticalAlignment::Bottom; + + let hsl = Hsl::from(Srgb::from(self.base)); + for i in 0..self.len() { + let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0); + let graded = Hsl { + lightness: 1.0 - pct, + ..hsl + }; + let color: Color = Srgb::from(graded.clamp()).into(); + + let anchor = Point { + x: (i as f32) * box_size.width, + y: box_size.height + 2.0 * pad, + }; + + let rect = Path::rectangle(anchor, box_size); + frame.fill(&rect, color); + + frame.fill_text(canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height + 2.0 * pad, + }, + ..text + }); + } + } +} + +impl Default for Theme { + fn default() -> Self { + Theme::new(Color::from_rgb8(75, 128, 190)) + } +} + +fn color_hex_string(color: &Color) -> String { + format!( + "#{:x}{:x}{:x}", + (255.0 * color.r).round() as u8, + (255.0 * color.g).round() as u8, + (255.0 * color.b).round() as u8 + ) +} + +#[derive(Default)] +struct ColorPicker { + sliders: [slider::State; 3], + color_space: PhantomData, +} + +trait ColorSpace: Sized { + const LABEL: &'static str; + const COMPONENT_RANGES: [RangeInclusive; 3]; + + fn new(a: f32, b: f32, c: f32) -> Self; + + fn components(&self) -> [f32; 3]; + + fn to_string(&self) -> String; +} + +impl ColorPicker { + fn view(&mut self, color: C) -> Element { + let [c1, c2, c3] = color.components(); + let [s1, s2, s3] = &mut self.sliders; + let [cr1, cr2, cr3] = C::COMPONENT_RANGES; + + Row::new() + .spacing(10) + .align_items(Align::Center) + .push(Text::new(C::LABEL).width(Length::Units(50))) + .push(Slider::new(s1, cr1, c1, move |v| C::new(v, c2, c3))) + .push(Slider::new(s2, cr2, c2, move |v| C::new(c1, v, c3))) + .push(Slider::new(s3, cr3, c3, move |v| C::new(c1, c2, v))) + .push( + Text::new(color.to_string()) + .width(Length::Units(185)) + .size(14), + ) + .into() + } +} + +impl ColorSpace for Color { + const LABEL: &'static str = "RGB"; + const COMPONENT_RANGES: [RangeInclusive; 3] = + [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(r: f32, g: f32, b: f32) -> Self { + Color::from_rgb(r, g, b) + } + + fn components(&self) -> [f32; 3] { + [self.r, self.g, self.b] + } + + fn to_string(&self) -> String { + format!( + "rgb({:.0}, {:.0}, {:.0})", + 255.0 * self.r, + 255.0 * self.g, + 255.0 * self.b + ) + } +} + +impl ColorSpace for palette::Hsl { + const LABEL: &'static str = "HSL"; + const COMPONENT_RANGES: [RangeInclusive; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, saturation: f32, lightness: f32) -> Self { + palette::Hsl::new( + palette::RgbHue::from_degrees(hue), + saturation, + lightness, + ) + } + + fn components(&self) -> [f32; 3] { + [ + self.hue.to_positive_degrees(), + self.saturation, + self.lightness, + ] + } + + fn to_string(&self) -> String { + format!( + "hsl({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.saturation, + 100.0 * self.lightness + ) + } +} + +impl ColorSpace for palette::Hsv { + const LABEL: &'static str = "HSV"; + const COMPONENT_RANGES: [RangeInclusive; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, saturation: f32, value: f32) -> Self { + palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value) + } + + fn components(&self) -> [f32; 3] { + [self.hue.to_positive_degrees(), self.saturation, self.value] + } + + fn to_string(&self) -> String { + format!( + "hsv({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.saturation, + 100.0 * self.value + ) + } +} + +impl ColorSpace for palette::Hwb { + const LABEL: &'static str = "HWB"; + const COMPONENT_RANGES: [RangeInclusive; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { + palette::Hwb::new( + palette::RgbHue::from_degrees(hue), + whiteness, + blackness, + ) + } + + fn components(&self) -> [f32; 3] { + [ + self.hue.to_positive_degrees(), + self.whiteness, + self.blackness, + ] + } + + fn to_string(&self) -> String { + format!( + "hwb({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.whiteness, + 100.0 * self.blackness + ) + } +} + +impl ColorSpace for palette::Lab { + const LABEL: &'static str = "Lab"; + const COMPONENT_RANGES: [RangeInclusive; 3] = + [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; + + fn new(l: f32, a: f32, b: f32) -> Self { + palette::Lab::new(l, a, b) + } + + fn components(&self) -> [f32; 3] { + [self.l, self.a, self.b] + } + + fn to_string(&self) -> String { + format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b) + } +} + +impl ColorSpace for palette::Lch { + const LABEL: &'static str = "Lch"; + const COMPONENT_RANGES: [RangeInclusive; 3] = + [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; + + fn new(l: f32, chroma: f32, hue: f32) -> Self { + palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue)) + } + + fn components(&self) -> [f32; 3] { + [self.l, self.chroma, self.hue.to_positive_degrees()] + } + + fn to_string(&self) -> String { + format!( + "Lch({:.1}, {:.1}, {:.1})", + self.l, + self.chroma, + self.hue.to_positive_degrees() + ) + } +}