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 fend;
|
||||
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 url::Url;
|
||||
|
||||
use crate::engines::{EngineResponse, CLIENT};
|
||||
use crate::engines::{EngineResponse, RequestResponse, CLIENT};
|
||||
|
||||
pub fn request(query: &str) -> reqwest::RequestBuilder {
|
||||
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),
|
||||
],
|
||||
use super::colorpicker;
|
||||
|
||||
pub fn request(mut query: &str) -> RequestResponse {
|
||||
if !colorpicker::MatchedColorModel::new(query).is_empty() {
|
||||
// "color picker" is a wikipedia article but we only want to show the
|
||||
// actual color picker answer
|
||||
return RequestResponse::None;
|
||||
}
|
||||
|
||||
// adding "wikipedia" to the start or end of your query is common when you
|
||||
// want to get a wikipedia article
|
||||
if let Some(stripped_query) = query.strip_suffix(" wikipedia") {
|
||||
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)]
|
||||
|
@ -42,6 +42,7 @@ engines! {
|
||||
Fend = "fend",
|
||||
Ip = "ip",
|
||||
Notepad = "notepad",
|
||||
ColorPicker = "colorpicker",
|
||||
Numbat = "numbat",
|
||||
Thesaurus = "thesaurus",
|
||||
Timezone = "timezone",
|
||||
@ -70,6 +71,7 @@ engine_requests! {
|
||||
Fend => answer::fend::request, None,
|
||||
Ip => answer::ip::request, None,
|
||||
Notepad => answer::notepad::request, None,
|
||||
ColorPicker => answer::colorpicker::request, None,
|
||||
Numbat => answer::numbat::request, None,
|
||||
Thesaurus => answer::thesaurus::request, parse_response,
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-bottom: 1rem;
|
||||
|
@ -73,6 +73,7 @@ pub async fn run(config: Config) {
|
||||
"style.css",
|
||||
"script.js",
|
||||
"robots.txt",
|
||||
"scripts/colorpicker.js",
|
||||
"themes/catppuccin-mocha.css",
|
||||
"themes/catppuccin-latte.css",
|
||||
"themes/nord-bluish.css",
|
||||
|
Loading…
Reference in New Issue
Block a user