mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -05:00
feat: color_picker component
This commit is contained in:
parent
bfec51713c
commit
3c9a13aef2
3 changed files with 278 additions and 13 deletions
|
@ -36,6 +36,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
.melt-color-picker-popover__layer {
|
.melt-color-picker-popover__layer {
|
||||||
|
@ -55,6 +56,18 @@
|
||||||
background-image: linear-gradient(rgba(0, 0, 0, 0), rgb(0, 0, 0));
|
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 {
|
.melt-color-picker-slider {
|
||||||
height: 12px;
|
height: 12px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
|
|
160
src/color_picker/color.rs
Normal file
160
src/color_picker/color.rs
Normal file
|
@ -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<HSV> 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<RGBA> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,52 @@
|
||||||
|
mod color;
|
||||||
|
|
||||||
use crate::{mount_style, teleport::Teleport, utils::maybe_rw_signal::MaybeRwSignal};
|
use crate::{mount_style, teleport::Teleport, utils::maybe_rw_signal::MaybeRwSignal};
|
||||||
|
pub use color::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_dom::helpers::WindowListenerHandle;
|
use leptos_dom::helpers::WindowListenerHandle;
|
||||||
use wasm_bindgen::__rt::IntoJsResult;
|
use wasm_bindgen::__rt::IntoJsResult;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<String>) -> impl IntoView {
|
pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<RGBA>) -> impl IntoView {
|
||||||
mount_style("color-picker", include_str!("./color-picker.css"));
|
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 label = create_rw_signal(String::new());
|
||||||
let style = create_memo(move |_| {
|
let style = create_memo(move |_| {
|
||||||
let mut style = String::new();
|
let mut style = String::new();
|
||||||
|
|
||||||
value.with(|value| {
|
value.with(|value| {
|
||||||
if value.is_empty() {
|
let value = value.to_hex_string();
|
||||||
label.set("Invalid value".into());
|
style.push_str(&format!("background-color: {value};"));
|
||||||
return;
|
let (s, v) = sv.get_untracked();
|
||||||
|
if s < 0.5 && v > 0.5 {
|
||||||
|
style.push_str("color: #000;");
|
||||||
|
} else {
|
||||||
|
style.push_str("color: #fff;");
|
||||||
}
|
}
|
||||||
|
label.set(value);
|
||||||
style.push_str(&format!("background-color: {value}"));
|
|
||||||
label.set(value.clone());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
style
|
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 is_show_popover = create_rw_signal(false);
|
||||||
let trigger_ref = create_node_ref::<html::Div>();
|
let trigger_ref = create_node_ref::<html::Div>();
|
||||||
let popover_ref = create_node_ref::<html::Div>();
|
let popover_ref = create_node_ref::<html::Div>();
|
||||||
|
@ -59,7 +83,7 @@ pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<String>) -> impl
|
||||||
is_show_popover.set(false);
|
is_show_popover.set(false);
|
||||||
});
|
});
|
||||||
on_cleanup(move || timer.remove());
|
on_cleanup(move || timer.remove());
|
||||||
let hue = create_rw_signal(0);
|
|
||||||
view! {
|
view! {
|
||||||
<div class="melt-color-picker-trigger" on:click=show_popover ref=trigger_ref>
|
<div class="melt-color-picker-trigger" on:click=show_popover ref=trigger_ref>
|
||||||
<div class="melt-color-picker-trigger__content" style=move || style.get()>
|
<div class="melt-color-picker-trigger__content" style=move || style.get()>
|
||||||
|
@ -75,17 +99,85 @@ pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<String>) -> impl
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="melt-color-picker-popover__panel">
|
<Panel hue=hue.read_only() sv/>
|
||||||
<div class="melt-color-picker-popover__layer"></div>
|
|
||||||
<div class="melt-color-picker-popover__layer--shadowed"></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<HueSlider hue/>
|
<HueSlider hue/>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Panel(hue: ReadSignal<u16>, sv: RwSignal<(f64, f64)>) -> impl IntoView {
|
||||||
|
let panel_ref = create_node_ref::<html::Div>();
|
||||||
|
let mouse = store_value(Vec::<WindowListenerHandle>::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! {
|
||||||
|
<div class="melt-color-picker-popover__panel" ref=panel_ref>
|
||||||
|
<div
|
||||||
|
class="melt-color-picker-popover__layer"
|
||||||
|
style:background-image=move || {
|
||||||
|
format!("linear-gradient(90deg, white, hsl({}, 100%, 50%))", hue.get())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="melt-color-picker-popover__layer--shadowed"></div>
|
||||||
|
<div
|
||||||
|
class="melt-color-picker-popover__handle"
|
||||||
|
on:mousedown=on_mouse_down
|
||||||
|
style=move || {
|
||||||
|
format!(
|
||||||
|
"left: calc({}% - 6px); bottom: calc({}% - 6px)",
|
||||||
|
sv.get().0 * 100.0,
|
||||||
|
sv.get().1 * 100.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn HueSlider(hue: RwSignal<u16>) -> impl IntoView {
|
fn HueSlider(hue: RwSignal<u16>) -> impl IntoView {
|
||||||
let rail_ref = create_node_ref::<html::Div>();
|
let rail_ref = create_node_ref::<html::Div>();
|
||||||
|
|
Loading…
Add table
Reference in a new issue