From a28799b65e4440fae72797006eaf30e4f45f38e5 Mon Sep 17 00:00:00 2001 From: Max Unsted Date: Mon, 2 May 2022 16:05:40 +0100 Subject: add pure version of color_palette example --- Cargo.toml | 1 + examples/pure/color_palette/Cargo.toml | 10 + examples/pure/color_palette/README.md | 15 + examples/pure/color_palette/screenshot.png | Bin 0 -> 44798 bytes examples/pure/color_palette/src/main.rs | 465 +++++++++++++++++++++++++++++ 5 files changed, 491 insertions(+) create mode 100644 examples/pure/color_palette/Cargo.toml create mode 100644 examples/pure/color_palette/README.md create mode 100644 examples/pure/color_palette/screenshot.png create mode 100644 examples/pure/color_palette/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 56b4d06a..2f6727eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ members = [ "examples/url_handler", "examples/websocket", "examples/pure/arc", + "examples/pure/color_palette", "examples/pure/component", "examples/pure/counter", "examples/pure/game_of_life", diff --git a/examples/pure/color_palette/Cargo.toml b/examples/pure/color_palette/Cargo.toml new file mode 100644 index 00000000..d08309d5 --- /dev/null +++ b/examples/pure/color_palette/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pure_color_palette" +version = "0.1.0" +authors = ["Clark Moody "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["pure", "canvas", "palette"] } +palette = "0.6.0" diff --git a/examples/pure/color_palette/README.md b/examples/pure/color_palette/README.md new file mode 100644 index 00000000..f90020b1 --- /dev/null +++ b/examples/pure/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 pure_color_palette +``` diff --git a/examples/pure/color_palette/screenshot.png b/examples/pure/color_palette/screenshot.png new file mode 100644 index 00000000..e8da35c4 Binary files /dev/null and b/examples/pure/color_palette/screenshot.png differ diff --git a/examples/pure/color_palette/src/main.rs b/examples/pure/color_palette/src/main.rs new file mode 100644 index 00000000..118c80cc --- /dev/null +++ b/examples/pure/color_palette/src/main.rs @@ -0,0 +1,465 @@ +use iced::pure::{ + column, row, text, + widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}, + widget::Slider, + Element, Sandbox, +}; +use iced::{ + alignment, Alignment, Color, Length, Point, Rectangle, Settings, Size, + Vector, +}; +use palette::{self, convert::FromColor, Hsl, Srgb}; +use std::marker::PhantomData; +use std::ops::RangeInclusive; + +pub fn main() -> iced::Result { + 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, +} + +#[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_color(hsl), + Message::HsvColorChanged(hsv) => palette::Srgb::from_color(hsv), + Message::HwbColorChanged(hwb) => palette::Srgb::from_color(hwb), + Message::LabColorChanged(lab) => palette::Srgb::from_color(lab), + Message::LchColorChanged(lch) => palette::Srgb::from_color(lch), + }; + + self.theme = Theme::new(srgb); + } + + fn view(&self) -> Element { + let base = self.theme.base; + + let srgb = palette::Srgb::from(base); + let hsl = palette::Hsl::from_color(srgb); + let hsv = palette::Hsv::from_color(srgb); + let hwb = palette::Hwb::from_color(srgb); + let lab = palette::Lab::from_color(srgb); + let lch = palette::Lch::from_color(srgb); + + column() + .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(self.theme.view()) + .into() + } +} + +#[derive(Debug)] +pub struct Theme { + lower: Vec, + base: Color, + higher: Vec, + canvas_cache: canvas::Cache, +} + +impl Theme { + pub fn new(base: impl Into) -> Theme { + use palette::{Hue, Shade}; + + let base = base.into(); + + // Convert to HSL color for manipulation + let hsl = Hsl::from_color(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(color).into()) + .collect(), + base, + higher: higher + .iter() + .map(|&color| Srgb::from_color(color).into()) + .collect(), + canvas_cache: canvas::Cache::default(), + } + } + + 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()) + } + + pub fn view(&self) -> Element { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn draw(&self, frame: &mut Frame) { + 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: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::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, + }; + frame.fill_rectangle(anchor, box_size, 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 = alignment::Vertical::Bottom; + + let hsl = Hsl::from_color(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_color(graded).into(); + + let anchor = Point { + x: (i as f32) * box_size.width, + y: box_size.height + 2.0 * pad, + }; + + frame.fill_rectangle(anchor, box_size, 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 canvas::Program for Theme { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &iced::Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { + let theme = self.canvas_cache.draw(bounds.size(), |frame| { + self.draw(frame); + }); + + vec![theme] + } +} + +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 { + 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(&self, color: C) -> Element { + let [c1, c2, c3] = color.components(); + let [cr1, cr2, cr3] = C::COMPONENT_RANGES; + + fn slider<'a, C: Clone>( + range: RangeInclusive, + component: f32, + update: impl Fn(f32) -> C + 'a, + ) -> Slider<'a, f64, C, iced::Renderer> { + Slider::new(range, f64::from(component), move |v| update(v as f32)) + .step(0.01) + } + + row() + .spacing(10) + .align_items(Alignment::Center) + .push(text(C::LABEL).width(Length::Units(50))) + .push(slider(cr1, c1, move |v| C::new(v, c2, c3))) + .push(slider(cr2, c2, move |v| C::new(c1, v, c3))) + .push(slider(cr3, c3, move |v| C::new(c1, c2, v))) + .push(text(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() + ) + } +} -- cgit