Add color picker answer (#13)
* Add color picker answer * set autocomplete=off for the inputs * add symbols to the regexes * tweak hsv and hsl regex * don't show wikipedia when searching 'color picker'
This commit is contained in:
parent
fdf8163fbb
commit
95f7c147f1
@ -1,3 +1,4 @@
|
|||||||
|
pub mod colorpicker;
|
||||||
pub mod dictionary;
|
pub mod dictionary;
|
||||||
pub mod fend;
|
pub mod fend;
|
||||||
pub mod ip;
|
pub mod ip;
|
||||||
|
383
src/engines/answer/colorpicker.rs
Normal file
383
src/engines/answer/colorpicker.rs
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
use maud::html;
|
||||||
|
|
||||||
|
use crate::engines::{EngineResponse, SearchQuery};
|
||||||
|
|
||||||
|
use super::regex;
|
||||||
|
|
||||||
|
pub fn request(query: &SearchQuery) -> EngineResponse {
|
||||||
|
let matched_colors = MatchedColorModel::new(&query.query);
|
||||||
|
|
||||||
|
let rgb;
|
||||||
|
let cmyk;
|
||||||
|
let hsv;
|
||||||
|
let hsl;
|
||||||
|
|
||||||
|
if let Some(rgb_) = matched_colors.rgb {
|
||||||
|
rgb = rgb_;
|
||||||
|
cmyk = rgb_to_cmyk(rgb);
|
||||||
|
hsv = rgb_to_hsv(rgb);
|
||||||
|
hsl = rgb_to_hsl(rgb);
|
||||||
|
} else if let Some(cmyk_) = matched_colors.cmyk {
|
||||||
|
cmyk = cmyk_;
|
||||||
|
rgb = cmyk_to_rgb(cmyk);
|
||||||
|
hsv = rgb_to_hsv(rgb);
|
||||||
|
hsl = rgb_to_hsl(rgb);
|
||||||
|
} else if let Some(hsv_) = matched_colors.hsv {
|
||||||
|
hsv = hsv_;
|
||||||
|
rgb = hsv_to_rgb(hsv);
|
||||||
|
cmyk = rgb_to_cmyk(rgb);
|
||||||
|
hsl = hsv_to_hsl(hsv);
|
||||||
|
} else if let Some(hsl_) = matched_colors.hsl {
|
||||||
|
hsl = hsl_;
|
||||||
|
rgb = hsv_to_rgb(hsl_to_hsv(hsl));
|
||||||
|
cmyk = rgb_to_cmyk(rgb);
|
||||||
|
hsv = rgb_to_hsv(rgb);
|
||||||
|
} else {
|
||||||
|
return EngineResponse::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (r, g, b) = rgb;
|
||||||
|
let (c, m, y, k) = cmyk;
|
||||||
|
let (hsv_h, hsv_s, hsv_v) = hsv;
|
||||||
|
let (hsl_h, hsl_s, hsl_l) = hsl;
|
||||||
|
|
||||||
|
let hex_str = format!(
|
||||||
|
"#{:02x}{:02x}{:02x}",
|
||||||
|
(r * 255.) as u8,
|
||||||
|
(g * 255.) as u8,
|
||||||
|
(b * 255.) as u8
|
||||||
|
);
|
||||||
|
let rgb_str = format!(
|
||||||
|
"{}, {}, {}",
|
||||||
|
(r * 255.) as u8,
|
||||||
|
(g * 255.) as u8,
|
||||||
|
(b * 255.) as u8
|
||||||
|
);
|
||||||
|
let cmyk_str = format!(
|
||||||
|
"{:.0}%, {:.0}%, {:.0}%, {:.0}%",
|
||||||
|
c * 100.,
|
||||||
|
m * 100.,
|
||||||
|
y * 100.,
|
||||||
|
k * 100.
|
||||||
|
);
|
||||||
|
let hsv_str = format!(
|
||||||
|
"{:.0}°, {:.0}%, {:.0}%",
|
||||||
|
hsv_h * 360.,
|
||||||
|
hsv_s * 100.,
|
||||||
|
hsv_v * 100.
|
||||||
|
);
|
||||||
|
let hsl_str = format!(
|
||||||
|
"{:.0}°, {:.0}%, {:.0}%",
|
||||||
|
hsl_h * 360.,
|
||||||
|
hsl_s * 100.,
|
||||||
|
hsl_l * 100.
|
||||||
|
);
|
||||||
|
|
||||||
|
let hue_picker_x = hsv_h * 100.;
|
||||||
|
let picker_x = hsv_s * 100.;
|
||||||
|
let picker_y = (1. - hsv_v) * 100.;
|
||||||
|
|
||||||
|
let hue_css_color = format!("hsl({}, 100%, 50%)", hsv_h * 360.);
|
||||||
|
|
||||||
|
// yes the design of this is absolutely nabbed from google's
|
||||||
|
EngineResponse::answer_html(html! {
|
||||||
|
div.answer-colorpicker {
|
||||||
|
div.answer-colorpicker-preview-container {
|
||||||
|
div.answer-colorpicker-preview style=(format!("background-color: {hex_str}")) {}
|
||||||
|
div.answer-colorpicker-canvas-container {
|
||||||
|
div.answer-colorpicker-picker-container {
|
||||||
|
div.answer-colorpicker-picker style=(format!("background-color: {hex_str}; left: {picker_x}%; top: {picker_y}%;")) {}
|
||||||
|
}
|
||||||
|
svg.answer-colorpicker-canvas {
|
||||||
|
defs {
|
||||||
|
linearGradient id="saturation" x1="0%" x2="100%" y1="0%" y2="0%" {
|
||||||
|
stop offset="0%" stop-color="#fff" {}
|
||||||
|
stop.answer-colorpicker-canvas-hue-svg offset="100%" stop-color=(hex_str) {}
|
||||||
|
}
|
||||||
|
linearGradient id="value" x1="0%" x2="0%" y1="0%" y2="100%" {
|
||||||
|
stop offset="0%" stop-color="#fff" {}
|
||||||
|
stop offset="100%" stop-color="#000" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the .1 fixes a bug that's present at least on firefox that makes the
|
||||||
|
// rightmost column of pixels look wrong
|
||||||
|
rect width="100.1%" height="100%" fill="url(#saturation)" {}
|
||||||
|
rect width="100.1%" height="100%" fill="url(#value)" style="mix-blend-mode: multiply" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.answer-colorpicker-slider-container {
|
||||||
|
div.answer-colorpicker-huepicker style=(format!("background-color: {hue_css_color}; left: {hue_picker_x}%")) {}
|
||||||
|
svg.answer-colorpicker-slider {
|
||||||
|
defs {
|
||||||
|
linearGradient id="hue" x1="0%" x2="100%" y1="0%" y2="0%" {
|
||||||
|
stop offset="0%" stop-color="#ff0000" {}
|
||||||
|
stop offset="16.666%" stop-color="#ffff00" {}
|
||||||
|
stop offset="33.333%" stop-color="#00ff00" {}
|
||||||
|
stop offset="50%" stop-color="#00ffff" {}
|
||||||
|
stop offset="66.666%" stop-color="#0000ff" {}
|
||||||
|
stop offset="83.333%" stop-color="#ff00ff" {}
|
||||||
|
stop offset="100%" stop-color="#ff0000" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rect width="100%" height="50%" y="25%" fill="url(#hue)" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.answer-colorpicker-hex-input-container {
|
||||||
|
label for="answer-colorpicker-hex-input" { "HEX" }
|
||||||
|
div.answer-colorpicker-input-container {
|
||||||
|
input #answer-colorpicker-hex-input type="text" autocomplete="off" value=(hex_str) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.answer-colorpicker-other-inputs {
|
||||||
|
div {
|
||||||
|
label for="answer-colorpicker-rgb-input" { "RGB" }
|
||||||
|
div.answer-colorpicker-input-container {
|
||||||
|
input #answer-colorpicker-rgb-input type="text" autocomplete="off" value=(rgb_str) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label for="answer-colorpicker-cmyk-input" { "CMYK" }
|
||||||
|
div.answer-colorpicker-input-container {
|
||||||
|
input #answer-colorpicker-cmyk-input type="text" autocomplete="off" value=(cmyk_str) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label for="answer-colorpicker-hsv-input" { "HSV" }
|
||||||
|
div.answer-colorpicker-input-container {
|
||||||
|
input #answer-colorpicker-hsv-input type="text" autocomplete="off" value=(hsv_str) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label for="answer-colorpicker-hsl-input" { "HSL" }
|
||||||
|
div.answer-colorpicker-input-container {
|
||||||
|
input #answer-colorpicker-hsl-input type="text" autocomplete="off" value=(hsl_str) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script src="/scripts/colorpicker.js" {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MatchedColorModel {
|
||||||
|
pub rgb: Option<(f64, f64, f64)>,
|
||||||
|
pub cmyk: Option<(f64, f64, f64, f64)>,
|
||||||
|
pub hsv: Option<(f64, f64, f64)>,
|
||||||
|
pub hsl: Option<(f64, f64, f64)>,
|
||||||
|
}
|
||||||
|
impl MatchedColorModel {
|
||||||
|
pub fn new(query: &str) -> Self {
|
||||||
|
let mut rgb = None;
|
||||||
|
let mut cmyk = None;
|
||||||
|
let mut hsv = None;
|
||||||
|
let mut hsl = None;
|
||||||
|
|
||||||
|
if regex!("^color ?picker$").is_match(&query.to_lowercase()) {
|
||||||
|
// default to red
|
||||||
|
rgb = Some((1., 0., 0.));
|
||||||
|
} else if let Some(caps) = regex!("^#?([0-9a-f]{6})$").captures(query) {
|
||||||
|
let hex_str = caps.get(1).unwrap().as_str();
|
||||||
|
let hex_num = u32::from_str_radix(hex_str, 16).unwrap();
|
||||||
|
let r = ((hex_num >> 16) & 0xff) as f64 / 255.;
|
||||||
|
let g = ((hex_num >> 8) & 0xff) as f64 / 255.;
|
||||||
|
let b = (hex_num & 0xff) as f64 / 255.;
|
||||||
|
|
||||||
|
let r = r.clamp(0., 1.);
|
||||||
|
let g = g.clamp(0., 1.);
|
||||||
|
let b = b.clamp(0., 1.);
|
||||||
|
|
||||||
|
rgb = Some((r, g, b));
|
||||||
|
} else if let Some(caps) =
|
||||||
|
regex!("^rgb\\((\\d{1,3}), ?(\\d{1,3}), ?(\\d{1,3})\\)$").captures(query)
|
||||||
|
{
|
||||||
|
let r = caps
|
||||||
|
.get(1)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 255.;
|
||||||
|
let g = caps
|
||||||
|
.get(2)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 255.;
|
||||||
|
let b = caps
|
||||||
|
.get(3)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 255.;
|
||||||
|
|
||||||
|
let r = r.clamp(0., 1.);
|
||||||
|
let g = g.clamp(0., 1.);
|
||||||
|
let b = b.clamp(0., 1.);
|
||||||
|
|
||||||
|
rgb = Some((r, g, b));
|
||||||
|
} else if let Some(caps) =
|
||||||
|
regex!("^cmyk\\((\\d{1,3})%, ?(\\d{1,3})%, ?(\\d{1,3})%, ?(\\d{1,3})%\\)$")
|
||||||
|
.captures(query)
|
||||||
|
{
|
||||||
|
let c = caps
|
||||||
|
.get(1)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
let m = caps
|
||||||
|
.get(2)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
let y = caps
|
||||||
|
.get(3)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
let k = caps
|
||||||
|
.get(4)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
|
||||||
|
let c = c.clamp(0., 1.);
|
||||||
|
let m = m.clamp(0., 1.);
|
||||||
|
let y = y.clamp(0., 1.);
|
||||||
|
let k = k.clamp(0., 1.);
|
||||||
|
|
||||||
|
cmyk = Some((c, m, y, k));
|
||||||
|
} else if let Some(caps) =
|
||||||
|
regex!("^hsv\\((\\d{1,3})(?:°|deg|), ?(\\d{1,3})%, ?(\\d{1,3})%\\)$").captures(query)
|
||||||
|
{
|
||||||
|
let h = caps
|
||||||
|
.get(1)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 360.;
|
||||||
|
let s = caps
|
||||||
|
.get(2)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
let v = caps
|
||||||
|
.get(3)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
|
||||||
|
let h = h.clamp(0., 1.);
|
||||||
|
let s = s.clamp(0., 1.);
|
||||||
|
let v = v.clamp(0., 1.);
|
||||||
|
|
||||||
|
hsv = Some((h, s, v));
|
||||||
|
} else if let Some(caps) =
|
||||||
|
regex!("^hsl\\((\\d{1,3})(?:°|deg|), ?(\\d{1,3})%, ?(\\d{1,3})%\\)$").captures(query)
|
||||||
|
{
|
||||||
|
let h = caps
|
||||||
|
.get(1)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 360.;
|
||||||
|
let s = caps
|
||||||
|
.get(2)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
let l = caps
|
||||||
|
.get(3)
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
/ 100.;
|
||||||
|
|
||||||
|
let h = h.clamp(0., 1.);
|
||||||
|
let s = s.clamp(0., 1.);
|
||||||
|
let l = l.clamp(0., 1.);
|
||||||
|
|
||||||
|
hsl = Some((h, s, l));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rgb,
|
||||||
|
cmyk,
|
||||||
|
hsv,
|
||||||
|
hsl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.rgb.is_none() && self.cmyk.is_none() && self.hsv.is_none() && self.hsl.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the code from colorpicker.js ported to rust
|
||||||
|
// see that file for credits
|
||||||
|
fn hsv_to_hsl((h, s, v): (f64, f64, f64)) -> (f64, f64, f64) {
|
||||||
|
let l = v - (v * s) / 2.;
|
||||||
|
let m = f64::min(l, 1. - l);
|
||||||
|
(h, if m != 0. { (v - l) / m } else { 0. }, l)
|
||||||
|
}
|
||||||
|
fn hsl_to_hsv((h, s, l): (f64, f64, f64)) -> (f64, f64, f64) {
|
||||||
|
let v = s * f64::min(l, 1. - l) + l;
|
||||||
|
(h, if v != 0. { 2. - (2. * l) / v } else { 0. }, v)
|
||||||
|
}
|
||||||
|
fn hsv_to_rgb((h, s, v): (f64, f64, f64)) -> (f64, f64, f64) {
|
||||||
|
let f = |n: f64| {
|
||||||
|
let k = (n + h * 6.) % 6.;
|
||||||
|
v - v * s * f64::max(f64::min(k, 4. - k), 0.)
|
||||||
|
};
|
||||||
|
(f(5.), f(3.), f(1.))
|
||||||
|
}
|
||||||
|
fn rgb_to_hsv((r, g, b): (f64, f64, f64)) -> (f64, f64, f64) {
|
||||||
|
let v = f64::max(r, f64::max(g, b));
|
||||||
|
let c = v - f64::min(r, f64::min(g, b));
|
||||||
|
let h = if c != 0. {
|
||||||
|
if v == r {
|
||||||
|
(g - b) / c
|
||||||
|
} else if v == g {
|
||||||
|
2. + (b - r) / c
|
||||||
|
} else {
|
||||||
|
4. + (r - g) / c
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
};
|
||||||
|
(
|
||||||
|
if h < 0. { h + 6. } else { h } / 6.,
|
||||||
|
if v != 0. { c / v } else { 0. },
|
||||||
|
v,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn rgb_to_hsl((r, g, b): (f64, f64, f64)) -> (f64, f64, f64) {
|
||||||
|
let v = f64::max(r, f64::max(g, b));
|
||||||
|
let c = v - f64::min(r, f64::min(g, b));
|
||||||
|
let f = 1. - f64::abs(v + v - c - 1.);
|
||||||
|
let h = if c != 0. {
|
||||||
|
if v == r {
|
||||||
|
(g - b) / c
|
||||||
|
} else if v == g {
|
||||||
|
2. + (b - r) / c
|
||||||
|
} else {
|
||||||
|
4. + (r - g) / c
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
};
|
||||||
|
(
|
||||||
|
if h < 0. { h + 6. } else { h } / 6.,
|
||||||
|
if f != 0. { c / f } else { 0. },
|
||||||
|
(v + v - c) / 2.,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn rgb_to_cmyk((r, g, b): (f64, f64, f64)) -> (f64, f64, f64, f64) {
|
||||||
|
let k = 1. - f64::max(r, f64::max(g, b));
|
||||||
|
if k == 1. {
|
||||||
|
return (0., 0., 0., 1.);
|
||||||
|
}
|
||||||
|
let c = (1. - r - k) / (1. - k);
|
||||||
|
let m = (1. - g - k) / (1. - k);
|
||||||
|
let y = (1. - b - k) / (1. - k);
|
||||||
|
(c, m, y, k)
|
||||||
|
}
|
||||||
|
fn cmyk_to_rgb((c, m, y, k): (f64, f64, f64, f64)) -> (f64, f64, f64) {
|
||||||
|
let r = (1. - c) * (1. - k);
|
||||||
|
let g = (1. - m) * (1. - k);
|
||||||
|
let b = (1. - y) * (1. - k);
|
||||||
|
(r, g, b)
|
||||||
|
}
|
@ -4,25 +4,43 @@ use maud::html;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::engines::{EngineResponse, CLIENT};
|
use crate::engines::{EngineResponse, RequestResponse, CLIENT};
|
||||||
|
|
||||||
pub fn request(query: &str) -> reqwest::RequestBuilder {
|
use super::colorpicker;
|
||||||
CLIENT.get(
|
|
||||||
Url::parse_with_params(
|
pub fn request(mut query: &str) -> RequestResponse {
|
||||||
"https://en.wikipedia.org/w/api.php",
|
if !colorpicker::MatchedColorModel::new(query).is_empty() {
|
||||||
&[
|
// "color picker" is a wikipedia article but we only want to show the
|
||||||
("format", "json"),
|
// actual color picker answer
|
||||||
("action", "query"),
|
return RequestResponse::None;
|
||||||
("prop", "extracts|pageimages"),
|
}
|
||||||
("exintro", ""),
|
|
||||||
("explaintext", ""),
|
// adding "wikipedia" to the start or end of your query is common when you
|
||||||
("redirects", "1"),
|
// want to get a wikipedia article
|
||||||
("exsentences", "2"),
|
if let Some(stripped_query) = query.strip_suffix(" wikipedia") {
|
||||||
("titles", query),
|
query = stripped_query
|
||||||
],
|
} else if let Some(stripped_query) = query.strip_prefix("wikipedia ") {
|
||||||
|
query = stripped_query
|
||||||
|
}
|
||||||
|
|
||||||
|
CLIENT
|
||||||
|
.get(
|
||||||
|
Url::parse_with_params(
|
||||||
|
"https://en.wikipedia.org/w/api.php",
|
||||||
|
&[
|
||||||
|
("format", "json"),
|
||||||
|
("action", "query"),
|
||||||
|
("prop", "extracts|pageimages"),
|
||||||
|
("exintro", ""),
|
||||||
|
("explaintext", ""),
|
||||||
|
("redirects", "1"),
|
||||||
|
("exsentences", "2"),
|
||||||
|
("titles", query),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.into()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -42,6 +42,7 @@ engines! {
|
|||||||
Fend = "fend",
|
Fend = "fend",
|
||||||
Ip = "ip",
|
Ip = "ip",
|
||||||
Notepad = "notepad",
|
Notepad = "notepad",
|
||||||
|
ColorPicker = "colorpicker",
|
||||||
Numbat = "numbat",
|
Numbat = "numbat",
|
||||||
Thesaurus = "thesaurus",
|
Thesaurus = "thesaurus",
|
||||||
Timezone = "timezone",
|
Timezone = "timezone",
|
||||||
@ -70,6 +71,7 @@ engine_requests! {
|
|||||||
Fend => answer::fend::request, None,
|
Fend => answer::fend::request, None,
|
||||||
Ip => answer::ip::request, None,
|
Ip => answer::ip::request, None,
|
||||||
Notepad => answer::notepad::request, None,
|
Notepad => answer::notepad::request, None,
|
||||||
|
ColorPicker => answer::colorpicker::request, None,
|
||||||
Numbat => answer::numbat::request, None,
|
Numbat => answer::numbat::request, None,
|
||||||
Thesaurus => answer::thesaurus::request, parse_response,
|
Thesaurus => answer::thesaurus::request, parse_response,
|
||||||
Timezone => answer::timezone::request, None,
|
Timezone => answer::timezone::request, None,
|
||||||
|
349
src/web/assets/scripts/colorpicker.js
Normal file
349
src/web/assets/scripts/colorpicker.js
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
// some guy on stackoverflow wrote a bunch of codegolfed color space conversion functions so i
|
||||||
|
// stole them for this (except the cmyk functions, those were stolen from other places)
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/54116681
|
||||||
|
function hsvToHsl(h, s, v) {
|
||||||
|
const l = v - (v * s) / 2;
|
||||||
|
const m = Math.min(l, 1 - l);
|
||||||
|
return [h, m ? (v - l) / m : 0, l];
|
||||||
|
}
|
||||||
|
function hslToHsv(h, s, l) {
|
||||||
|
let v = s * Math.min(l, 1 - l) + l;
|
||||||
|
return [h, v ? 2 - (2 * l) / v : 0, v];
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/54024653
|
||||||
|
function hsvToRgb(h, s, v) {
|
||||||
|
let f = (n, k = (n + h / 60) % 6) =>
|
||||||
|
v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
|
||||||
|
return [f(5), f(3), f(1)];
|
||||||
|
}
|
||||||
|
// https://stackoverflow.com/a/54070620
|
||||||
|
function rgbToHsv(r, g, b) {
|
||||||
|
let v = Math.max(r, g, b),
|
||||||
|
c = v - Math.min(r, g, b);
|
||||||
|
let h =
|
||||||
|
c && (v == r ? (g - b) / c : v == g ? 2 + (b - r) / c : 4 + (r - g) / c);
|
||||||
|
return [60 * (h < 0 ? h + 6 : h), v && c / v, v];
|
||||||
|
}
|
||||||
|
// https://stackoverflow.com/a/54071699
|
||||||
|
function rgbToHsl(r, g, b) {
|
||||||
|
let v = Math.max(r, g, b),
|
||||||
|
c = v - Math.min(r, g, b),
|
||||||
|
f = 1 - Math.abs(v + v - c - 1);
|
||||||
|
let h =
|
||||||
|
c && (v == r ? (g - b) / c : v == g ? 2 + (b - r) / c : 4 + (r - g) / c);
|
||||||
|
return [60 * (h < 0 ? h + 6 : h), f ? c / f : 0, (v + v - c) / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.codeproject.com/Articles/4488/XCmyk-CMYK-to-RGB-Calculator-with-source-code
|
||||||
|
function rgbToCmyk(r, g, b) {
|
||||||
|
const k = 1 - Math.max(r, g, b);
|
||||||
|
if (k === 1) return [0, 0, 0, 1];
|
||||||
|
const c = (1 - r - k) / (1 - k);
|
||||||
|
const m = (1 - g - k) / (1 - k);
|
||||||
|
const y = (1 - b - k) / (1 - k);
|
||||||
|
return [c, m, y, k];
|
||||||
|
}
|
||||||
|
// https://stackoverflow.com/a/37643472
|
||||||
|
function cmykToRgb(c, m, y, k) {
|
||||||
|
const r = (1 - c) * (1 - k);
|
||||||
|
const g = (1 - m) * (1 - k);
|
||||||
|
const b = (1 - y) * (1 - k);
|
||||||
|
return [r, g, b];
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for making it so an input isn't modified if we just typed in it
|
||||||
|
let activeInput = null;
|
||||||
|
document.addEventListener("keydown", () => {
|
||||||
|
activeInput = document.activeElement;
|
||||||
|
});
|
||||||
|
document.addEventListener("focusout", () => {
|
||||||
|
activeInput = null;
|
||||||
|
|
||||||
|
// in case they set an input to an invalid value
|
||||||
|
updateColorPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorPickerEl = document.getElementsByClassName("answer-colorpicker")[0];
|
||||||
|
|
||||||
|
const canvasEl = colorPickerEl.getElementsByClassName(
|
||||||
|
"answer-colorpicker-canvas"
|
||||||
|
)[0];
|
||||||
|
const canvasHueSvgEl = canvasEl.getElementsByClassName(
|
||||||
|
"answer-colorpicker-canvas-hue-svg"
|
||||||
|
)[0];
|
||||||
|
const pickerEl = colorPickerEl.getElementsByClassName(
|
||||||
|
"answer-colorpicker-picker"
|
||||||
|
)[0];
|
||||||
|
const previewEl = colorPickerEl.getElementsByClassName(
|
||||||
|
"answer-colorpicker-preview"
|
||||||
|
)[0];
|
||||||
|
const sliderEl = colorPickerEl.getElementsByClassName(
|
||||||
|
"answer-colorpicker-slider"
|
||||||
|
)[0];
|
||||||
|
const huepickerEl = colorPickerEl.getElementsByClassName(
|
||||||
|
"answer-colorpicker-huepicker"
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
const hexInputEl = document.getElementById("answer-colorpicker-hex-input");
|
||||||
|
const rgbInputEl = document.getElementById("answer-colorpicker-rgb-input");
|
||||||
|
const cmykInputEl = document.getElementById("answer-colorpicker-cmyk-input");
|
||||||
|
const hsvInputEl = document.getElementById("answer-colorpicker-hsv-input");
|
||||||
|
const hslInputEl = document.getElementById("answer-colorpicker-hsl-input");
|
||||||
|
|
||||||
|
let hsv = parseHsv(hsvInputEl.value);
|
||||||
|
let hsl = parseHsl(hslInputEl.value);
|
||||||
|
let rgb = parseRgb(rgbInputEl.value);
|
||||||
|
let cmyk = parseCmyk(cmykInputEl.value);
|
||||||
|
|
||||||
|
function clamp(n, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHsv(h, s, v) {
|
||||||
|
h = clamp(h, 0, 360);
|
||||||
|
s = clamp(s, 0, 1);
|
||||||
|
v = clamp(v, 0, 1);
|
||||||
|
|
||||||
|
hsv = [h, s, v];
|
||||||
|
hsl = hsvToHsl(...hsv);
|
||||||
|
rgb = hsvToRgb(...hsv);
|
||||||
|
cmyk = rgbToCmyk(...rgb);
|
||||||
|
updateColorPreview();
|
||||||
|
}
|
||||||
|
function setHsl(h, s, l) {
|
||||||
|
h = clamp(h, 0, 360);
|
||||||
|
s = clamp(s, 0, 1);
|
||||||
|
l = clamp(l, 0, 1);
|
||||||
|
|
||||||
|
hsl = [h, s, l];
|
||||||
|
hsv = hslToHsv(...hsl);
|
||||||
|
rgb = hsvToRgb(...hsv);
|
||||||
|
cmyk = rgbToCmyk(...rgb);
|
||||||
|
updateColorPreview();
|
||||||
|
}
|
||||||
|
function setRgb(r, g, b) {
|
||||||
|
r = clamp(r, 0, 1);
|
||||||
|
g = clamp(g, 0, 1);
|
||||||
|
b = clamp(b, 0, 1);
|
||||||
|
|
||||||
|
rgb = [r, g, b];
|
||||||
|
hsl = rgbToHsl(...rgb);
|
||||||
|
hsv = hslToHsv(...hsl);
|
||||||
|
cmyk = rgbToCmyk(...rgb);
|
||||||
|
updateColorPreview();
|
||||||
|
}
|
||||||
|
function setCmyk(c, m, y, k) {
|
||||||
|
c = clamp(c, 0, 1);
|
||||||
|
m = clamp(m, 0, 1);
|
||||||
|
y = clamp(y, 0, 1);
|
||||||
|
k = clamp(k, 0, 1);
|
||||||
|
|
||||||
|
cmyk = [c, m, y, k];
|
||||||
|
rgb = cmykToRgb(...cmyk);
|
||||||
|
hsl = rgbToHsl(...rgb);
|
||||||
|
hsv = rgbToHsv(...rgb);
|
||||||
|
updateColorPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouseInCanvas = false;
|
||||||
|
function canvasMouseDown(clientX, clientY) {
|
||||||
|
activeInput = null;
|
||||||
|
updatePicker(clientX, clientY);
|
||||||
|
mouseInCanvas = true;
|
||||||
|
}
|
||||||
|
function canvasMouseMove(clientX, clientY) {
|
||||||
|
activeInput;
|
||||||
|
if (mouseInCanvas) updatePicker(clientX, clientY);
|
||||||
|
}
|
||||||
|
function canvasMouseUp() {
|
||||||
|
mouseInCanvas = false;
|
||||||
|
}
|
||||||
|
canvasEl.addEventListener("mousedown", (e) => {
|
||||||
|
canvasMouseDown(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
canvasEl.addEventListener("touchstart", (e) => {
|
||||||
|
canvasMouseDown(e.touches[0].clientX, e.touches[0].clientY);
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
canvasMouseUp();
|
||||||
|
});
|
||||||
|
document.addEventListener("touchend", () => {
|
||||||
|
canvasMouseUp();
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
canvasMouseMove(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
document.addEventListener("touchmove", (e) => {
|
||||||
|
canvasMouseMove(e.touches[0].clientX, e.touches[0].clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mouseInSlider = false;
|
||||||
|
function sliderMouseDown(clientX) {
|
||||||
|
updateHuePicker(clientX);
|
||||||
|
mouseInSlider = true;
|
||||||
|
}
|
||||||
|
function sliderMouseMove(clientX) {
|
||||||
|
if (mouseInSlider) updateHuePicker(clientX);
|
||||||
|
}
|
||||||
|
function sliderMouseUp() {
|
||||||
|
mouseInSlider = false;
|
||||||
|
}
|
||||||
|
sliderEl.addEventListener("mousedown", (e) => {
|
||||||
|
sliderMouseDown(e.clientX);
|
||||||
|
});
|
||||||
|
sliderEl.addEventListener("touchstart", (e) => {
|
||||||
|
sliderMouseDown(e.touches[0].clientX);
|
||||||
|
});
|
||||||
|
huepickerEl.addEventListener("mousedown", (e) => {
|
||||||
|
sliderMouseDown(e.clientX);
|
||||||
|
});
|
||||||
|
huepickerEl.addEventListener("touchstart", (e) => {
|
||||||
|
sliderMouseDown(e.touches[0].clientX);
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
sliderMouseUp();
|
||||||
|
});
|
||||||
|
document.addEventListener("touchend", () => {
|
||||||
|
sliderMouseUp();
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
sliderMouseMove(e.clientX);
|
||||||
|
});
|
||||||
|
document.addEventListener("touchmove", (e) => {
|
||||||
|
sliderMouseMove(e.touches[0].clientX);
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePicker(clientX, clientY) {
|
||||||
|
const rect = canvasEl.getBoundingClientRect();
|
||||||
|
let x = clientX - rect.left;
|
||||||
|
let y = clientY - rect.top;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
if (y < 0) y = 0;
|
||||||
|
if (x > rect.width) x = rect.width;
|
||||||
|
if (y > rect.height) y = rect.height;
|
||||||
|
|
||||||
|
pickerEl.style.left = `${(x / rect.width) * 100}%`;
|
||||||
|
pickerEl.style.top = `${(y / rect.height) * 100}%`;
|
||||||
|
|
||||||
|
const hue = hsv[0];
|
||||||
|
setHsv(hue, x / rect.width, 1 - y / rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHuePicker(clientX) {
|
||||||
|
const rect = sliderEl.getBoundingClientRect();
|
||||||
|
let x = clientX - rect.left;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
if (x > rect.width) x = rect.width;
|
||||||
|
|
||||||
|
huepickerEl.style.left = `${(x / rect.width) * 100}%`;
|
||||||
|
|
||||||
|
const hue = (x / rect.width) * 360;
|
||||||
|
setHsv(hue, hsv[1], hsv[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColorPreview() {
|
||||||
|
const [r, g, b] = rgb;
|
||||||
|
const [hue, saturation, value] = hsv;
|
||||||
|
|
||||||
|
const color = `rgb(${r * 255}, ${g * 255}, ${b * 255})`;
|
||||||
|
pickerEl.style.backgroundColor = color;
|
||||||
|
previewEl.style.backgroundColor = color;
|
||||||
|
|
||||||
|
const hueColor = `hsl(${hue}, 100%, 50%)`;
|
||||||
|
huepickerEl.style.backgroundColor = hueColor;
|
||||||
|
canvasHueSvgEl.style.setProperty("stop-color", hueColor);
|
||||||
|
|
||||||
|
pickerEl.style.left = `${saturation * 100}%`;
|
||||||
|
pickerEl.style.top = `${(1 - value) * 100}%`;
|
||||||
|
|
||||||
|
if (activeInput !== hexInputEl) {
|
||||||
|
hexInputEl.value =
|
||||||
|
"#" +
|
||||||
|
rgb
|
||||||
|
.map((c) =>
|
||||||
|
Math.round(c * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
if (activeInput !== rgbInputEl) {
|
||||||
|
rgbInputEl.value = rgb.map((c) => Math.round(c * 255)).join(", ");
|
||||||
|
}
|
||||||
|
if (activeInput !== cmykInputEl) {
|
||||||
|
const cmykPercent = cmyk.map((c) => Math.round(c * 100));
|
||||||
|
cmykInputEl.value = `${cmykPercent[0]}%, ${cmykPercent[1]}%, ${cmykPercent[2]}%, ${cmykPercent[3]}%`;
|
||||||
|
}
|
||||||
|
if (activeInput !== hsvInputEl) {
|
||||||
|
const hAngle = Math.round(hsv[0]);
|
||||||
|
hsvInputEl.value = `${hAngle}°, ${Math.round(hsv[1] * 100)}%, ${Math.round(
|
||||||
|
hsv[2] * 100
|
||||||
|
)}%`;
|
||||||
|
}
|
||||||
|
if (activeInput !== hslInputEl) {
|
||||||
|
hslInputEl.value = `${Math.round(hsl[0])}°, ${Math.round(
|
||||||
|
hsl[1] * 100
|
||||||
|
)}%, ${Math.round(hsl[2] * 100)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHex(value) {
|
||||||
|
value = hexInputEl.value.replace("#", "");
|
||||||
|
if (value.length === 6) {
|
||||||
|
const r = parseInt(value.slice(0, 2), 16) / 255;
|
||||||
|
const g = parseInt(value.slice(2, 4), 16) / 255;
|
||||||
|
const b = parseInt(value.slice(4, 6), 16) / 255;
|
||||||
|
return [r, g, b];
|
||||||
|
} else if (value.length === 3) {
|
||||||
|
const r = parseInt(value[0] + value[0], 16) / 255;
|
||||||
|
const g = parseInt(value[1] + value[1], 16) / 255;
|
||||||
|
const b = parseInt(value[2] + value[2], 16) / 255;
|
||||||
|
return [r, g, b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setFromHexInput() {
|
||||||
|
setRgb(...parseHex(hexInputEl.value));
|
||||||
|
}
|
||||||
|
hexInputEl.addEventListener("input", setFromHexInput);
|
||||||
|
|
||||||
|
function parseRgb(value) {
|
||||||
|
return value.split(",").map((c) => parseInt(c) / 255);
|
||||||
|
}
|
||||||
|
function setFromRgbInput() {
|
||||||
|
setRgb(...parseRgb(rgbInputEl.value));
|
||||||
|
}
|
||||||
|
rgbInputEl.addEventListener("input", setFromRgbInput);
|
||||||
|
|
||||||
|
function parseCmyk(value) {
|
||||||
|
return value.split(",").map((c) => parseInt(c) / 100);
|
||||||
|
}
|
||||||
|
function setFromCmykInput() {
|
||||||
|
setCmyk(...parseCmyk(cmykInputEl.value));
|
||||||
|
}
|
||||||
|
cmykInputEl.addEventListener("input", setFromCmykInput);
|
||||||
|
|
||||||
|
function parseHsv(value) {
|
||||||
|
value = hsvInputEl.value.split(",").map((c) => parseInt(c));
|
||||||
|
value[1] /= 100;
|
||||||
|
value[2] /= 100;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function setFromHsvInput() {
|
||||||
|
setHsv(...parseHsv(hsvInputEl.value));
|
||||||
|
}
|
||||||
|
hsvInputEl.addEventListener("input", setFromHsvInput);
|
||||||
|
|
||||||
|
function parseHsl(value) {
|
||||||
|
value = hslInputEl.value.split(",").map((c) => parseInt(c));
|
||||||
|
value[1] /= 100;
|
||||||
|
value[2] /= 100;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function setFromHslInput() {
|
||||||
|
setHsl(...parseHsl(hslInputEl.value));
|
||||||
|
}
|
||||||
|
hslInputEl.addEventListener("input", setFromHslInput);
|
||||||
|
|
||||||
|
updateColorPreview();
|
@ -368,6 +368,82 @@ h3.answer-thesaurus-category-title {
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.answer-colorpicker-preview-container {
|
||||||
|
display: flex;
|
||||||
|
height: 228px;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-preview {
|
||||||
|
width: 204px;
|
||||||
|
max-width: 33%;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-picker-container {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-picker,
|
||||||
|
.answer-colorpicker-huepicker {
|
||||||
|
position: absolute;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
transform: translate(-0.5rem, -0.5rem);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-canvas-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-canvas {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-slider-container {
|
||||||
|
margin: 1rem;
|
||||||
|
position: relative;
|
||||||
|
height: 1rem;
|
||||||
|
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-slider {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-huepicker {
|
||||||
|
transform: translate(-0.5rem, -50%);
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
.answer-colorpicker label {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-hex-input-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-hex-input-container label {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
#answer-colorpicker-hex-input {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-other-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-input-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.answer-colorpicker-other-inputs input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* infobox */
|
/* infobox */
|
||||||
.infobox {
|
.infobox {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
@ -73,6 +73,7 @@ pub async fn run(config: Config) {
|
|||||||
"style.css",
|
"style.css",
|
||||||
"script.js",
|
"script.js",
|
||||||
"robots.txt",
|
"robots.txt",
|
||||||
|
"scripts/colorpicker.js",
|
||||||
"themes/catppuccin-mocha.css",
|
"themes/catppuccin-mocha.css",
|
||||||
"themes/catppuccin-latte.css",
|
"themes/catppuccin-latte.css",
|
||||||
"themes/nord-bluish.css",
|
"themes/nord-bluish.css",
|
||||||
|
Loading…
Reference in New Issue
Block a user