From f177be9a39f5700e783dc7f3ebef8d81ba9d853d Mon Sep 17 00:00:00 2001 From: PixelDots Date: Fri, 31 May 2024 17:34:08 -0500 Subject: [PATCH 1/4] Add HSV and OKLCH Graphs --- Cargo.toml | 1 + src/app.rs | 9 +-- src/colorspace/hsv.rs | 20 +++++- src/colorspace/oklch.rs | 32 ++++++++- src/main.rs | 1 + src/shaders/hsv.rs | 86 ++++++++++++++++++++++++ src/shaders/hsv.wgsl | 48 ++++++++++++++ src/shaders/mod.rs | 142 ++++++++++++++++++++++++++++++++++++++++ src/shaders/oklch.rs | 88 +++++++++++++++++++++++++ src/shaders/oklch.wgsl | 67 +++++++++++++++++++ src/shaders/vertex.wgsl | 11 ++++ 11 files changed, 495 insertions(+), 10 deletions(-) create mode 100644 src/shaders/hsv.rs create mode 100644 src/shaders/hsv.wgsl create mode 100644 src/shaders/mod.rs create mode 100644 src/shaders/oklch.rs create mode 100644 src/shaders/oklch.wgsl create mode 100644 src/shaders/vertex.wgsl diff --git a/Cargo.toml b/Cargo.toml index 0f798d8..14f8805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" license = "GPL-3.0" [dependencies] +bytemuck = { version = "1.16.0", features = ["derive"] } i18n-embed-fl = "0.8" log = "0.4.21" once_cell = "1.19.0" diff --git a/src/app.rs b/src/app.rs index 0d7ff62..8a9f9f7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -222,14 +222,11 @@ impl Application for ColorPicker { contents = contents.push(widget::container( widget::column::with_capacity(2) .push(sidebar) - .push( - content - .map(move |message| Message::ColorSpace { index, message }) - .apply(widget::scrollable), - ) + .push(content.map(move |message| Message::ColorSpace { index, message })) .spacing(10.0) .padding(10.0) - .width(300.0), + .width(300.0) + .apply(widget::scrollable), )); } diff --git a/src/colorspace/hsv.rs b/src/colorspace/hsv.rs index d96c433..9c61c02 100644 --- a/src/colorspace/hsv.rs +++ b/src/colorspace/hsv.rs @@ -5,7 +5,9 @@ use cosmic::{ widget, }; -use crate::{colorspace::ColorSpaceMessage as Message, fl, widgets::color_slider}; +use crate::{ + colorspace::ColorSpaceMessage as Message, fl, shaders::hsv as shader, widgets::color_slider, +}; const COLOR_STOPS_HUE: [ColorStop; 7] = [ ColorStop { @@ -163,6 +165,21 @@ impl Hsv { .push(widget::container(red).style(cosmic::style::Container::Card)) .push(widget::container(green).style(cosmic::style::Container::Card)) .push(widget::container(blue).style(cosmic::style::Container::Card)) + .push( + widget::container( + widget::container( + cosmic::iced_widget::shader(shader::ColorGraph { + hue: self.values[0], + saturation: self.values[1], + value: self.values[2], + }) + .width(100) + .height(100), + ) + .padding(10.0), + ) + .style(cosmic::style::Container::Card), + ) .spacing(10.0); content.into() @@ -234,6 +251,7 @@ fn rgb_to_hsv(r: f32, g: f32, b: f32) -> [f32; 3] { [h, s, x_max] } +// ---- Tests ---- #[cfg(test)] mod test { use super::{hsv_to_rgb, rgb_to_hsv}; diff --git a/src/colorspace/oklch.rs b/src/colorspace/oklch.rs index 37816bf..fe2cd8f 100644 --- a/src/colorspace/oklch.rs +++ b/src/colorspace/oklch.rs @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ - iced::{gradient::ColorStop, Alignment, Color}, + iced::{gradient::ColorStop, Alignment, Color, Length}, widget, }; -use crate::{colorspace::ColorSpaceMessage as Message, fl, widgets::color_slider}; +use crate::{ + colorspace::ColorSpaceMessage as Message, fl, shaders::oklch as shader, widgets::color_slider, +}; const COLOR_STOPS_LIGHTNESS: [ColorStop; 2] = [ ColorStop { @@ -112,6 +114,14 @@ impl Oklch { .align_items(Alignment::Center) .spacing(10.0), ) + .push( + cosmic::iced_widget::shader(shader::ColorGraph::<0> { + lightness: self.values[0], + chroma: self.values[1], + hue: self.values[2], + }) + .width(Length::Fill), + ) .push(color_slider( 0f32..=1.0f32, values[0], @@ -131,8 +141,16 @@ impl Oklch { .align_items(Alignment::Center) .spacing(10.0), ) + .push( + cosmic::iced_widget::shader(shader::ColorGraph::<1> { + lightness: self.values[0], + chroma: self.values[1], + hue: self.values[2], + }) + .width(Length::Fill), + ) .push(color_slider( - 0f32..=0.5f32, + 0f32..=0.37f32, values[1], |value| Message::ChangeValue { index: 1, value }, &COLOR_STOPS_CHROMA, @@ -150,6 +168,14 @@ impl Oklch { .align_items(Alignment::Center) .spacing(10.0), ) + .push( + cosmic::iced_widget::shader(shader::ColorGraph::<2> { + lightness: self.values[0], + chroma: self.values[1], + hue: self.values[2], + }) + .width(Length::Fill), + ) .push(color_slider( 0f32..=360f32, values[2], diff --git a/src/main.rs b/src/main.rs index 19cbddf..83c9759 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use app::ColorPicker; mod app; mod colorspace; mod core; +mod shaders; mod widgets; fn main() -> cosmic::iced::Result { diff --git a/src/shaders/hsv.rs b/src/shaders/hsv.rs new file mode 100644 index 0000000..9bdae2e --- /dev/null +++ b/src/shaders/hsv.rs @@ -0,0 +1,86 @@ +use cosmic::iced_widget::shader::{self}; + +use crate::shaders::ShaderPipeline; + +// ---- Shader ---- +pub struct ColorGraph { + pub hue: f32, + pub saturation: f32, + pub value: f32, +} + +impl shader::Program for ColorGraph { + type State = (); + type Primitive = Primitive; + + fn draw( + &self, + state: &Self::State, + cursor: cosmic::iced_core::mouse::Cursor, + bounds: cosmic::iced::Rectangle, + ) -> Self::Primitive { + Primitive::new(self.hue, self.saturation, self.value) + } +} + +#[derive(Debug)] +pub struct Primitive { + uniforms: Uniforms, +} + +impl Primitive { + pub fn new(hue: f32, saturation: f32, value: f32) -> Self { + Self { + uniforms: Uniforms { + hue, + saturation, + value, + }, + } + } +} + +impl shader::Primitive for Primitive { + fn prepare( + &self, + format: shader::wgpu::TextureFormat, + device: &shader::wgpu::Device, + queue: &shader::wgpu::Queue, + bounds: cosmic::iced::Rectangle, + target_size: cosmic::iced::Size, + scale_factor: f32, + storage: &mut shader::Storage, + ) { + if !storage.has::>() { + storage.store(ShaderPipeline::::new( + device, + queue, + format, + include_str!("hsv.wgsl"), + )); + } + + let pipeline = storage.get_mut::>().unwrap(); + pipeline.write(queue, &self.uniforms); + } + + fn render( + &self, + storage: &shader::Storage, + target: &shader::wgpu::TextureView, + target_size: cosmic::iced::Size, + viewport: cosmic::iced::Rectangle, + encoder: &mut shader::wgpu::CommandEncoder, + ) { + let pipeline = storage.get::>().unwrap(); + pipeline.render(target, encoder, viewport); + } +} + +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Uniforms { + hue: f32, + saturation: f32, + value: f32, +} diff --git a/src/shaders/hsv.wgsl b/src/shaders/hsv.wgsl new file mode 100644 index 0000000..7c8d359 --- /dev/null +++ b/src/shaders/hsv.wgsl @@ -0,0 +1,48 @@ +struct HSV { + hue: f32, + saturation: f32, + value: f32, +} + +@group(0) @binding(0) var hsv: HSV; + +@fragment +fn fs_main( + @builtin(position) _clip_pos: vec4, + @location(0) uv: vec2, +) -> @location(0) vec4 { + // uv.x = saturation + // uv.y = value + + // HSV to RGB + let c = uv.y * uv.x; + let h_ = hsv.hue / 60.0; + let x = c * (1.0 - abs(h_ % 2.0 - 1.0)); + + var r1 = 0.0; + var g1 = 0.0; + var b1 = 0.0; + if 0.0 <= h_ && h_ < 1.0 { + r1 = c; + g1 = x; + } else if 1.0 <= h_ && h_ < 2.0 { + r1 = x; + g1 = c; + } else if 2.0 <= h_ && h_ < 3.0 { + g1 = c; + b1 = x; + } else if 3.0 <= h_ && h_ < 4.0 { + g1 = x; + b1 = c; + } else if 4.0 <= h_ && h_ < 5.0 { + r1 = x; + b1 = c; + } else { + r1 = c; + b1 = x; + } + + let m = uv.y - c; + let color = vec4(r1 + m, g1 + m, b1 + m, 1.0); + return color; +} diff --git a/src/shaders/mod.rs b/src/shaders/mod.rs new file mode 100644 index 0000000..d1e1fb3 --- /dev/null +++ b/src/shaders/mod.rs @@ -0,0 +1,142 @@ +pub mod hsv; +pub mod oklch; + +use std::marker::PhantomData; + +use cosmic::{ + iced::Rectangle, + iced_widget::shader::wgpu::{self}, +}; + +pub struct ShaderPipeline { + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + data: wgpu::Buffer, + phantom: PhantomData, +} + +impl ShaderPipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + shader: &str, + ) -> Self { + let data_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("shader data buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("shader uniform bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("shader uniform bind group"), + layout: &uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: data_buffer.as_entire_binding(), + }], + }); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("data pipeline layout"), + bind_group_layouts: &[&uniform_bind_group_layout], + push_constant_ranges: &[], + }); + + let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("graph vertex shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "vertex.wgsl" + ))), + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("graph shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader)), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("graph pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &vertex_shader, + entry_point: "vs_main", + buffers: &[], + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + Self { + pipeline, + bind_group: uniform_bind_group, + data: data_buffer, + phantom: PhantomData::default(), + } + } + + pub fn write(&self, queue: &wgpu::Queue, data: &T) { + queue.write_buffer(&self.data, 0, bytemuck::bytes_of(data)); + } + + pub fn render( + &self, + target: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + viewport: Rectangle, + ) { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("shader.pipeline.pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.pipeline); + pass.set_viewport( + viewport.x as f32, + viewport.y as f32, + viewport.width as f32, + viewport.height as f32, + 0.0, + 1.0, + ); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..3, 0..1); + } +} diff --git a/src/shaders/oklch.rs b/src/shaders/oklch.rs new file mode 100644 index 0000000..bb51335 --- /dev/null +++ b/src/shaders/oklch.rs @@ -0,0 +1,88 @@ +use cosmic::iced_widget::shader::{self}; + +use crate::shaders::ShaderPipeline; + +// ---- Shader ---- +pub struct ColorGraph { + pub lightness: f32, + pub chroma: f32, + pub hue: f32, +} + +impl shader::Program for ColorGraph { + type State = (); + type Primitive = Primitive; + + fn draw( + &self, + state: &Self::State, + cursor: cosmic::iced_core::mouse::Cursor, + bounds: cosmic::iced::Rectangle, + ) -> Self::Primitive { + Primitive::::new(self.lightness, self.chroma, self.hue) + } +} + +#[derive(Debug)] +pub struct Primitive { + uniforms: Uniforms, +} + +impl Primitive { + pub fn new(lightness: f32, chroma: f32, hue: f32) -> Self { + Self { + uniforms: Uniforms { + lightness, + chroma, + hue, + mode: M, + }, + } + } +} + +impl shader::Primitive for Primitive { + fn prepare( + &self, + format: shader::wgpu::TextureFormat, + device: &shader::wgpu::Device, + queue: &shader::wgpu::Queue, + bounds: cosmic::iced::Rectangle, + target_size: cosmic::iced::Size, + scale_factor: f32, + storage: &mut shader::Storage, + ) { + if !storage.has::>() { + storage.store(ShaderPipeline::::new( + device, + queue, + format, + include_str!("oklch.wgsl"), + )); + } + + let pipeline = storage.get_mut::>().unwrap(); + pipeline.write(queue, &self.uniforms); + } + + fn render( + &self, + storage: &shader::Storage, + target: &shader::wgpu::TextureView, + target_size: cosmic::iced::Size, + viewport: cosmic::iced::Rectangle, + encoder: &mut shader::wgpu::CommandEncoder, + ) { + let pipeline = storage.get::>().unwrap(); + pipeline.render(target, encoder, viewport); + } +} + +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Uniforms { + lightness: f32, + chroma: f32, + hue: f32, + mode: u32, +} diff --git a/src/shaders/oklch.wgsl b/src/shaders/oklch.wgsl new file mode 100644 index 0000000..a3b137d --- /dev/null +++ b/src/shaders/oklch.wgsl @@ -0,0 +1,67 @@ +struct OKLCH { + lightness: f32, + chroma: f32, + hue: f32, + mode: u32, +} + +const MODE_LIGHTNESS = 0u; +const MODE_CHROMA = 1u; +const MODE_HUE = 2u; + +@group(0) @binding(0) var oklch: OKLCH; + +@fragment +fn fs_main( + @builtin(position) _clip_pos: vec4, + @location(0) uv: vec2, +) -> @location(0) vec4 { + var rgb = vec3(0.0); + switch oklch.mode { + case MODE_LIGHTNESS: { + let chroma = uv.y * 0.37; + let lightness = uv.x; + + rgb = oklch_to_rgb(lightness, chroma, oklch.hue); + } + case MODE_CHROMA: { + let chroma = uv.y * 0.37; + let hue = uv.x * 360.0; + + rgb = oklch_to_rgb(oklch.lightness, chroma, hue); + } + case MODE_HUE: { + let lightness = uv.y; + let hue = uv.x * 360.0; + + rgb = oklch_to_rgb(lightness, oklch.chroma, hue); + } + default: {} + } + + var color = vec4(rgb, 1.0); + if max(color.x, max(color.y, color.z)) > 1.0 || min(color.x, min(color.y, color.z)) < 0.0 { + color.w = 0.0; + } + return color; +} + +fn oklch_to_rgb(okl: f32, okc: f32, okh: f32) -> vec3 { + let h = radians(okh); + let a = okc * cos(h); + let b = okc * sin(h); + + let l_ = okl + 0.3963377774 * a + 0.2158037573 * b; + let m_ = okl - 0.1055613458 * a - 0.0638541728 * b; + let s_ = okl - 0.0894841775 * a - 1.2914855480 * b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + return vec3( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, + ); +} diff --git a/src/shaders/vertex.wgsl b/src/shaders/vertex.wgsl new file mode 100644 index 0000000..6d39d07 --- /dev/null +++ b/src/shaders/vertex.wgsl @@ -0,0 +1,11 @@ +struct Output { + @builtin(position) clip_pos: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> Output { + let uv = vec2(vec2((vertex_index << 1) & 2, vertex_index & 2)); + let position = vec4(uv * 2 - 1, 0, 1); + return Output(position, uv); +} From 85f3ab3ecdff36875367acb6b8976a07f18636b8 Mon Sep 17 00:00:00 2001 From: PixelDots Date: Thu, 6 Jun 2024 10:08:45 -0500 Subject: [PATCH 2/4] Added `OKLAB` graph Use alpha `0.1` when rgb value is outside SRGB (`OKLCH` graph) Added `show_graphs` variable --- src/app.rs | 32 ++++++++++++--- src/colorspace/cmyk.rs | 2 +- src/colorspace/hsv.rs | 13 +++--- src/colorspace/oklab.rs | 41 ++++++++++++++++--- src/colorspace/oklch.rs | 59 ++++++++++++++------------- src/colorspace/rgb.rs | 2 +- src/shaders/mod.rs | 1 + src/shaders/oklab.rs | 88 +++++++++++++++++++++++++++++++++++++++++ src/shaders/oklab.wgsl | 64 ++++++++++++++++++++++++++++++ src/shaders/oklch.wgsl | 2 +- 10 files changed, 257 insertions(+), 47 deletions(-) create mode 100644 src/shaders/oklab.rs create mode 100644 src/shaders/oklab.wgsl diff --git a/src/app.rs b/src/app.rs index 8a9f9f7..d372648 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,6 +18,7 @@ use log::info; pub struct ColorPicker { pub spaces: Vec, last_edited: usize, + show_graphs: bool, colorspace_combo: widget::combo_box::State, core: Core, @@ -94,6 +95,7 @@ impl Application for ColorPicker { let mut app = ColorPicker { spaces: vec![ColorSpace::default()], last_edited: 0, + show_graphs: true, colorspace_combo: widget::combo_box::State::new(vec![ ColorSpaceCombo::Rgb, @@ -163,11 +165,31 @@ impl Application for ColorPicker { for (colorspace, index) in self.spaces.iter().zip(0..) { let (rgb, content, combo_selection) = match colorspace { - ColorSpace::Rgb(rgb) => (rgb.to_rgb(), rgb.view(), ColorSpaceCombo::Rgb), - ColorSpace::Hsv(hsv) => (hsv.to_rgb(), hsv.view(), ColorSpaceCombo::Hsv), - ColorSpace::Oklab(oklab) => (oklab.to_rgb(), oklab.view(), ColorSpaceCombo::Oklab), - ColorSpace::Oklch(oklch) => (oklch.to_rgb(), oklch.view(), ColorSpaceCombo::Oklch), - ColorSpace::Cmyk(cmyk) => (cmyk.to_rgb(), cmyk.view(), ColorSpaceCombo::Cmyk), + ColorSpace::Rgb(rgb) => ( + rgb.to_rgb(), + rgb.view(self.show_graphs), + ColorSpaceCombo::Rgb, + ), + ColorSpace::Hsv(hsv) => ( + hsv.to_rgb(), + hsv.view(self.show_graphs), + ColorSpaceCombo::Hsv, + ), + ColorSpace::Oklab(oklab) => ( + oklab.to_rgb(), + oklab.view(self.show_graphs), + ColorSpaceCombo::Oklab, + ), + ColorSpace::Oklch(oklch) => ( + oklch.to_rgb(), + oklch.view(self.show_graphs), + ColorSpaceCombo::Oklch, + ), + ColorSpace::Cmyk(cmyk) => ( + cmyk.to_rgb(), + cmyk.view(self.show_graphs), + ColorSpaceCombo::Cmyk, + ), }; let min_rgb = rgb[0].min(rgb[1]).min(rgb[2]).min(0.0); diff --git a/src/colorspace/cmyk.rs b/src/colorspace/cmyk.rs index 53cd58b..fb04c13 100644 --- a/src/colorspace/cmyk.rs +++ b/src/colorspace/cmyk.rs @@ -109,7 +109,7 @@ impl Cmyk { self.strings[index] = string; } - pub fn view<'a>(&self) -> cosmic::Element<'a, Message> { + pub fn view<'a>(&self, _show_graphs: bool) -> cosmic::Element<'a, Message> { let values = &self.values; let strings = &self.strings; diff --git a/src/colorspace/hsv.rs b/src/colorspace/hsv.rs index 9c61c02..55402ca 100644 --- a/src/colorspace/hsv.rs +++ b/src/colorspace/hsv.rs @@ -99,7 +99,7 @@ impl Hsv { self.strings[index] = string; } - pub fn view<'a>(&self) -> cosmic::Element<'a, Message> { + pub fn view<'a>(&self, show_graphs: bool) -> cosmic::Element<'a, Message> { let values = &self.values; let strings = &self.strings; @@ -161,11 +161,14 @@ impl Hsv { .spacing(10.0) .padding(10.0); - let content = widget::column::with_capacity(3) + let mut content = widget::column::with_capacity(3) .push(widget::container(red).style(cosmic::style::Container::Card)) .push(widget::container(green).style(cosmic::style::Container::Card)) .push(widget::container(blue).style(cosmic::style::Container::Card)) - .push( + .spacing(10.0); + + if show_graphs { + content = content.push( widget::container( widget::container( cosmic::iced_widget::shader(shader::ColorGraph { @@ -179,8 +182,8 @@ impl Hsv { .padding(10.0), ) .style(cosmic::style::Container::Card), - ) - .spacing(10.0); + ); + } content.into() } diff --git a/src/colorspace/oklab.rs b/src/colorspace/oklab.rs index 6d05527..8a896f3 100644 --- a/src/colorspace/oklab.rs +++ b/src/colorspace/oklab.rs @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ - iced::{gradient::ColorStop, Alignment, Color}, + iced::{gradient::ColorStop, Alignment, Color, Length}, widget, }; -use crate::{colorspace::ColorSpaceMessage as Message, fl, widgets::color_slider}; +use crate::{ + colorspace::ColorSpaceMessage as Message, fl, shaders::oklab as shader, widgets::color_slider, +}; const COLOR_STOPS_LIGHTNESS: [ColorStop; 2] = [ ColorStop { @@ -77,11 +79,11 @@ impl Oklab { self.strings[index] = string; } - pub fn view<'a>(&self) -> cosmic::Element<'a, Message> { + pub fn view<'a>(&self, show_graphs: bool) -> cosmic::Element<'a, Message> { let values = &self.values; let strings = &self.strings; - let lightness = widget::column::with_capacity(3) + let mut lightness = widget::column::with_capacity(3) .push( widget::row::with_capacity(2) .push(widget::text(fl!("lightness")).size(20.0)) @@ -100,7 +102,7 @@ impl Oklab { )) .spacing(10.0) .padding(10.0); - let green_red = widget::column::with_capacity(3) + let mut green_red = widget::column::with_capacity(3) .push( widget::row::with_capacity(2) .push(widget::text(fl!("green-red")).size(20.0)) @@ -119,7 +121,7 @@ impl Oklab { )) .spacing(10.0) .padding(10.0); - let blue_yellow = widget::column::with_capacity(3) + let mut blue_yellow = widget::column::with_capacity(3) .push( widget::row::with_capacity(2) .push(widget::text(fl!("blue-yellow")).size(20.0)) @@ -139,6 +141,33 @@ impl Oklab { .spacing(10.0) .padding(10.0); + if show_graphs { + lightness = lightness.push( + cosmic::iced_widget::shader(shader::ColorGraph::<0> { + lightness: self.values[0], + green_red: self.values[1], + blue_yellow: self.values[2], + }) + .width(Length::Fill), + ); + green_red = green_red.push( + cosmic::iced_widget::shader(shader::ColorGraph::<1> { + lightness: self.values[0], + green_red: self.values[1], + blue_yellow: self.values[2], + }) + .width(Length::Fill), + ); + blue_yellow = blue_yellow.push( + cosmic::iced_widget::shader(shader::ColorGraph::<2> { + lightness: self.values[0], + green_red: self.values[1], + blue_yellow: self.values[2], + }) + .width(Length::Fill), + ); + } + let content = widget::column::with_capacity(3) .push(widget::container(lightness).style(cosmic::style::Container::Card)) .push(widget::container(green_red).style(cosmic::style::Container::Card)) diff --git a/src/colorspace/oklch.rs b/src/colorspace/oklch.rs index fe2cd8f..025ef0e 100644 --- a/src/colorspace/oklch.rs +++ b/src/colorspace/oklch.rs @@ -99,11 +99,11 @@ impl Oklch { self.strings[index] = string; } - pub fn view<'a>(&self) -> cosmic::Element<'a, Message> { + pub fn view<'a>(&self, show_graphs: bool) -> cosmic::Element<'a, Message> { let values = &self.values; let strings = &self.strings; - let lightness = widget::column::with_capacity(3) + let mut lightness = widget::column::with_capacity(3) .push( widget::row::with_capacity(2) .push(widget::text(fl!("lightness")).size(20.0)) @@ -114,14 +114,6 @@ impl Oklch { .align_items(Alignment::Center) .spacing(10.0), ) - .push( - cosmic::iced_widget::shader(shader::ColorGraph::<0> { - lightness: self.values[0], - chroma: self.values[1], - hue: self.values[2], - }) - .width(Length::Fill), - ) .push(color_slider( 0f32..=1.0f32, values[0], @@ -130,7 +122,7 @@ impl Oklch { )) .spacing(10.0) .padding(10.0); - let chroma = widget::column::with_capacity(3) + let mut chroma = widget::column::with_capacity(3) .push( widget::row::with_capacity(2) .push(widget::text(fl!("chroma")).size(20.0)) @@ -141,14 +133,6 @@ impl Oklch { .align_items(Alignment::Center) .spacing(10.0), ) - .push( - cosmic::iced_widget::shader(shader::ColorGraph::<1> { - lightness: self.values[0], - chroma: self.values[1], - hue: self.values[2], - }) - .width(Length::Fill), - ) .push(color_slider( 0f32..=0.37f32, values[1], @@ -157,7 +141,7 @@ impl Oklch { )) .spacing(10.0) .padding(10.0); - let hue = widget::column::with_capacity(3) + let mut hue = widget::column::with_capacity(3) .push( widget::row::with_capacity(2) .push(widget::text(fl!("hue")).size(20.0)) @@ -168,14 +152,6 @@ impl Oklch { .align_items(Alignment::Center) .spacing(10.0), ) - .push( - cosmic::iced_widget::shader(shader::ColorGraph::<2> { - lightness: self.values[0], - chroma: self.values[1], - hue: self.values[2], - }) - .width(Length::Fill), - ) .push(color_slider( 0f32..=360f32, values[2], @@ -185,6 +161,33 @@ impl Oklch { .spacing(10.0) .padding(10.0); + if show_graphs { + lightness = lightness.push( + cosmic::iced_widget::shader(shader::ColorGraph::<0> { + lightness: self.values[0], + chroma: self.values[1], + hue: self.values[2], + }) + .width(Length::Fill), + ); + chroma = chroma.push( + cosmic::iced_widget::shader(shader::ColorGraph::<1> { + lightness: self.values[0], + chroma: self.values[1], + hue: self.values[2], + }) + .width(Length::Fill), + ); + hue = hue.push( + cosmic::iced_widget::shader(shader::ColorGraph::<2> { + lightness: self.values[0], + chroma: self.values[1], + hue: self.values[2], + }) + .width(Length::Fill), + ); + } + let content = widget::column::with_capacity(3) .push(widget::container(lightness).style(cosmic::style::Container::Card)) .push(widget::container(chroma).style(cosmic::style::Container::Card)) diff --git a/src/colorspace/rgb.rs b/src/colorspace/rgb.rs index ac6c9fc..1e5cb5f 100644 --- a/src/colorspace/rgb.rs +++ b/src/colorspace/rgb.rs @@ -84,7 +84,7 @@ impl Rgb { self.strings[index] = string; } - pub fn view<'a>(&self) -> cosmic::Element<'a, Message> { + pub fn view<'a>(&self, _show_graphs: bool) -> cosmic::Element<'a, Message> { let values = &self.values; let strings = &self.strings; diff --git a/src/shaders/mod.rs b/src/shaders/mod.rs index d1e1fb3..5d5b87b 100644 --- a/src/shaders/mod.rs +++ b/src/shaders/mod.rs @@ -1,4 +1,5 @@ pub mod hsv; +pub mod oklab; pub mod oklch; use std::marker::PhantomData; diff --git a/src/shaders/oklab.rs b/src/shaders/oklab.rs new file mode 100644 index 0000000..cedc921 --- /dev/null +++ b/src/shaders/oklab.rs @@ -0,0 +1,88 @@ +use cosmic::iced_widget::shader::{self}; + +use crate::shaders::ShaderPipeline; + +// ---- Shader ---- +pub struct ColorGraph { + pub lightness: f32, + pub green_red: f32, + pub blue_yellow: f32, +} + +impl shader::Program for ColorGraph { + type State = (); + type Primitive = Primitive; + + fn draw( + &self, + state: &Self::State, + cursor: cosmic::iced_core::mouse::Cursor, + bounds: cosmic::iced::Rectangle, + ) -> Self::Primitive { + Primitive::::new(self.lightness, self.green_red, self.blue_yellow) + } +} + +#[derive(Debug)] +pub struct Primitive { + uniforms: Uniforms, +} + +impl Primitive { + pub fn new(lightness: f32, green_red: f32, blue_yellow: f32) -> Self { + Self { + uniforms: Uniforms { + lightness, + green_red, + blue_yellow, + mode: M, + }, + } + } +} + +impl shader::Primitive for Primitive { + fn prepare( + &self, + format: shader::wgpu::TextureFormat, + device: &shader::wgpu::Device, + queue: &shader::wgpu::Queue, + bounds: cosmic::iced::Rectangle, + target_size: cosmic::iced::Size, + scale_factor: f32, + storage: &mut shader::Storage, + ) { + if !storage.has::>() { + storage.store(ShaderPipeline::::new( + device, + queue, + format, + include_str!("oklab.wgsl"), + )); + } + + let pipeline = storage.get_mut::>().unwrap(); + pipeline.write(queue, &self.uniforms); + } + + fn render( + &self, + storage: &shader::Storage, + target: &shader::wgpu::TextureView, + target_size: cosmic::iced::Size, + viewport: cosmic::iced::Rectangle, + encoder: &mut shader::wgpu::CommandEncoder, + ) { + let pipeline = storage.get::>().unwrap(); + pipeline.render(target, encoder, viewport); + } +} + +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Uniforms { + lightness: f32, + green_red: f32, + blue_yellow: f32, + mode: u32, +} diff --git a/src/shaders/oklab.wgsl b/src/shaders/oklab.wgsl new file mode 100644 index 0000000..07f3b1e --- /dev/null +++ b/src/shaders/oklab.wgsl @@ -0,0 +1,64 @@ +struct OKLAB { + lightness: f32, + green_red: f32, + blue_yellow: f32, + mode: u32, +} + +const MODE_LIGHTNESS = 0u; +const MODE_GREEN_RED = 1u; +const MODE_BLUE_YELLOW = 2u; + +@group(0) @binding(0) var oklab: OKLAB; + +@fragment +fn fs_main( + @builtin(position) _clip_pos: vec4, + @location(0) uv: vec2, +) -> @location(0) vec4 { + var rgb = vec3(0.0); + switch oklab.mode { + case MODE_LIGHTNESS: { + let green_red = uv.y - 0.5; + let lightness = uv.x; + + rgb = oklab_to_rgb(lightness, green_red, oklab.blue_yellow); + + } + case MODE_GREEN_RED: { + let green_red = uv.y - 0.5; + let blue_yellow = uv.x - 0.5; + + rgb = oklab_to_rgb(oklab.lightness, green_red, blue_yellow); + } + case MODE_BLUE_YELLOW: { + let lightness = uv.y; + let blue_yellow = uv.x - 0.5; + + rgb = oklab_to_rgb(lightness, oklab.green_red, blue_yellow); + } + default: {} + } + + var color = vec4(rgb, 1.0); + if max(color.x, max(color.y, color.z)) > 1.0 || min(color.x, min(color.y, color.z)) < 0.0 { + color.w = 0.1; + } + return color; +} + +fn oklab_to_rgb(okl: f32, a: f32, b: f32) -> vec3 { + let l_ = okl + 0.3963377774 * a + 0.2158037573 * b; + let m_ = okl - 0.1055613458 * a - 0.0638541728 * b; + let s_ = okl - 0.0894841775 * a - 1.2914855480 * b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + return vec3( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, + ); +} diff --git a/src/shaders/oklch.wgsl b/src/shaders/oklch.wgsl index a3b137d..8b81208 100644 --- a/src/shaders/oklch.wgsl +++ b/src/shaders/oklch.wgsl @@ -41,7 +41,7 @@ fn fs_main( var color = vec4(rgb, 1.0); if max(color.x, max(color.y, color.z)) > 1.0 || min(color.x, min(color.y, color.z)) < 0.0 { - color.w = 0.0; + color.w = 0.1; } return color; } From 7d02e442e366e9e4380e3e18197adc5a081b878e Mon Sep 17 00:00:00 2001 From: PixelDots Date: Thu, 6 Jun 2024 12:35:31 -0500 Subject: [PATCH 3/4] Added Graphs toggle Keybinding and CheckBox --- i18n/en/cosmic_color_picker.ftl | 1 + src/app.rs | 32 +++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/i18n/en/cosmic_color_picker.ftl b/i18n/en/cosmic_color_picker.ftl index 9b265ae..f9dee89 100644 --- a/i18n/en/cosmic_color_picker.ftl +++ b/i18n/en/cosmic_color_picker.ftl @@ -2,6 +2,7 @@ app-title = COSMIC Color Picker ## Menu view = View +graphs = Graphs menu-about = About ## About diff --git a/src/app.rs b/src/app.rs index d372648..e44cc51 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,6 +21,7 @@ pub struct ColorPicker { show_graphs: bool, colorspace_combo: widget::combo_box::State, + keybinds: HashMap, core: Core, } @@ -37,6 +38,7 @@ pub enum Message { AddSpace, RemoveSpace(usize), + ToggleGraphs, ToggleAboutPage, LaunchUrl(String), @@ -46,6 +48,7 @@ pub enum Message { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { + ToggleGraphs, About, } @@ -54,6 +57,7 @@ impl MenuAction for Action { fn message(&self) -> Message { match self { + Action::ToggleGraphs => Message::ToggleGraphs, Action::About => Message::ToggleAboutPage, } } @@ -80,8 +84,11 @@ impl Application for ColorPicker { vec![MenuBar::new(vec![menu::Tree::with_children( menu::root(fl!("view")), menu::items( - &HashMap::new(), - vec![menu::Item::Button(fl!("menu-about"), Action::About)], + &self.keybinds, + vec![ + menu::Item::CheckBox(fl!("graphs"), self.show_graphs, Action::ToggleGraphs), + menu::Item::Button(fl!("menu-about"), Action::About), + ], ), )]) .into()] @@ -92,10 +99,19 @@ impl Application for ColorPicker { } fn init(core: Core, _flags: Self::Flags) -> (Self, Command) { + let mut keybinds = HashMap::new(); + keybinds.insert( + menu::KeyBind { + modifiers: vec![menu::key_bind::Modifier::Ctrl], + key: Key::Character("g".into()), + }, + Action::ToggleGraphs, + ); + let mut app = ColorPicker { spaces: vec![ColorSpace::default()], last_edited: 0, - show_graphs: true, + show_graphs: false, colorspace_combo: widget::combo_box::State::new(vec![ ColorSpaceCombo::Rgb, @@ -104,6 +120,7 @@ impl Application for ColorPicker { ColorSpaceCombo::Oklch, ColorSpaceCombo::Cmyk, ]), + keybinds, core, }; @@ -137,6 +154,9 @@ impl Application for ColorPicker { self.spaces.remove(index); } + Message::ToggleGraphs => { + self.show_graphs = !self.show_graphs; + } Message::ToggleAboutPage => { self.core.window.show_context = !self.core.window.show_context; } @@ -151,6 +171,12 @@ impl Application for ColorPicker { return self.copy_to_clipboard(index); } Message::Key(key, modifiers) => { + for (key_bind, action) in self.keybinds.iter() { + if key_bind.matches(modifiers, &key) { + return self.update(action.message()); + } + } + if modifiers.control() && key == Key::Character("c".into()) { return self.copy_to_clipboard(self.last_edited); } From c7301eae35d374891ca5d7debfa504630fae18c5 Mon Sep 17 00:00:00 2001 From: PixelDots Date: Wed, 12 Jun 2024 12:28:49 -0500 Subject: [PATCH 4/4] Update `OKLAB` and `OKLCH` graphs Removed `libcosmic` `rev` key --- Cargo.toml | 1 - src/shaders/mod.rs | 2 +- src/shaders/oklab.wgsl | 10 ++++------ src/shaders/oklch.wgsl | 4 ++-- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14f8805..3d1ff22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ rust-embed = "8.3.0" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic.git" -rev = "2fc4184" default-features = false features = ["dbus-config", "tokio", "winit", "wgpu"] diff --git a/src/shaders/mod.rs b/src/shaders/mod.rs index 5d5b87b..b51547a 100644 --- a/src/shaders/mod.rs +++ b/src/shaders/mod.rs @@ -35,7 +35,7 @@ impl ShaderPipeline { label: Some("shader uniform bind group layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, diff --git a/src/shaders/oklab.wgsl b/src/shaders/oklab.wgsl index 07f3b1e..63deaed 100644 --- a/src/shaders/oklab.wgsl +++ b/src/shaders/oklab.wgsl @@ -19,17 +19,15 @@ fn fs_main( var rgb = vec3(0.0); switch oklab.mode { case MODE_LIGHTNESS: { - let green_red = uv.y - 0.5; let lightness = uv.x; - rgb = oklab_to_rgb(lightness, green_red, oklab.blue_yellow); - + rgb = oklab_to_rgb(lightness, oklab.green_red, oklab.blue_yellow); } case MODE_GREEN_RED: { - let green_red = uv.y - 0.5; - let blue_yellow = uv.x - 0.5; + let lightness = uv.y; + let green_red = uv.x - 0.5; - rgb = oklab_to_rgb(oklab.lightness, green_red, blue_yellow); + rgb = oklab_to_rgb(lightness, green_red, oklab.blue_yellow); } case MODE_BLUE_YELLOW: { let lightness = uv.y; diff --git a/src/shaders/oklch.wgsl b/src/shaders/oklch.wgsl index 8b81208..731177b 100644 --- a/src/shaders/oklch.wgsl +++ b/src/shaders/oklch.wgsl @@ -25,8 +25,8 @@ fn fs_main( rgb = oklch_to_rgb(lightness, chroma, oklch.hue); } case MODE_CHROMA: { - let chroma = uv.y * 0.37; - let hue = uv.x * 360.0; + let chroma = uv.x * 0.37; + let hue = uv.y * 360.0; rgb = oklch_to_rgb(oklch.lightness, chroma, hue); }