From 3c9a13aef2a57832f6bcff0d0937890f23335313 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Mon, 9 Oct 2023 23:30:19 +0800 Subject: [PATCH] feat: color_picker component --- src/color_picker/color-picker.css | 13 +++ src/color_picker/color.rs | 160 ++++++++++++++++++++++++++++++ src/color_picker/mod.rs | 118 +++++++++++++++++++--- 3 files changed, 278 insertions(+), 13 deletions(-) create mode 100644 src/color_picker/color.rs diff --git a/src/color_picker/color-picker.css b/src/color_picker/color-picker.css index f1922f1..ecfcb31 100644 --- a/src/color_picker/color-picker.css +++ b/src/color_picker/color-picker.css @@ -36,6 +36,7 @@ position: relative; height: 180px; margin-bottom: 8px; + cursor: crosshair; } .melt-color-picker-popover__layer { @@ -55,6 +56,18 @@ background-image: linear-gradient(rgba(0, 0, 0, 0), rgb(0, 0, 0)); } +.melt-color-picker-popover__handle { + position: absolute; + left: -6px; + bottom: -6px; + width: 12px; + height: 12px; + border-radius: 6px; + box-sizing: border-box; + border: 2px solid white; + box-shadow: 0 0 2px 0 rgba(0, 0, 0, .45); +} + .melt-color-picker-slider { height: 12px; padding-right: 12px; diff --git a/src/color_picker/color.rs b/src/color_picker/color.rs new file mode 100644 index 0000000..43b5a3d --- /dev/null +++ b/src/color_picker/color.rs @@ -0,0 +1,160 @@ +#[derive(Clone)] +pub struct RGBA { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +impl Default for RGBA { + fn default() -> Self { + Self { + red: Default::default(), + green: Default::default(), + blue: Default::default(), + alpha: u8::MAX, + } + } +} + +impl RGBA { + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { + red: r, + green: g, + blue: b, + alpha: a, + } + } + + pub fn new_rgb(r: u8, g: u8, b: u8) -> Self { + Self { + red: r, + green: g, + blue: b, + alpha: u8::MAX, + } + } + + pub fn to_hex_string(&self) -> String { + if self.alpha == u8::MAX { + format!("#{:02X}{:02X}{:02X}", self.red, self.green, self.blue) + } else { + format!( + "#{:02X}{:02X}{:02X}{:02X}", + self.red, self.green, self.blue, self.alpha + ) + } + } +} + +impl From for RGBA { + fn from(value: HSV) -> Self { + let HSV { + hue: h, + saturation: s, + value: v, + alpha, + } = value; + let h = f64::from(h); + + let c = v * s; + let x = c * (1.0 - f64::abs(((h / 60.0) % 2.0) - 1.0)); + let m = v - c; + + let (r, g, b) = if (0.0..60.0).contains(&h) { + (c, x, 0.0) + } else if (60.0..120.0).contains(&h) { + (x, c, 0.0) + } else if (120.0..180.0).contains(&h) { + (0.0, c, x) + } else if (180.0..240.0).contains(&h) { + (0.0, x, c) + } else if (240.0..300.0).contains(&h) { + (x, 0.0, c) + } else if (300.0..360.0).contains(&h) { + (c, 0.0, x) + } else { + (c, x, 0.0) + }; + + let (r, g, b) = ( + ((r + m) * 255.0) as u8, + ((g + m) * 255.0) as u8, + ((b + m) * 255.0) as u8, + ); + + RGBA::new(r, g, b, alpha) + } +} + +#[derive(Clone)] +pub struct HSV { + pub hue: u16, + pub saturation: f64, + pub value: f64, + pub alpha: u8, +} + +impl HSV { + pub fn new(hue: u16, saturation: f64, value: f64) -> Self { + Self { + hue, + saturation, + value, + alpha: u8::MAX, + } + } + + pub fn new_alpha(hue: u16, saturation: f64, value: f64, alpha: u8) -> Self { + Self { + hue, + saturation, + value, + alpha, + } + } +} + +impl From for HSV { + fn from(value: RGBA) -> Self { + let RGBA { + red: r, + green: g, + blue: b, + alpha, + } = value; + + let (r, g, b) = (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0); + + let c_max = f64::max(r, f64::max(g, b)); + let c_min = f64::min(r, f64::min(g, b)); + let delta = c_max - c_min; + + let hue = if delta == 0.0 { + 0.0 + } else if c_max == r { + 60.0 * (((g - b) / delta) % 6.0) + } else if c_max == g { + 60.0 * (((b - r) / delta) + 2.0) + } else if c_max == b { + 60.0 * (((r - g) / delta) + 4.0) + } else { + unreachable!() + }; + + let saturation = match c_max == 0.0 { + true => 0.0, + false => delta / c_max, + }; + + let value = c_max; + + HSV { + hue: hue.to_string().parse().unwrap(), + saturation, + value, + alpha, + } + } +} diff --git a/src/color_picker/mod.rs b/src/color_picker/mod.rs index 3b8dae5..a41836b 100644 --- a/src/color_picker/mod.rs +++ b/src/color_picker/mod.rs @@ -1,28 +1,52 @@ +mod color; + use crate::{mount_style, teleport::Teleport, utils::maybe_rw_signal::MaybeRwSignal}; +pub use color::*; use leptos::*; use leptos_dom::helpers::WindowListenerHandle; use wasm_bindgen::__rt::IntoJsResult; #[component] -pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal) -> impl IntoView { +pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal) -> impl IntoView { mount_style("color-picker", include_str!("./color-picker.css")); + let hue = create_rw_signal(0); + let sv = create_rw_signal((0.0, 0.0)); let label = create_rw_signal(String::new()); let style = create_memo(move |_| { let mut style = String::new(); value.with(|value| { - if value.is_empty() { - label.set("Invalid value".into()); - return; + let value = value.to_hex_string(); + style.push_str(&format!("background-color: {value};")); + let (s, v) = sv.get_untracked(); + if s < 0.5 && v > 0.5 { + style.push_str("color: #000;"); + } else { + style.push_str("color: #fff;"); } - - style.push_str(&format!("background-color: {value}")); - label.set(value.clone()); + label.set(value); }); style }); + create_effect(move |prev| { + let (s, v) = sv.get(); + let hue_value = hue.get(); + if prev.is_none() { + let HSV { + hue: h, + saturation: s, + value: v, + .. + } = value.get_untracked().into(); + hue.set(h); + sv.set((s, v)) + } else { + value.set(RGBA::from(HSV::new(hue_value, s, v))); + } + }); + let is_show_popover = create_rw_signal(false); let trigger_ref = create_node_ref::(); let popover_ref = create_node_ref::(); @@ -59,7 +83,7 @@ pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal) -> impl is_show_popover.set(false); }); on_cleanup(move || timer.remove()); - let hue = create_rw_signal(0); + view! {
@@ -75,17 +99,85 @@ pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal) -> impl } > -
-
-
-
-
+
} } +#[component] +fn Panel(hue: ReadSignal, sv: RwSignal<(f64, f64)>) -> impl IntoView { + let panel_ref = create_node_ref::(); + let mouse = store_value(Vec::::new()); + + let on_mouse_down = move |_| { + let on_mouse_move = window_event_listener(ev::mousemove, move |ev| { + if let Some(panel) = panel_ref.get_untracked() { + let rect = panel.get_bounding_client_rect(); + let ev_x = f64::from(ev.x()); + let ev_y = f64::from(ev.y()); + + let v = (rect.bottom() - ev_y) / rect.height(); + let s = (ev_x - rect.x()) / rect.width(); + + let v = if v > 1.0 { + 1.0 + } else if v < 0.0 { + 0.0 + } else { + v + }; + let s = if s > 1.0 { + 1.0 + } else if s < 0.0 { + 0.0 + } else { + s + }; + + sv.set((s, v)) + } + }); + let on_mouse_up = window_event_listener(ev::mouseup, move |_| { + mouse.update_value(|value| { + for handle in value.drain(..).into_iter() { + handle.remove(); + } + }); + }); + mouse.update_value(|value| { + value.push(on_mouse_move); + value.push(on_mouse_up); + }); + }; + + view! { +
+
+
+
+
+
+
+ } +} + #[component] fn HueSlider(hue: RwSignal) -> impl IntoView { let rail_ref = create_node_ref::();