From f177be9a39f5700e783dc7f3ebef8d81ba9d853d Mon Sep 17 00:00:00 2001 From: PixelDots Date: Fri, 31 May 2024 17:34:08 -0500 Subject: [PATCH] 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); +}