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, errors::ColorError> { let srgb = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); Ok(Box::new([srgb.red, srgb.green, srgb.blue])) } #[wasm_bindgen] pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> Result { let srgb = Srgb::new(r, g, b); Ok(format!("{:x}", srgb)) } #[wasm_bindgen] pub fn represent_hsl(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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 { 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::())) } #[wasm_bindgen] pub fn represent_lab(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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 { 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::())) } #[wasm_bindgen] pub fn represent_oklch(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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 { 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::())) } #[wasm_bindgen] pub fn represent_hct(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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 { 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::())) } #[wasm_bindgen] pub fn shift_hue(color: &str, degree: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())) } #[wasm_bindgen] pub fn lighten(color: &str, percent: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())) } #[wasm_bindgen] pub fn lighten_absolute(color: &str, value: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())) } #[wasm_bindgen] pub fn darken(color: &str, percent: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())) } #[wasm_bindgen] pub fn darken_absolute(color: &str, value: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())) } #[wasm_bindgen] pub fn mix(color1: &str, color2: &str, percent: f32) -> Result { let srgb1 = Srgb::from_str(color1) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color1.to_string()))? .into_format::(); let srgb2 = Srgb::from_str(color2) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color2.to_string()))? .into_format::(); let mixed_color = srgb1.mix(srgb2, percent); Ok(format!("{:x}", mixed_color.into_format::())) } #[wasm_bindgen] pub fn tint(color: &str, percent: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); let tinted_color = origin_color.mix(Srgb::new(1.0, 1.0, 1.0), percent); Ok(format!("{:x}", tinted_color.into_format::())) } #[wasm_bindgen] pub fn shade(color: &str, percent: f32) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); let shaded_color = origin_color.mix(Srgb::new(0.0, 0.0, 0.0), percent); Ok(format!("{:x}", shaded_color.into_format::())) } #[wasm_bindgen] pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result { let fg_srgb = Srgb::from_str(fg_color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(fg_color.to_string()))? .into_format::(); let bg_srgb = Srgb::from_str(bg_color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(bg_color.to_string()))? .into_format::(); Ok(fg_srgb.relative_contrast(bg_srgb)) } #[wasm_bindgen] pub fn analogous_30(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::()), format!("{:x}", srgb_p30.into_format::()), ]) } #[wasm_bindgen] pub fn analogous_60(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::()), format!("{:x}", srgb_p60.into_format::()), ]) } #[wasm_bindgen] pub fn complementary(color: &str) -> Result { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())) } #[wasm_bindgen] pub fn split_complementary(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::()), format!("{:x}", srgb_p210.into_format::()), ]) } #[wasm_bindgen] pub fn tetradic(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::()), format!("{:x}", srgb_p180.into_format::()), format!("{:x}", srgb_p270.into_format::()), ]) } #[wasm_bindgen] pub fn triadic(color: &str) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::()), format!("{:x}", srgb_p240.into_format::()), ]) } #[wasm_bindgen] pub fn series( color: &str, expand_amount: i16, step: f32, ) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())); } color_series.push(format!("{:x}", origin_color.into_format::())); 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::())); } Ok(color_series) } #[wasm_bindgen] pub fn tonal_lighten_series( color: &str, expand_amount: i16, step: f32, ) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())); } Ok(color_series) } #[wasm_bindgen] pub fn tonal_darken_series( color: &str, expand_amount: i16, step: f32, ) -> Result, errors::ColorError> { let origin_color = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); 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::())); } Ok(color_series.into_iter().rev().collect()) } #[wasm_bindgen] pub fn color_categories() -> Result { let categories = Category::iter() .map(|variant| { serde_json::json!({ "label": variant.label(), "value": variant.to_string(), }) }) .collect::>(); serde_wasm_bindgen::to_value(&categories).map_err(|e| e.to_string()) } #[wasm_bindgen] pub fn search_color_cards(tag: String, category: Option) -> Result { 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::>(); 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 { let srgb = Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(); let other_srgb = Srgb::from_str(other) .map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))? .into_format::(); Ok(srgb.difference(&other_srgb)) } #[wasm_bindgen] pub fn differ_in_hsl( color: &str, other: &str, ) -> Result { let hsl = Hsl::from_color( Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(), ); let other_hsl = Hsl::from_color( Srgb::from_str(other) .map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))? .into_format::(), ); Ok(hsl.difference(&other_hsl)) } #[wasm_bindgen] pub fn differ_in_hct( color: &str, other: &str, ) -> Result { let hct = Cam16Jch::from_xyz( Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::() .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::() .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 { let oklch = Oklch::from_color( Srgb::from_str(color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? .into_format::(), ); let other_oklch = Oklch::from_color( Srgb::from_str(other) .map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))? .into_format::(), ); Ok(oklch.difference(&other_oklch)) } #[wasm_bindgen] pub fn tint_scale( basic_color: &str, mixed_color: &str, ) -> Result { let basic_color = Srgb::from_str(basic_color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))? .into_format::(); let mixed_color = Srgb::from_str(mixed_color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))? .into_format::(); Ok(reversing::MixReversing::from_tint_rgb( basic_color, mixed_color, )) } #[wasm_bindgen] pub fn shade_scale( basic_color: &str, mixed_color: &str, ) -> Result { let basic_color = Srgb::from_str(basic_color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))? .into_format::(); let mixed_color = Srgb::from_str(mixed_color) .map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))? .into_format::(); Ok(reversing::MixReversing::from_shade_rgb( basic_color, mixed_color, )) }