color-q/color-module/src/lib.rs

534 lines
19 KiB
Rust

use std::{str::FromStr, sync::Arc};
use color_card::Category;
use color_differ::ColorDifference;
use palette::{
cam16::{Cam16Jch, Parameters},
color_difference::Wcag21RelativeContrast,
color_theory::*,
convert::FromColorUnclamped,
Darken, FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Lighten, Mix, Oklch, ShiftHue, Srgb,
};
use strum::IntoEnumIterator;
use wasm_bindgen::prelude::*;
mod color_card;
mod color_differ;
mod errors;
mod reversing;
#[wasm_bindgen]
pub fn represent_rgb(color: &str) -> Result<Box<[u8]>, errors::ColorError> {
let srgb = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<u8>();
Ok(Box::new([srgb.red, srgb.green, srgb.blue]))
}
#[wasm_bindgen]
pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> Result<String, errors::ColorError> {
let srgb = Srgb::new(r, g, b);
Ok(format!("{:x}", srgb))
}
#[wasm_bindgen]
pub fn represent_hsl(color: &str) -> Result<Box<[f32]>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hsl = Hsl::from_color(origin_color);
Ok(Box::new([
hsl.hue.into_positive_degrees(),
hsl.saturation,
hsl.lightness,
]))
}
#[wasm_bindgen]
pub fn hsl_to_hex(h: f32, s: f32, l: f32) -> Result<String, errors::ColorError> {
let hsl = Hsl::new(h, s, l);
let srgb = Srgb::from_color_unclamped(hsl);
if !srgb.is_within_bounds() {
return Err(errors::ColorError::ComponentOutOfBounds);
}
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn represent_lab(color: &str) -> Result<Box<[f32]>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let lab = Lab::from_color(origin_color);
Ok(Box::new([lab.l, lab.a, lab.b]))
}
#[wasm_bindgen]
pub fn lab_to_hex(l: f32, a: f32, b: f32) -> Result<String, errors::ColorError> {
let lab = Lab::new(l, a, b);
let srgb = Srgb::from_color_unclamped(lab);
if !srgb.is_within_bounds() {
return Err(errors::ColorError::ComponentOutOfBounds);
}
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn represent_oklch(color: &str) -> Result<Box<[f32]>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let lch = Oklch::from_color(origin_color);
Ok(Box::new([
lch.l,
lch.chroma,
lch.hue.into_positive_degrees(),
]))
}
#[wasm_bindgen]
pub fn oklch_to_hex(l: f32, c: f32, h: f32) -> Result<String, errors::ColorError> {
let lch = Oklch::new(l, c, h);
let srgb = Srgb::from_color_unclamped(lch);
if !srgb.is_within_bounds() {
return Err(errors::ColorError::ComponentOutOfBounds);
}
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn represent_hct(color: &str) -> Result<Box<[f32]>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
Ok(Box::new([
hct.hue.into_positive_degrees(),
hct.chroma,
hct.lightness,
]))
}
#[wasm_bindgen]
pub fn hct_to_hex(hue: f32, chroma: f32, tone: f32) -> Result<String, errors::ColorError> {
let hct = Cam16Jch::new(tone, chroma, hue);
let srgb = Srgb::from_color_unclamped(hct.into_xyz(Parameters::default_static_wp(40.0)));
if !srgb.is_within_bounds() {
return Err(errors::ColorError::ComponentOutOfBounds);
}
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn shift_hue(color: &str, degree: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let shifted_color = oklch.shift_hue(degree);
let srgb = Srgb::from_color(shifted_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn lighten(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let lightened_color = oklch.lighten(percent);
let srgb = Srgb::from_color(lightened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn lighten_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let lightened_color = oklch.lighten_fixed(value);
let srgb = Srgb::from_color(lightened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn darken(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let darkened_color = oklch.darken(percent);
let srgb = Srgb::from_color(darkened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn darken_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let darkened_color = oklch.darken_fixed(value);
let srgb = Srgb::from_color(darkened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn mix(color1: &str, color2: &str, percent: f32) -> Result<String, errors::ColorError> {
let srgb1 = Srgb::from_str(color1)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color1.to_string()))?
.into_format::<f32>();
let srgb2 = Srgb::from_str(color2)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color2.to_string()))?
.into_format::<f32>();
let mixed_color = srgb1.mix(srgb2, percent);
Ok(format!("{:x}", mixed_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn tint(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let tinted_color = origin_color.mix(Srgb::new(1.0, 1.0, 1.0), percent);
Ok(format!("{:x}", tinted_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn shade(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let shaded_color = origin_color.mix(Srgb::new(0.0, 0.0, 0.0), percent);
Ok(format!("{:x}", shaded_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, errors::ColorError> {
let fg_srgb = Srgb::from_str(fg_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(fg_color.to_string()))?
.into_format::<f32>();
let bg_srgb = Srgb::from_str(bg_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(bg_color.to_string()))?
.into_format::<f32>();
Ok(fg_srgb.relative_contrast(bg_srgb))
}
#[wasm_bindgen]
pub fn analogous_30(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_n30, color_p30) = oklch.analogous();
let srgb_n30 = Srgb::from_color(color_n30);
let srgb_p30 = Srgb::from_color(color_p30);
Ok(vec![
format!("{:x}", srgb_n30.into_format::<u8>()),
format!("{:x}", srgb_p30.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn analogous_60(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_n60, color_p60) = oklch.analogous_secondary();
let srgb_n60 = Srgb::from_color(color_n60);
let srgb_p60 = Srgb::from_color(color_p60);
Ok(vec![
format!("{:x}", srgb_n60.into_format::<u8>()),
format!("{:x}", srgb_p60.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn complementary(color: &str) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let complementary_color = oklch.complementary();
let srgb = Srgb::from_color(complementary_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn split_complementary(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p150, color_p210) = oklch.split_complementary();
let srgb_p150 = Srgb::from_color(color_p150);
let srgb_p210 = Srgb::from_color(color_p210);
Ok(vec![
format!("{:x}", srgb_p150.into_format::<u8>()),
format!("{:x}", srgb_p210.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn tetradic(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p90, color_p180, color_p270) = oklch.tetradic();
let srgb_p90 = Srgb::from_color(color_p90);
let srgb_p180 = Srgb::from_color(color_p180);
let srgb_p270 = Srgb::from_color(color_p270);
Ok(vec![
format!("{:x}", srgb_p90.into_format::<u8>()),
format!("{:x}", srgb_p180.into_format::<u8>()),
format!("{:x}", srgb_p270.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn triadic(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p120, color_p240) = oklch.triadic();
let srgb_p120 = Srgb::from_color(color_p120);
let srgb_p240 = Srgb::from_color(color_p240);
Ok(vec![
format!("{:x}", srgb_p120.into_format::<u8>()),
format!("{:x}", srgb_p240.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Arc::new(Oklch::from_color(origin_color.clone()));
let mut color_series = Vec::new();
for s in (1..=expand_amount).rev() {
let darkened_color = Arc::clone(&oklch).darken(s as f32 * step);
let srgb = Srgb::from_color(darkened_color);
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
color_series.push(format!("{:x}", origin_color.into_format::<u8>()));
for s in 1..=expand_amount {
let lightened_color = Arc::clone(&oklch).lighten(s as f32 * step);
let srgb = Srgb::from_color(lightened_color);
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series)
}
#[wasm_bindgen]
pub fn tonal_lighten_series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
let mut color_series = Vec::new();
let mut lightness = hct.lightness;
for _ in 1..=expand_amount {
lightness += (100.0 - lightness) * step;
let lightened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
let srgb = Srgb::from_color(lightened_color.into_xyz(Parameters::default_static_wp(40.0)));
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series)
}
#[wasm_bindgen]
pub fn tonal_darken_series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
let mut color_series = Vec::new();
let mut lightness = hct.lightness;
for _ in 1..=expand_amount {
lightness *= 1.0 - step;
let darkened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
let srgb = Srgb::from_color(darkened_color.into_xyz(Parameters::default_static_wp(40.0)));
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series.into_iter().rev().collect())
}
#[wasm_bindgen]
pub fn color_categories() -> Result<JsValue, String> {
let categories = Category::iter()
.map(|variant| {
serde_json::json!({
"label": variant.label(),
"value": variant.to_string(),
})
})
.collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&categories).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn search_color_cards(tag: String, category: Option<String>) -> Result<JsValue, String> {
let selected_category = category.and_then(|c| Category::from_str(&c).ok());
let all_cards = &*color_card::COLOR_CARDS;
let mut cards = all_cards
.iter()
.filter(|card| card.tags.contains(&tag))
.filter(|card| {
if let Some(category) = &selected_category {
let card_category = Category::from_oklch(&card.oklch);
card_category == *category
} else {
true
}
})
.collect::<Vec<_>>();
cards.sort_by(|a, b| {
a.oklch[2]
.partial_cmp(&b.oklch[2])
.or_else(|| a.oklch[1].partial_cmp(&b.oklch[1]))
.or_else(|| a.oklch[0].partial_cmp(&b.oklch[0]))
.unwrap_or(std::cmp::Ordering::Equal)
});
serde_wasm_bindgen::to_value(&cards).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn differ_in_rgb(
color: &str,
other: &str,
) -> Result<color_differ::rgb::RGBDifference, errors::ColorError> {
let srgb = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let other_srgb = Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>();
Ok(srgb.difference(&other_srgb))
}
#[wasm_bindgen]
pub fn differ_in_hsl(
color: &str,
other: &str,
) -> Result<color_differ::hsl::HSLDifference, errors::ColorError> {
let hsl = Hsl::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let other_hsl = Hsl::from_color(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>(),
);
Ok(hsl.difference(&other_hsl))
}
#[wasm_bindgen]
pub fn differ_in_hct(
color: &str,
other: &str,
) -> Result<color_differ::cam16jch::HctDiffference, errors::ColorError> {
let hct = Cam16Jch::from_xyz(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let other_hct = Cam16Jch::from_xyz(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
Ok(hct.difference(&other_hct))
}
#[wasm_bindgen]
pub fn differ_in_oklch(
color: &str,
other: &str,
) -> Result<color_differ::oklch::OklchDifference, errors::ColorError> {
let oklch = Oklch::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let other_oklch = Oklch::from_color(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>(),
);
Ok(oklch.difference(&other_oklch))
}
#[wasm_bindgen]
pub fn tint_scale(
basic_color: &str,
mixed_color: &str,
) -> Result<reversing::MixReversing, errors::ColorError> {
let basic_color = Srgb::from_str(basic_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))?
.into_format::<f32>();
let mixed_color = Srgb::from_str(mixed_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))?
.into_format::<f32>();
Ok(reversing::MixReversing::from_tint_rgb(
basic_color,
mixed_color,
))
}
#[wasm_bindgen]
pub fn shade_scale(
basic_color: &str,
mixed_color: &str,
) -> Result<reversing::MixReversing, errors::ColorError> {
let basic_color = Srgb::from_str(basic_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))?
.into_format::<f32>();
let mixed_color = Srgb::from_str(mixed_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))?
.into_format::<f32>();
Ok(reversing::MixReversing::from_shade_rgb(
basic_color,
mixed_color,
))
}