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:
mat 2024-07-16 03:07:23 -05:00 committed by GitHub
parent fdf8163fbb
commit 95f7c147f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 847 additions and 17 deletions

View File

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

View 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)
}

View File

@ -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)]

View File

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

View 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();

View File

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

View File

@ -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",