feat: color_picker component

This commit is contained in:
luoxiao 2023-10-09 23:30:19 +08:00
parent bfec51713c
commit 3c9a13aef2
3 changed files with 278 additions and 13 deletions

View file

@ -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;

160
src/color_picker/color.rs Normal file
View 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,
}
}
}

View file

@ -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<String>) -> impl IntoView {
pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<RGBA>) -> 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::<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);
});
on_cleanup(move || timer.remove());
let hue = create_rw_signal(0);
view! {
<div class="melt-color-picker-trigger" on:click=show_popover ref=trigger_ref>
<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">
<div class="melt-color-picker-popover__layer"></div>
<div class="melt-color-picker-popover__layer--shadowed"></div>
<div></div>
</div>
<Panel hue=hue.read_only() sv/>
<HueSlider hue/>
</div>
</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]
fn HueSlider(hue: RwSignal<u16>) -> impl IntoView {
let rail_ref = create_node_ref::<html::Div>();