diff --git a/color-module/src/convert/mod.rs b/color-module/src/convert/mod.rs new file mode 100644 index 0000000..10ec0fb --- /dev/null +++ b/color-module/src/convert/mod.rs @@ -0,0 +1,26 @@ +use palette::{ + cam16::{Cam16Jch, Parameters}, + convert::FromColorUnclamped, + IsWithinBounds, Srgb, +}; + +pub fn map_cam16jch_to_srgb(origin: &Cam16Jch) -> Srgb { + let mut new_original = Cam16Jch::new(origin.lightness, origin.chroma, origin.hue); + const FACTOR: f32 = 0.99; + loop { + let new_srgb = + Srgb::from_color_unclamped(new_original.into_xyz(Parameters::default_static_wp(40.0))); + if new_srgb.is_within_bounds() { + break new_srgb; + } + new_original = Cam16Jch::new( + new_original.lightness, + new_original.chroma * FACTOR, + new_original.hue, + ); + } +} + +pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch) -> String { + format!("{:x}", map_cam16jch_to_srgb(origin).into_format::()) +} diff --git a/color-module/src/errors.rs b/color-module/src/errors.rs index f6c24e7..dd4f54f 100644 --- a/color-module/src/errors.rs +++ b/color-module/src/errors.rs @@ -7,6 +7,10 @@ pub enum ColorError { UnrecogniazedRGB(String), #[error("Some color component is out of bounds")] ComponentOutOfBounds, + #[error("Unable to parse argument")] + UnableToParseArgument, + #[error("Unable to assemble output")] + UnableToAssembleOutput, } impl Into for ColorError { diff --git a/color-module/src/lib.rs b/color-module/src/lib.rs index 302c2d1..cad32dd 100644 --- a/color-module/src/lib.rs +++ b/color-module/src/lib.rs @@ -14,9 +14,11 @@ use wasm_bindgen::prelude::*; mod color_card; mod color_differ; +mod convert; mod errors; mod palettes; mod reversing; +mod schemes; #[wasm_bindgen] pub fn represent_rgb(color: &str) -> Result, errors::ColorError> { diff --git a/color-module/src/schemes/material_design_3/baseline.rs b/color-module/src/schemes/material_design_3/baseline.rs new file mode 100644 index 0000000..2a57e67 --- /dev/null +++ b/color-module/src/schemes/material_design_3/baseline.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; + +use serde::Serialize; + +use crate::convert::map_cam16jch_to_srgb_hex; + +use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette}; + +#[derive(Debug, Clone, Serialize)] +pub struct M3BaselineColors { + pub primary: M3ColorSet, + pub secondary: M3ColorSet, + pub tertiary: M3ColorSet, + pub error: M3ColorSet, + pub surface: M3SurfaceSet, + pub outline: String, + pub outline_variant: String, + pub scrim: String, + pub shadown: String, + pub customs: HashMap, + dark_set: bool, +} + +impl M3BaselineColors { + pub fn new( + p: &TonalPalette, + s: &TonalPalette, + t: &TonalPalette, + n: &TonalPalette, + nv: &TonalPalette, + e: &TonalPalette, + dark_set: bool, + ) -> Self { + let color_set_generator = if dark_set { + M3ColorSet::new_dark_set + } else { + M3ColorSet::new_light_set + }; + let surface_set_generator = if dark_set { + M3SurfaceSet::new_dark_set + } else { + M3SurfaceSet::new_light_set + }; + + let primary = color_set_generator(p); + let secondary = color_set_generator(s); + let tertiary = color_set_generator(t); + let surface = surface_set_generator(n, nv); + let error = color_set_generator(e); + let outline = if dark_set { n.tone(60.0) } else { n.tone(50.0) }; + let outline_variant = if dark_set { + nv.tone(30.0) + } else { + nv.tone(80.0) + }; + let scrim = n.tone(0.0); + let shadow = n.tone(0.0); + + Self { + primary, + secondary, + tertiary, + error, + surface, + outline: map_cam16jch_to_srgb_hex(&outline), + outline_variant: map_cam16jch_to_srgb_hex(&outline_variant), + scrim: map_cam16jch_to_srgb_hex(&scrim), + shadown: map_cam16jch_to_srgb_hex(&shadow), + customs: HashMap::new(), + dark_set, + } + } + + pub fn add_custom_set(&mut self, name: String, c: &TonalPalette) { + let color_set_generator = if self.dark_set { + M3ColorSet::new_dark_set + } else { + M3ColorSet::new_light_set + }; + self.customs.insert(name, color_set_generator(c)); + } + + pub fn to_css_variables(&self) -> Vec { + let mut css_variables = Vec::new(); + let prefix = if self.dark_set { "dark" } else { "light" }; + + css_variables.extend(self.primary.to_css_variables(prefix, "primary")); + css_variables.extend(self.secondary.to_css_variables(prefix, "secondary")); + css_variables.extend(self.tertiary.to_css_variables(prefix, "tertiary")); + css_variables.extend(self.error.to_css_variables(prefix, "error")); + css_variables.extend(self.surface.to_css_variables(prefix)); + css_variables.push(format!("--color-{}-outline: #{};", prefix, self.outline)); + css_variables.push(format!( + "--color-{}-outline-variant: #{};", + prefix, self.outline_variant + )); + css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim)); + css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadown)); + for (name, color_set) in &self.customs { + css_variables.extend(color_set.to_css_variables(prefix, name)); + } + + css_variables + } + + pub fn to_scss_variables(&self) -> Vec { + let mut scss_variables = Vec::new(); + let prefix = if self.dark_set { "dark" } else { "light" }; + + scss_variables.extend(self.primary.to_scss_variables(prefix, "primary")); + scss_variables.extend(self.secondary.to_scss_variables(prefix, "secondary")); + scss_variables.extend(self.tertiary.to_scss_variables(prefix, "tertiary")); + scss_variables.extend(self.error.to_scss_variables(prefix, "error")); + scss_variables.extend(self.surface.to_scss_variables(prefix)); + scss_variables.push(format!("$color-{}-outline: #{};", prefix, self.outline)); + scss_variables.push(format!( + "$color-{}-outline-variant: #{};", + prefix, self.outline_variant + )); + scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim)); + scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadown)); + for (name, color_set) in &self.customs { + scss_variables.extend(color_set.to_scss_variables(prefix, name)); + } + + scss_variables + } + + pub fn to_javascript_object_fields(&self) -> Vec { + let mut js_object_fields = Vec::new(); + let prefix = if self.dark_set { "dark" } else { "light" }; + + js_object_fields.extend(self.primary.to_javascript_object_fields(prefix, "primary")); + js_object_fields.extend( + self.secondary + .to_javascript_object_fields(prefix, "secondary"), + ); + js_object_fields.extend( + self.tertiary + .to_javascript_object_fields(prefix, "tertiary"), + ); + js_object_fields.extend(self.error.to_javascript_object_fields(prefix, "error")); + js_object_fields.extend(self.surface.to_javascript_object_fields(prefix)); + js_object_fields.push(format!("{}Outline: '#{}',", prefix, self.outline)); + js_object_fields.push(format!( + "{}OutlineVariant: '#{}',", + prefix, self.outline_variant + )); + js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim)); + js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadown)); + for (name, color_set) in &self.customs { + js_object_fields.extend(color_set.to_javascript_object_fields(prefix, name)); + } + + js_object_fields + } +} diff --git a/color-module/src/schemes/material_design_3/color_set.rs b/color-module/src/schemes/material_design_3/color_set.rs new file mode 100644 index 0000000..213f633 --- /dev/null +++ b/color-module/src/schemes/material_design_3/color_set.rs @@ -0,0 +1,181 @@ +use serde::Serialize; + +use crate::convert::map_cam16jch_to_srgb_hex; + +use super::tonal_palette::TonalPalette; + +#[derive(Debug, Clone, Serialize)] +pub struct M3ColorSet { + pub root: String, + pub on_root: String, + pub container: String, + pub on_conatiner: String, + pub fixed: String, + pub fixed_dim: String, + pub on_fixed: String, + pub fixed_variant: String, + pub inverse: String, +} + +impl M3ColorSet { + pub fn new_light_set(palette: &TonalPalette) -> Self { + let root = palette.tone(40.0); + let on_root = palette.tone(100.0); + let container = palette.tone(90.0); + let on_container = palette.tone(30.0); + let fixed = palette.tone(90.0); + let fixed_dim = palette.tone(80.0); + let on_fixed = palette.tone(10.0); + let fixed_variant = palette.tone(30.0); + let inverse = palette.tone(80.0); + + Self { + root: map_cam16jch_to_srgb_hex(&root), + on_root: map_cam16jch_to_srgb_hex(&on_root), + container: map_cam16jch_to_srgb_hex(&container), + on_conatiner: map_cam16jch_to_srgb_hex(&on_container), + fixed: map_cam16jch_to_srgb_hex(&fixed), + fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim), + on_fixed: map_cam16jch_to_srgb_hex(&on_fixed), + fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant), + inverse: map_cam16jch_to_srgb_hex(&inverse), + } + } + + pub fn new_dark_set(palette: &TonalPalette) -> Self { + let root = palette.tone(80.0); + let on_root = palette.tone(20.0); + let container = palette.tone(30.0); + let on_container = palette.tone(90.0); + let fixed = palette.tone(90.0); + let fixed_dim = palette.tone(80.0); + let on_fixed = palette.tone(10.0); + let fixed_variant = palette.tone(30.0); + let inverse = palette.tone(40.0); + + Self { + root: map_cam16jch_to_srgb_hex(&root), + on_root: map_cam16jch_to_srgb_hex(&on_root), + container: map_cam16jch_to_srgb_hex(&container), + on_conatiner: map_cam16jch_to_srgb_hex(&on_container), + fixed: map_cam16jch_to_srgb_hex(&fixed), + fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim), + on_fixed: map_cam16jch_to_srgb_hex(&on_fixed), + fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant), + inverse: map_cam16jch_to_srgb_hex(&inverse), + } + } + + pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec { + let mut variable_lines = Vec::new(); + + variable_lines.push(format!("--color-{}-{}: #{};", prefix, name, self.root)); + variable_lines.push(format!( + "--color-{}-on-{}: #{};", + prefix, name, self.on_root + )); + variable_lines.push(format!( + "--color-{}-{}-container: #{};", + prefix, name, self.container + )); + variable_lines.push(format!( + "--color-{}-on-{}-container: #{};", + prefix, name, self.on_conatiner + )); + variable_lines.push(format!( + "--color-{}-{}-fixed: #{};", + prefix, name, self.fixed + )); + variable_lines.push(format!( + "--color-{}-{}-fixed-dim: #{};", + prefix, name, self.fixed_dim + )); + variable_lines.push(format!( + "--color-{}-on-{}-fixed: #{};", + prefix, name, self.on_fixed + )); + variable_lines.push(format!( + "--color-{}-on-{}-fixed-variant: #{};", + prefix, name, self.fixed_variant + )); + variable_lines.push(format!( + "--color-{}-inverse-{}: #{};", + prefix, name, self.inverse + )); + + variable_lines + } + + pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec { + let mut variable_lines = Vec::new(); + + variable_lines.push(format!("$color-{}-{}: #{};", prefix, name, self.root)); + variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on_root)); + variable_lines.push(format!( + "$color-{}-{}-container: #{};", + prefix, name, self.container + )); + variable_lines.push(format!( + "$color-{}-on-{}-container: #{};", + prefix, name, self.on_conatiner + )); + variable_lines.push(format!( + "$color-{}-{}-fixed: #{};", + prefix, name, self.fixed + )); + variable_lines.push(format!( + "$color-{}-{}-fixed-dim: #{};", + prefix, name, self.fixed_dim + )); + variable_lines.push(format!( + "$color-{}-on-{}-fixed: #{};", + prefix, name, self.on_fixed + )); + variable_lines.push(format!( + "$color-{}-on-{}-fixed-variant: #{};", + prefix, name, self.fixed_variant + )); + variable_lines.push(format!( + "$color-{}-inverse-{}: #{};", + prefix, name, self.inverse + )); + + variable_lines + } + + pub fn to_javascript_object_fields(&self, prefix: &str, name: &str) -> Vec { + let mut variable_lines = Vec::new(); + let prefix = prefix.to_ascii_lowercase(); + let name = name + .chars() + .next() + .unwrap() + .to_ascii_uppercase() + .to_string() + + &name[1..]; + + variable_lines.push(format!("{}{}: '#{}',", prefix, name, self.root)); + variable_lines.push(format!("{}on{}: '#{}',", prefix, name, self.on_root)); + variable_lines.push(format!( + "{}{}Container: '#{}',", + prefix, name, self.container + )); + variable_lines.push(format!( + "{}On{}Container: '#{}',", + prefix, name, self.on_conatiner + )); + variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed)); + variable_lines.push(format!( + "{}{}FixedDim: '#{}',", + prefix, name, self.fixed_dim + )); + variable_lines.push(format!("{}On{}Fixed: '#{}',", prefix, name, self.on_fixed)); + variable_lines.push(format!( + "{}On{}FixedVariant: '#{}',", + prefix, name, self.fixed_variant + )); + variable_lines.push(format!("{}Inverse{}: '#{}',", prefix, name, self.inverse)); + + variable_lines + } +} diff --git a/color-module/src/schemes/material_design_3/mod.rs b/color-module/src/schemes/material_design_3/mod.rs new file mode 100644 index 0000000..f97e5ae --- /dev/null +++ b/color-module/src/schemes/material_design_3/mod.rs @@ -0,0 +1,130 @@ +use std::str::FromStr; + +use baseline::M3BaselineColors; +use palette::cam16::{Cam16Jch, Parameters}; +use palette::{IntoColor, Srgb}; +use serde::Serialize; +use tonal_palette::TonalPalette; + +use crate::convert::map_cam16jch_to_srgb_hex; +use crate::errors; + +use super::SchemeExport; + +mod baseline; +mod color_set; +mod surface; +mod tonal_palette; + +#[derive(Debug, Clone, Serialize)] +pub struct MaterialDesign3Scheme { + pub white: String, + pub black: String, + pub light_baseline: M3BaselineColors, + pub dark_baseline: M3BaselineColors, +} + +impl MaterialDesign3Scheme { + pub fn new(source_color: &str, error_color: &str) -> Result { + let source = Cam16Jch::from_xyz( + Srgb::from_str(source_color) + .map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))? + .into_format::() + .into_color(), + Parameters::default_static_wp(40.0), + ); + let error = Cam16Jch::from_xyz( + Srgb::from_str(error_color) + .map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))? + .into_format::() + .into_color(), + Parameters::default_static_wp(40.0), + ); + let source_hue = source.hue.into_positive_degrees(); + let p = TonalPalette::from_hue_and_chroma(source_hue, source.chroma); + let s = TonalPalette::from_hue_and_chroma(source_hue, source.chroma / 3.0); + let t = TonalPalette::from_hue_and_chroma(source_hue + 60.0, source.chroma / 2.0); + let n = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 12.0).min(4.0)); + let nv = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 6.0).min(8.0)); + let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0); + + Ok(Self { + white: map_cam16jch_to_srgb_hex(&Cam16Jch::new(100.0, 0.0, 0.0)), + black: map_cam16jch_to_srgb_hex(&Cam16Jch::new(0.0, 0.0, 0.0)), + light_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, false), + dark_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, true), + }) + } + + pub fn add_custom_color( + &mut self, + name: String, + color: String, + ) -> Result<(), errors::ColorError> { + let custom_color = Cam16Jch::from_xyz( + Srgb::from_str(&color) + .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))? + .into_format::() + .into_color(), + Parameters::default_static_wp(40.0), + ); + let hue = custom_color.hue.into_positive_degrees(); + let palette = TonalPalette::from_hue_and_chroma(hue, custom_color.chroma); + self.light_baseline.add_custom_set(name.clone(), &palette); + self.dark_baseline.add_custom_set(name.clone(), &palette); + Ok(()) + } +} + +impl SchemeExport for MaterialDesign3Scheme { + fn output_css_variables(&self) -> String { + let mut css_variables = Vec::new(); + + css_variables.push(format!("--color-white: #{};", self.white)); + css_variables.push(format!("--color-black: #{};", self.black)); + css_variables.extend(self.light_baseline.to_css_variables()); + css_variables.extend(self.dark_baseline.to_css_variables()); + + css_variables.join("\n") + } + + fn output_scss_variables(&self) -> String { + let mut scss_variables = Vec::new(); + + scss_variables.push(format!("$color-white: #{};", self.white)); + scss_variables.push(format!("$color-black: #{};", self.black)); + scss_variables.extend(self.light_baseline.to_scss_variables()); + scss_variables.extend(self.dark_baseline.to_scss_variables()); + + scss_variables.join("\n") + } + + fn output_javascript_object(&self) -> String { + let mut js_object = Vec::new(); + + js_object.push("const colorScheme = {".to_string()); + js_object.push(format!(" white: '#{}'", self.white)); + js_object.push(format!(" black: '#{}'", self.black)); + js_object.push(" light: {".to_string()); + js_object.extend( + self.light_baseline + .to_javascript_object_fields() + .into_iter() + .map(|s| format!(" {}", s)) + .collect::>(), + ); + js_object.push(" },".to_string()); + js_object.push(" dark: {".to_string()); + js_object.extend( + self.dark_baseline + .to_javascript_object_fields() + .into_iter() + .map(|s| format!(" {}", s)) + .collect::>(), + ); + js_object.push(" },".to_string()); + js_object.push("}".to_string()); + + js_object.join(",\n") + } +} diff --git a/color-module/src/schemes/material_design_3/surface.rs b/color-module/src/schemes/material_design_3/surface.rs new file mode 100644 index 0000000..55874f8 --- /dev/null +++ b/color-module/src/schemes/material_design_3/surface.rs @@ -0,0 +1,215 @@ +use serde::Serialize; + +use crate::convert::map_cam16jch_to_srgb_hex; + +use super::tonal_palette::TonalPalette; + +#[derive(Debug, Clone, Serialize)] +pub struct M3SurfaceSet { + pub root: String, + pub dim: String, + pub bright: String, + pub container: String, + pub container_lowest: String, + pub container_low: String, + pub container_high: String, + pub container_highest: String, + pub on_root: String, + pub on_root_variant: String, + pub inverse: String, + pub on_inverse: String, +} + +impl M3SurfaceSet { + pub fn new_light_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self { + let root = neutral.tone(98.0); + let dim = neutral.tone(87.0); + let bright = neutral.tone(98.0); + let container = neutral.tone(94.0); + let container_lowest = neutral.tone(100.0); + let container_low = neutral.tone(96.0); + let container_high = neutral.tone(92.0); + let container_highest = neutral.tone(90.0); + let on_root = neutral_variant.tone(10.0); + let on_root_variant = neutral_variant.tone(30.0); + let inverse = neutral.tone(20.0); + let on_inverse = neutral_variant.tone(95.0); + + Self { + root: map_cam16jch_to_srgb_hex(&root), + dim: map_cam16jch_to_srgb_hex(&dim), + bright: map_cam16jch_to_srgb_hex(&bright), + container: map_cam16jch_to_srgb_hex(&container), + container_lowest: map_cam16jch_to_srgb_hex(&container_lowest), + container_low: map_cam16jch_to_srgb_hex(&container_low), + container_high: map_cam16jch_to_srgb_hex(&container_high), + container_highest: map_cam16jch_to_srgb_hex(&container_highest), + on_root: map_cam16jch_to_srgb_hex(&on_root), + on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant), + inverse: map_cam16jch_to_srgb_hex(&inverse), + on_inverse: map_cam16jch_to_srgb_hex(&on_inverse), + } + } + + pub fn new_dark_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self { + let root = neutral.tone(6.0); + let dim = neutral.tone(6.0); + let bright = neutral.tone(24.0); + let container = neutral.tone(12.0); + let container_lowest = neutral.tone(4.0); + let container_low = neutral.tone(10.0); + let container_high = neutral.tone(17.0); + let container_highest = neutral.tone(22.0); + let on_root = neutral_variant.tone(90.0); + let on_root_variant = neutral_variant.tone(80.0); + let inverse = neutral.tone(90.0); + let on_inverse = neutral_variant.tone(20.0); + + Self { + root: map_cam16jch_to_srgb_hex(&root), + dim: map_cam16jch_to_srgb_hex(&dim), + bright: map_cam16jch_to_srgb_hex(&bright), + container: map_cam16jch_to_srgb_hex(&container), + container_lowest: map_cam16jch_to_srgb_hex(&container_lowest), + container_low: map_cam16jch_to_srgb_hex(&container_low), + container_high: map_cam16jch_to_srgb_hex(&container_high), + container_highest: map_cam16jch_to_srgb_hex(&container_highest), + on_root: map_cam16jch_to_srgb_hex(&on_root), + on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant), + inverse: map_cam16jch_to_srgb_hex(&inverse), + on_inverse: map_cam16jch_to_srgb_hex(&on_inverse), + } + } + + pub fn to_css_variables(&self, prefix: &str) -> Vec { + let mut css_variables = Vec::new(); + + css_variables.push(format!("--color-{}-surface: ${};", prefix, self.root)); + css_variables.push(format!("--color-{}-surface-dim: ${};", prefix, self.dim)); + css_variables.push(format!( + "--color-{}-surface-bright: ${};", + prefix, self.bright + )); + css_variables.push(format!( + "--color-{}-surface-container: ${};", + prefix, self.container + )); + css_variables.push(format!( + "--color-{}-surface-container-lowest: ${};", + prefix, self.container_lowest + )); + css_variables.push(format!( + "--color-{}-surface-container-low: ${};", + prefix, self.container_low + )); + css_variables.push(format!( + "--color-{}-surface-container-high: ${};", + prefix, self.container_high + )); + css_variables.push(format!( + "--color-{}-surface-container-highest: ${};", + prefix, self.container_highest + )); + css_variables.push(format!("--color-{}-on-surface: ${};", prefix, self.on_root)); + css_variables.push(format!( + "--color-{}-on-surface-variant: ${};", + prefix, self.on_root_variant + )); + css_variables.push(format!( + "--color-{}-inverse-surface: ${};", + prefix, self.inverse + )); + css_variables.push(format!( + "--color-{}-inverse-on-surface: ${};", + prefix, self.on_inverse + )); + + css_variables + } + + pub fn to_scss_variables(&self, prefix: &str) -> Vec { + let mut scss_variables = Vec::new(); + + scss_variables.push(format!("$color-{}-surface: ${};", prefix, self.root)); + scss_variables.push(format!("$color-{}-surface-dim: ${};", prefix, self.dim)); + scss_variables.push(format!( + "$color-{}-surface-bright: ${};", + prefix, self.bright + )); + scss_variables.push(format!( + "$color-{}-surface-container: ${};", + prefix, self.container + )); + scss_variables.push(format!( + "$color-{}-surface-container-lowest: ${};", + prefix, self.container_lowest + )); + scss_variables.push(format!( + "$color-{}-surface-container-low: ${};", + prefix, self.container_low + )); + scss_variables.push(format!( + "$color-{}-surface-container-high: ${};", + prefix, self.container_high + )); + scss_variables.push(format!( + "$color-{}-surface-container-highest: ${};", + prefix, self.container_highest + )); + scss_variables.push(format!("$color-{}-on-surface: ${};", prefix, self.on_root)); + scss_variables.push(format!( + "$color-{}-on-surface-variant: ${};", + prefix, self.on_root_variant + )); + scss_variables.push(format!( + "$color-{}-inverse-surface: ${};", + prefix, self.inverse + )); + scss_variables.push(format!( + "$color-{}-inverse-on-surface: ${};", + prefix, self.on_inverse + )); + + scss_variables + } + + pub fn to_javascript_object_fields(&self, prefix: &str) -> Vec { + let mut js_object_fields = Vec::new(); + + js_object_fields.push(format!("{}Surface: '#{}',", prefix, self.root)); + js_object_fields.push(format!("{}SurfaceDim: '#{}',", prefix, self.dim)); + js_object_fields.push(format!("{}SurfaceBright: '#{}',", prefix, self.bright)); + js_object_fields.push(format!( + "{}SurfaceContainer: '#{}',", + prefix, self.container + )); + js_object_fields.push(format!( + "{}SurfaceContainerLowest: '#{}',", + prefix, self.container_lowest + )); + js_object_fields.push(format!( + "{}SurfaceContainerLow: '#{}',", + prefix, self.container_low + )); + js_object_fields.push(format!( + "{}SurfaceContainerHigh: '#{}',", + prefix, self.container_high + )); + js_object_fields.push(format!( + "{}SurfaceContainerHighest: '#{}',", + prefix, self.container_highest + )); + js_object_fields.push(format!("{}OnSurface: '#{}',", prefix, self.on_root)); + js_object_fields.push(format!( + "{}OnSurfaceVariant: '#{}',", + prefix, self.on_root_variant + )); + js_object_fields.push(format!("{}InverseSurface: '#{}',", prefix, self.inverse)); + js_object_fields.push(format!( + "{}InverseOnSurface: '#{}',", + prefix, self.on_inverse + )); + + js_object_fields + } +} diff --git a/color-module/src/schemes/material_design_3/tonal_palette.rs b/color-module/src/schemes/material_design_3/tonal_palette.rs new file mode 100644 index 0000000..9f5ed2f --- /dev/null +++ b/color-module/src/schemes/material_design_3/tonal_palette.rs @@ -0,0 +1,95 @@ +use std::str::FromStr; + +use palette::{ + cam16::{Cam16Jch, Parameters}, + IntoColor, Srgb, +}; + +use crate::errors; + +#[derive(Debug, Clone)] +pub struct TonalPalette { + pub key_color: Cam16Jch, +} + +#[inline] +fn approximately_equal(a: f32, b: f32) -> bool { + const EPSILON: f32 = 0.000001; + (a - b).abs() < EPSILON +} + +#[inline] +fn find_max_chroma(cache: &mut Vec<(f32, f32)>, hue: f32, tone: f32) -> f32 { + for (k, v) in cache.iter() { + if approximately_equal(*k, tone) { + return *v; + } + } + let chroma = Cam16Jch::new(tone, 200.0, hue).chroma; + cache.push((tone, chroma)); + chroma +} + +fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch { + let mut max_chroma_cache = Vec::new(); + let hue = if hue >= 360.0 { hue - 360.0 } else { hue }; + const PIVOT_TONE: f32 = 50.0; + const TONE_STEP_SIZE: f32 = 1.0; + const EPSILON: f32 = 0.01; + + let mut lower_tone = 0.0_f32; + let mut upper_tone = 100.0_f32; + while lower_tone < upper_tone { + let mid_tone = ((lower_tone + upper_tone) / 2.0).floor(); + let is_ascending = find_max_chroma(&mut max_chroma_cache, hue, mid_tone) + < find_max_chroma(&mut max_chroma_cache, hue, mid_tone + TONE_STEP_SIZE); + let sufficient_chroma = + find_max_chroma(&mut max_chroma_cache, hue, mid_tone) >= chroma - EPSILON; + + if sufficient_chroma { + if (lower_tone - PIVOT_TONE).abs() < (upper_tone - PIVOT_TONE).abs() { + upper_tone = mid_tone; + } else { + if approximately_equal(lower_tone, mid_tone) { + return Cam16Jch::new(lower_tone, chroma, hue); + } + lower_tone = mid_tone; + } + } else { + if is_ascending { + lower_tone = mid_tone + TONE_STEP_SIZE; + } else { + upper_tone = mid_tone; + } + } + } + Cam16Jch::new(lower_tone, chroma, hue) +} + +impl TryFrom for TonalPalette { + type Error = errors::ColorError; + + fn try_from(value: String) -> Result { + let key_color = Cam16Jch::from_xyz( + Srgb::from_str(&value) + .map_err(|_| errors::ColorError::UnrecogniazedRGB(value))? + .into_format::() + .into_color(), + Parameters::default_static_wp(40.0), + ); + Ok(TonalPalette { key_color }) + } +} + +impl TonalPalette { + pub fn from_hue_and_chroma(hue: f32, chroma: f32) -> Self { + let key_color = from_hue_and_chroma(hue, chroma); + TonalPalette { key_color } + } + + pub fn tone(&self, tone: f32) -> Cam16Jch { + let toned_color = Cam16Jch::new(tone, self.key_color.chroma, self.key_color.hue); + + toned_color + } +} diff --git a/color-module/src/schemes/mod.rs b/color-module/src/schemes/mod.rs new file mode 100644 index 0000000..01a6a23 --- /dev/null +++ b/color-module/src/schemes/mod.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; + +use material_design_3::MaterialDesign3Scheme; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +use crate::errors; + +pub mod material_design_3; + +pub trait SchemeExport { + fn output_css_variables(&self) -> String; + fn output_scss_variables(&self) -> String; + fn output_javascript_object(&self) -> String; +} + +#[wasm_bindgen] +pub fn generate_material_design_3_scheme( + source_color: &str, + error_color: &str, + custom_colors: JsValue, +) -> Result { + let custom_colors: HashMap = serde_wasm_bindgen::from_value(custom_colors) + .map_err(|_| errors::ColorError::UnableToParseArgument)?; + let mut scheme = MaterialDesign3Scheme::new(source_color, error_color)?; + for (name, color) in custom_colors { + scheme.add_custom_color(name, color)?; + } + Ok(serde_wasm_bindgen::to_value(&( + scheme.clone(), + scheme.output_css_variables(), + scheme.output_scss_variables(), + scheme.output_javascript_object(), + )) + .map_err(|_| errors::ColorError::UnableToAssembleOutput)?) +}