diff --git a/color-module/src/convert/mod.rs b/color-module/src/convert/mod.rs index d1cdeae..5eb9cee 100644 --- a/color-module/src/convert/mod.rs +++ b/color-module/src/convert/mod.rs @@ -70,6 +70,13 @@ macro_rules! parse_to_oklch { .into_format::<f32>(), ) }; + ($origin: expr) => { + palette::Oklch::from_color( + palette::Srgb::from_str($origin) + .map_err(|_| crate::errors::ColorError::UnrecogniazedRGB($origin.to_string()))? + .into_format::<f32>(), + ) + }; } #[macro_export] diff --git a/color-module/src/schemes/mod.rs b/color-module/src/schemes/mod.rs index e1ac790..4c02f66 100644 --- a/color-module/src/schemes/mod.rs +++ b/color-module/src/schemes/mod.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use material_design_2::MaterialDesign2Scheme; use material_design_3::MaterialDesign3Scheme; use q_style::{QScheme, SchemeSetting}; +use swatch_style::{SwatchEntry, SwatchSchemeSetting}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; use crate::errors; @@ -126,3 +127,18 @@ pub fn generate_q_scheme_manually( )) .map_err(|_| errors::ColorError::UnableToAssembleOutput)?) } + +#[wasm_bindgen] +pub fn generate_swatch_scheme( + colors: Vec<SwatchEntry>, + setting: SwatchSchemeSetting, +) -> Result<JsValue, errors::ColorError> { + let scheme = swatch_style::SwatchScheme::new(colors, setting)?; + Ok(serde_wasm_bindgen::to_value(&( + scheme.swatches(), + scheme.output_css_variables(), + scheme.output_scss_variables(), + scheme.output_javascript_object(), + )) + .map_err(|_| errors::ColorError::UnableToAssembleOutput)?) +} diff --git a/color-module/src/schemes/swatch_style/mod.rs b/color-module/src/schemes/swatch_style/mod.rs new file mode 100644 index 0000000..bb2f284 --- /dev/null +++ b/color-module/src/schemes/swatch_style/mod.rs @@ -0,0 +1,122 @@ +use palette::FromColor; +use std::collections::HashMap; +use std::str::FromStr; + +pub use setting::SwatchSchemeSetting; +use swatch::Swatch; +use wasm_bindgen::prelude::*; + +use crate::{errors, parse_to_oklch}; + +use super::SchemeExport; + +mod setting; +mod swatch; + +#[derive(Debug, Clone)] +pub struct SwatchScheme { + light: HashMap<String, Swatch>, + dark: HashMap<String, Swatch>, +} + +#[derive(Debug, Clone)] +#[wasm_bindgen(getter_with_clone)] +pub struct SwatchEntry { + pub name: String, + pub color: String, +} + +impl SwatchScheme { + pub fn new( + colors: Vec<SwatchEntry>, + setting: SwatchSchemeSetting, + ) -> Result<Self, errors::ColorError> { + let mut light = HashMap::new(); + let mut dark = HashMap::new(); + + for entry in colors { + let color = parse_to_oklch!(&entry.color); + let darken_color = color * setting.dark_convert; + light.insert(entry.name.clone(), Swatch::new(&color, &setting)); + dark.insert(entry.name, Swatch::new(&darken_color, &setting)); + } + + Ok(Self { light, dark }) + } + + pub fn swatches(&self) -> HashMap<String, HashMap<String, Vec<String>>> { + let mut light_swatches = HashMap::new(); + let mut dark_swatches = HashMap::new(); + + for (name, swatch) in &self.light { + light_swatches.insert(name.clone(), swatch.swtch_hex()); + } + for (name, swatch) in &self.dark { + dark_swatches.insert(name.clone(), swatch.swtch_hex()); + } + + HashMap::from([ + ("light".to_string(), light_swatches), + ("dark".to_string(), dark_swatches), + ]) + } +} + +impl SchemeExport for SwatchScheme { + fn output_css_variables(&self) -> String { + let mut variables = Vec::new(); + + for (name, swatch) in &self.light { + variables.extend(swatch.to_css_variables("light", name)); + } + for (name, swatch) in &self.dark { + variables.extend(swatch.to_css_variables("dark", name)); + } + + variables.join("\n") + } + + fn output_scss_variables(&self) -> String { + let mut variables = Vec::new(); + + for (name, swatch) in &self.light { + variables.extend(swatch.to_scss_variables("light", name)); + } + for (name, swatch) in &self.dark { + variables.extend(swatch.to_scss_variables("dark", name)); + } + + variables.join("\n") + } + + fn output_javascript_object(&self) -> String { + let mut object = Vec::new(); + object.push("{".to_string()); + + object.push(" light: {".to_string()); + for (name, swatch) in &self.light { + object.extend( + swatch + .to_javascript_fields("light", name) + .iter() + .map(|s| format!(" {}", s)), + ); + } + object.push(" },".to_string()); + + object.push(" dark: {".to_string()); + for (name, swatch) in &self.dark { + object.extend( + swatch + .to_javascript_fields("dark", name) + .iter() + .map(|s| format!(" {}", s)), + ); + } + object.push(" },".to_string()); + + object.push("}".to_string()); + + object.join("\n") + } +} diff --git a/color-module/src/schemes/swatch_style/setting.rs b/color-module/src/schemes/swatch_style/setting.rs new file mode 100644 index 0000000..a42dea1 --- /dev/null +++ b/color-module/src/schemes/swatch_style/setting.rs @@ -0,0 +1,29 @@ +use serde::Serialize; +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::schemes::q_style::ColorShifting; + +#[derive(Debug, Clone, Serialize)] +#[wasm_bindgen] +pub struct SwatchSchemeSetting { + pub amount: usize, + pub min_lightness: f32, + pub max_lightness: f32, + pub include_primary: bool, + pub dark_convert: ColorShifting, +} + +impl Default for SwatchSchemeSetting { + fn default() -> Self { + Self { + amount: 10, + min_lightness: 10.0, + max_lightness: 90.0, + include_primary: false, + dark_convert: ColorShifting { + chroma: -0.3, + lightness: -0.3, + }, + } + } +} diff --git a/color-module/src/schemes/swatch_style/swatch.rs b/color-module/src/schemes/swatch_style/swatch.rs new file mode 100644 index 0000000..a445cec --- /dev/null +++ b/color-module/src/schemes/swatch_style/swatch.rs @@ -0,0 +1,133 @@ +use palette::Oklch; + +use crate::convert::map_oklch_to_srgb_hex; + +use super::setting::SwatchSchemeSetting; + +#[derive(Debug, Clone)] +pub struct Swatch { + min_key: f32, + max_key: f32, + primary_key: Oklch, + include_primary: bool, + color_amount: usize, +} + +impl Swatch { + pub fn new(primary: &Oklch, setting: &SwatchSchemeSetting) -> Self { + Self { + min_key: primary.l.min(setting.min_lightness), + max_key: primary.l.max(setting.max_lightness), + primary_key: primary.clone(), + include_primary: setting.include_primary, + color_amount: setting.amount, + } + } + + fn find_interval(&self) -> (usize, usize) { + if !self.include_primary { + return (0, 0); + } + if self.primary_key.l == self.min_key { + return (0, 1); + } + if self.primary_key.l == self.max_key { + return (self.color_amount - 2, self.color_amount - 1); + } + let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32; + let index = ((self.primary_key.l - self.min_key) / step) as usize; + + (index, index + 1) + } + + pub fn swatch(&self) -> Vec<Oklch> { + let mut swatch = Vec::new(); + if self.include_primary { + let (_, primary_index) = self.find_interval(); + if primary_index > 0 { + let step = (self.max_key - self.min_key) / primary_index as f32; + for i in 0..primary_index { + let lightness = self.min_key + step * i as f32; + swatch.push(Oklch { + l: lightness, + ..self.primary_key + }); + } + } + if primary_index < self.color_amount - 1 { + let step = + (self.max_key - self.min_key) / (self.color_amount - primary_index) as f32; + for i in primary_index..self.color_amount { + let lightness = self.min_key + step * i as f32; + swatch.push(Oklch { + l: lightness, + ..self.primary_key + }); + } + } + } else { + let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32; + for i in 0..self.color_amount { + let lightness = self.min_key + step * i as f32; + swatch.push(Oklch { + l: lightness, + ..self.primary_key + }); + } + } + swatch + } + + pub fn swtch_hex(&self) -> Vec<String> { + self.swatch().iter().map(map_oklch_to_srgb_hex).collect() + } + + pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> { + let mut variables = Vec::new(); + for (i, color) in self.swatch().iter().enumerate() { + variables.push(format!( + "--color-{}-{}-{}: #{};", + prefix, + name, + i * 100, + map_oklch_to_srgb_hex(color) + )); + } + variables + } + + pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> { + let mut variables = Vec::new(); + for (i, color) in self.swatch().iter().enumerate() { + variables.push(format!( + "${}-{}-{}: #{};", + prefix, + name, + i * 100, + map_oklch_to_srgb_hex(color) + )); + } + variables + } + + pub fn to_javascript_fields(&self, prefix: &str, name: &str) -> Vec<String> { + let mut variables = Vec::new(); + let capitalized_name = name + .chars() + .next() + .unwrap() + .to_ascii_uppercase() + .to_string() + + &name[1..]; + for (i, color) in self.swatch().iter().enumerate() { + variables.push(format!( + "{}{}{}: '#{}',", + prefix, + capitalized_name, + i * 100, + map_oklch_to_srgb_hex(color) + )); + } + variables + } +}