From 715459eef05c7a454393213fbe74c7550c86f4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Thu, 13 Feb 2025 14:27:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0M3=E5=8A=A8=E6=80=81=E9=85=8D?= =?UTF-8?q?=E8=89=B2Scheme=E7=9A=84=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- color-module/src/lib.rs | 11 + .../src/schemes/material_design_3/baseline.rs | 28 + .../src/schemes/material_design_3/mod.rs | 15 +- .../src/schemes/material_design_3/surface.rs | 14 + .../material_design_3_dynamic/constants.rs | 363 ++++++++ .../material_design_3_dynamic/contrast.rs | 102 +++ .../contrast_curve.rs | 37 + .../dynamic_color.rs | 239 +++++ .../dynamic_scheme.rs | 71 ++ .../material_colors.rs | 867 ++++++++++++++++++ .../schemes/material_design_3_dynamic/mod.rs | 187 ++++ color-module/src/schemes/mod.rs | 47 + color-module/src/theory.rs | 30 + 13 files changed, 2009 insertions(+), 2 deletions(-) create mode 100644 color-module/src/schemes/material_design_3_dynamic/constants.rs create mode 100644 color-module/src/schemes/material_design_3_dynamic/contrast.rs create mode 100644 color-module/src/schemes/material_design_3_dynamic/contrast_curve.rs create mode 100644 color-module/src/schemes/material_design_3_dynamic/dynamic_color.rs create mode 100644 color-module/src/schemes/material_design_3_dynamic/dynamic_scheme.rs create mode 100644 color-module/src/schemes/material_design_3_dynamic/material_colors.rs create mode 100644 color-module/src/schemes/material_design_3_dynamic/mod.rs diff --git a/color-module/src/lib.rs b/color-module/src/lib.rs index f7907d6..9e81756 100644 --- a/color-module/src/lib.rs +++ b/color-module/src/lib.rs @@ -135,3 +135,14 @@ pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result(); Ok(fg_srgb.relative_contrast(bg_srgb)) } + +#[macro_export] +macro_rules! cond { + ($s: expr, $a: expr, $b: expr) => { + if $s { + $a + } else { + $b + } + }; +} diff --git a/color-module/src/schemes/material_design_3/baseline.rs b/color-module/src/schemes/material_design_3/baseline.rs index 92b643b..ea508d3 100644 --- a/color-module/src/schemes/material_design_3/baseline.rs +++ b/color-module/src/schemes/material_design_3/baseline.rs @@ -80,6 +80,34 @@ impl M3BaselineColors { self.customs.insert(name, color_set_generator(c)); } + pub fn full_custom( + primary: M3ColorSet, + secondary: M3ColorSet, + tertiary: M3ColorSet, + error: M3ColorSet, + surface: M3SurfaceSet, + outline: String, + outline_variant: String, + scrim: String, + shadow: String, + customs: HashMap, + dark_set: bool, + ) -> Self { + Self { + primary, + secondary, + tertiary, + error, + surface, + outline, + outline_variant, + scrim, + shadow, + customs, + dark_set, + } + } + pub fn to_css_variables(&self) -> Vec { let mut css_variables = Vec::new(); let prefix = if self.dark_set { "dark" } else { "light" }; diff --git a/color-module/src/schemes/material_design_3/mod.rs b/color-module/src/schemes/material_design_3/mod.rs index d590326..f0518a9 100644 --- a/color-module/src/schemes/material_design_3/mod.rs +++ b/color-module/src/schemes/material_design_3/mod.rs @@ -1,9 +1,11 @@ use std::str::FromStr; -use baseline::M3BaselineColors; +pub use baseline::M3BaselineColors; +pub use color_set::M3ColorSet; use palette::{IntoColor, Lch, Srgb}; use serde::Serialize; -use tonal_palette::TonalPalette; +pub use surface::M3SurfaceSet; +pub use tonal_palette::TonalPalette; use crate::convert::map_lch_to_srgb_hex; use crate::errors; @@ -64,6 +66,15 @@ impl MaterialDesign3Scheme { self.dark_baseline.add_custom_set(name.clone(), &palette); Ok(()) } + + pub fn full_custom(light_baseline: M3BaselineColors, dark_baseline: M3BaselineColors) -> Self { + Self { + white: map_lch_to_srgb_hex(&Lch::new(100.0, 0.0, 0.0)), + black: map_lch_to_srgb_hex(&Lch::new(0.0, 0.0, 0.0)), + light_baseline, + dark_baseline, + } + } } impl SchemeExport for MaterialDesign3Scheme { diff --git a/color-module/src/schemes/material_design_3/surface.rs b/color-module/src/schemes/material_design_3/surface.rs index 972687c..f1907b6 100644 --- a/color-module/src/schemes/material_design_3/surface.rs +++ b/color-module/src/schemes/material_design_3/surface.rs @@ -9,6 +9,7 @@ pub struct M3SurfaceSet { pub root: String, pub dim: String, pub bright: String, + pub variant: String, pub container: String, pub container_lowest: String, pub container_low: String, @@ -25,6 +26,7 @@ impl M3SurfaceSet { let root = neutral.tone(98.0); let dim = neutral.tone(87.0); let bright = neutral.tone(98.0); + let variant = neutral_variant.tone(90.0); let container = neutral.tone(94.0); let container_lowest = neutral.tone(100.0); let container_low = neutral.tone(96.0); @@ -39,6 +41,7 @@ impl M3SurfaceSet { root: map_lch_to_srgb_hex(&root), dim: map_lch_to_srgb_hex(&dim), bright: map_lch_to_srgb_hex(&bright), + variant: map_lch_to_srgb_hex(&variant), container: map_lch_to_srgb_hex(&container), container_lowest: map_lch_to_srgb_hex(&container_lowest), container_low: map_lch_to_srgb_hex(&container_low), @@ -55,6 +58,7 @@ impl M3SurfaceSet { let root = neutral.tone(6.0); let dim = neutral.tone(6.0); let bright = neutral.tone(24.0); + let variant = neutral_variant.tone(30.0); let container = neutral.tone(12.0); let container_lowest = neutral.tone(4.0); let container_low = neutral.tone(10.0); @@ -69,6 +73,7 @@ impl M3SurfaceSet { root: map_lch_to_srgb_hex(&root), dim: map_lch_to_srgb_hex(&dim), bright: map_lch_to_srgb_hex(&bright), + variant: map_lch_to_srgb_hex(&variant), container: map_lch_to_srgb_hex(&container), container_lowest: map_lch_to_srgb_hex(&container_lowest), container_low: map_lch_to_srgb_hex(&container_low), @@ -90,6 +95,10 @@ impl M3SurfaceSet { "--color-{}-surface-bright: #{};", prefix, self.bright )); + css_variables.push(format!( + "--color-{}-surface-variant: #{};", + prefix, self.variant + )); css_variables.push(format!( "--color-{}-surface-container: #{};", prefix, self.container @@ -136,6 +145,10 @@ impl M3SurfaceSet { "$color-{}-surface-bright: #{};", prefix, self.bright )); + scss_variables.push(format!( + "$color-{}-surface-variant: #{};", + prefix, self.variant + )); scss_variables.push(format!( "$color-{}-surface-container: #{};", prefix, self.container @@ -179,6 +192,7 @@ impl M3SurfaceSet { 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!("{}SurfaceVariant: '#{}',", prefix, self.variant)); js_object_fields.push(format!( "{}SurfaceContainer: '#{}',", prefix, self.container diff --git a/color-module/src/schemes/material_design_3_dynamic/constants.rs b/color-module/src/schemes/material_design_3_dynamic/constants.rs new file mode 100644 index 0000000..8786bd9 --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/constants.rs @@ -0,0 +1,363 @@ +use enum_iterator::Sequence; +use palette::{ + color_theory::{Analogous, Complementary}, + Lch, +}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use strum::Display; +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::{ + schemes::material_design_3::TonalPalette, + theory::{harmonize_hue, sanitize_hue_degrees}, +}; + +use super::dynamic_color::CustomPaletteGenerator; + +#[derive(Debug, Clone, Copy, PartialEq, Display, Sequence, Serialize_repr, Deserialize_repr)] +#[wasm_bindgen] +#[repr(u8)] +pub enum Variant { + Monochrome, + Neutral, + TonalSpot, + Vibrant, + Expressive, + Fidelity, + Content, + Rainbow, + FruitSalad, +} + +impl Variant { + pub fn label(&self) -> String { + match self { + Variant::Monochrome => "Monochrome".to_string(), + Variant::Neutral => "Neutral".to_string(), + Variant::TonalSpot => "Tonal Spot".to_string(), + Variant::Vibrant => "Vibrant".to_string(), + Variant::Expressive => "Expressive".to_string(), + Variant::Fidelity => "Fidelity".to_string(), + Variant::Content => "Content".to_string(), + Variant::Rainbow => "Rainbow".to_string(), + Variant::FruitSalad => "Fruit Salad".to_string(), + } + } + pub fn hues(&self) -> Vec { + match self { + Variant::Vibrant => vec![0.0, 41.0, 61.0, 101.0, 131.0, 181.0, 251.0, 301.0, 360.0], + Variant::Expressive => vec![0.0, 21.0, 51.0, 121.0, 151.0, 191.0, 271.0, 321.0, 360.0], + _ => vec![], + } + } + + pub fn secondary_rotation(&self) -> Vec { + match self { + Variant::Vibrant => vec![18.0, 15.0, 10.0, 12.0, 15.0, 18.0, 15.0, 12.0, 12.0], + Variant::Expressive => vec![45.0, 95.0, 45.0, 20.0, 45.0, 90.0, 45.0, 45.0, 45.0], + _ => vec![], + } + } + + pub fn tertiary_rotation(&self) -> Vec { + match self { + Variant::Vibrant => vec![35.0, 30.0, 20.0, 25.0, 30.0, 35.0, 30.0, 25.0, 25.0], + Variant::Expressive => vec![120.0, 120.0, 20.0, 45.0, 20.0, 15.0, 20.0, 120.0, 120.0], + _ => vec![], + } + } + + pub fn build_palette( + &self, + source_color: Lch, + harmonize_customs: bool, + ) -> ( + TonalPalette, + TonalPalette, + TonalPalette, + TonalPalette, + TonalPalette, + CustomPaletteGenerator, + ) { + match self { + Variant::Monochrome => ( + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 0.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 0.0) + }) + }, + ), + Variant::Neutral => ( + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 12.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 8.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 2.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 2.0), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 12.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 12.0) + }) + }, + ), + Variant::TonalSpot => ( + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 36.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0), + TonalPalette::from_hue_and_chroma( + sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 60.0), + 24.0, + ), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 6.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 8.0), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 36.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 36.0) + }) + }, + ), + Variant::Vibrant => ( + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 200.0), + TonalPalette::from_hue_and_chroma( + rotate_hue(&source_color, &self.hues(), &self.secondary_rotation()), + 24.0, + ), + TonalPalette::from_hue_and_chroma( + rotate_hue(&source_color, &self.hues(), &self.tertiary_rotation()), + 32.0, + ), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 10.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 12.0), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 200.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 200.0) + }) + }, + ), + Variant::Expressive => ( + TonalPalette::from_hue_and_chroma( + sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 240.0), + 40.0, + ), + TonalPalette::from_hue_and_chroma( + rotate_hue(&source_color, &self.hues(), &self.secondary_rotation()), + 24.0, + ), + TonalPalette::from_hue_and_chroma( + rotate_hue(&source_color, &self.hues(), &self.tertiary_rotation()), + 32.0, + ), + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees() + 15.0, + 8.0, + ), + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees() + 15.0, + 12.0, + ), + if harmonize_customs { + let source_hue = + sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 240.0); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 40.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 40.0) + }) + }, + ), + Variant::Fidelity => ( + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + source_color.chroma, + ), + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + (source_color.chroma - 32.0).max(source_color.chroma * 0.5), + ), + TonalPalette { + key_color: fix_disliked(&source_color.complementary()), + }, + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + source_color.chroma / 8.0, + ), + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + source_color.chroma / 8.0 + 4.0, + ), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + let source_chroma = source_color.chroma; + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + source_chroma, + ) + }) + } else { + let source_chroma = source_color.chroma; + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + c.hue.into_positive_degrees(), + source_chroma, + ) + }) + }, + ), + Variant::Content => ( + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + source_color.chroma, + ), + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + (source_color.chroma - 32.0).max(source_color.chroma * 0.5), + ), + TonalPalette { + key_color: fix_disliked(&source_color.analogous().1), + }, + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + source_color.chroma / 8.0, + ), + TonalPalette::from_hue_and_chroma( + source_color.hue.into_positive_degrees(), + source_color.chroma / 8.0 + 4.0, + ), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + c.chroma, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), c.chroma) + }) + }, + ), + Variant::Rainbow => ( + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 48.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0), + TonalPalette::from_hue_and_chroma( + sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 60.0), + 24.0, + ), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0), + if harmonize_customs { + let source_hue = source_color.hue.into_positive_degrees(); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 48.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 48.0) + }) + }, + ), + Variant::FruitSalad => ( + TonalPalette::from_hue_and_chroma( + sanitize_hue_degrees(source_color.hue.into_positive_degrees() - 50.0), + 48.0, + ), + TonalPalette::from_hue_and_chroma( + sanitize_hue_degrees(source_color.hue.into_positive_degrees() - 50.0), + 36.0, + ), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 36.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 10.0), + TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0), + if harmonize_customs { + let source_hue = + sanitize_hue_degrees(source_color.hue.into_positive_degrees() - 50.0); + Box::new(move |c: &Lch| { + TonalPalette::from_hue_and_chroma( + harmonize_hue(c.hue.into_positive_degrees(), source_hue), + 48.0, + ) + }) + } else { + Box::new(|c: &Lch| { + TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 48.0) + }) + }, + ), + } + } +} + +fn rotate_hue(source: &Lch, hues: &Vec, rotations: &Vec) -> f32 { + let source_hue = source.hue.into_positive_degrees(); + if rotations.len() == 1 { + return sanitize_hue_degrees(source_hue + rotations[0]); + } + let hues_size = hues.len(); + for i in 0..=hues_size - 2 { + let hue = hues[i]; + let next_hue = hues[i + 1]; + if hue < source_hue && source_hue < next_hue { + return sanitize_hue_degrees(source_hue + rotations[i]); + } + } + + source_hue +} + +pub fn fix_disliked(color: &Lch) -> Lch { + let hue = color.hue.into_positive_degrees().round() >= 90.0 + && color.hue.into_positive_degrees().round() <= 111.0; + let chroma = color.chroma.round() > 16.0; + let lightness = color.l.round() < 65.0; + + if hue && chroma && lightness { + Lch::new(70.0, color.chroma, color.hue) + } else { + color.clone() + } +} diff --git a/color-module/src/schemes/material_design_3_dynamic/contrast.rs b/color-module/src/schemes/material_design_3_dynamic/contrast.rs new file mode 100644 index 0000000..64683ce --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/contrast.rs @@ -0,0 +1,102 @@ +fn lab_inv_f(ft: f32) -> f32 { + let e = 216.0 / 24389.0; + let k = 24389.0 / 27.0; + let ft3 = ft * ft * ft; + if ft3 > e { + ft3 + } else { + (116.0 * ft - 16.0) / k + } +} + +fn lab_f(t: f32) -> f32 { + let e = 216.0 / 24389.0; + let k = 24389.0 / 27.0; + if t > e { + t.powf(1.0 / 3.0) + } else { + (k * t + 16.0) / 116.0 + } +} + +#[inline] +fn y_from_lstar(lstar: f32) -> f32 { + 100.0 * lab_inv_f((lstar + 16.0) / 116.0) +} + +#[inline] +fn lstar_from_y(y: f32) -> f32 { + lab_f(y / 100.0) * 116.0 - 16.0 +} + +pub fn ratio_of_ys(y1: f32, y2: f32) -> f32 { + let lighter = y1.max(y2); + let darker = y1.min(y2); + (lighter + 5.0) / (darker + 5.0) +} + +pub fn ratio_of_tones(a: f32, b: f32) -> f32 { + let tone_a = a.clamp(0.0, 100.0); + let tone_b = b.clamp(0.0, 100.0); + ratio_of_ys(y_from_lstar(tone_a), y_from_lstar(tone_b)) +} + +pub fn lighter(tone: f32, ratio: f32) -> f32 { + if tone < 0.0 || tone > 100.0 { + return -1.0; + } + + let dark_y = y_from_lstar(tone); + let light_y = ratio * (dark_y + 5.0) - 5.0; + let real_contrast = ratio_of_ys(light_y, dark_y); + let delta = (real_contrast - ratio).abs(); + if real_contrast < ratio && delta > 0.04 { + return -1.0; + } + + let return_value = lstar_from_y(light_y) + 0.4; + if return_value < 0.0 || return_value > 100.0 { + return -1.0; + } + return_value +} + +pub fn darker(tone: f32, ratio: f32) -> f32 { + if tone < 0.0 || tone > 100.0 { + return -1.0; + } + + let light_y = y_from_lstar(tone); + let dark_y = ((light_y + 5.0) / ratio) - 5.0; + let real_contrast = ratio_of_ys(light_y, dark_y); + + let delta = (real_contrast - ratio).abs(); + + if real_contrast < ratio && delta > 0.04 { + return -1.0; + } + + let return_value = lstar_from_y(dark_y) - 0.4; + if return_value < 0.0 || return_value > 100.0 { + return -1.0; + } + return_value +} + +pub fn unsafe_lighter(tone: f32, ratio: f32) -> f32 { + let safe_lighter = lighter(tone, ratio); + if safe_lighter < 0.0 { + 100.0 + } else { + safe_lighter + } +} + +pub fn unsafe_darker(tone: f32, ratio: f32) -> f32 { + let safe_darker = darker(tone, ratio); + if safe_darker < 0.0 { + 0.0 + } else { + safe_darker + } +} diff --git a/color-module/src/schemes/material_design_3_dynamic/contrast_curve.rs b/color-module/src/schemes/material_design_3_dynamic/contrast_curve.rs new file mode 100644 index 0000000..3fbe60f --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/contrast_curve.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Clone, Copy)] +pub struct ContrastCurve { + low: f32, + normal: f32, + medium: f32, + high: f32, +} + +#[inline] +fn lerp(start: f32, stop: f32, amount: f32) -> f32 { + (1.0 - start) * start + amount * stop +} + +impl ContrastCurve { + pub fn new(low: f32, normal: f32, medium: f32, high: f32) -> Self { + Self { + low, + normal, + medium, + high, + } + } + + pub fn get(&self, contrast_level: f32) -> f32 { + if contrast_level <= -1.0 { + self.low + } else if contrast_level < 0.0 { + lerp(self.low, self.normal, (contrast_level + 1.0) / 1.0) + } else if contrast_level < 0.5 { + lerp(self.normal, self.medium, contrast_level / 0.5) + } else if contrast_level < 1.0 { + lerp(self.medium, self.high, (contrast_level - 0.5) / 0.5) + } else { + self.high + } + } +} diff --git a/color-module/src/schemes/material_design_3_dynamic/dynamic_color.rs b/color-module/src/schemes/material_design_3_dynamic/dynamic_color.rs new file mode 100644 index 0000000..24db217 --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/dynamic_color.rs @@ -0,0 +1,239 @@ +use palette::Lch; + +use crate::schemes::material_design_3::TonalPalette; + +use super::{contrast_curve::ContrastCurve, dynamic_scheme::DynamicScheme}; + +pub type TonalPaletteGenerator = Box TonalPalette>; +pub type ToneSearcher = Box f32>; +pub type DynamicColorSearcher = Box DynamicColor>; +pub type ToneDeltaPairGenerator = Box ToneDeltaPair>; +pub type CustomPaletteGenerator = Box TonalPalette>; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(dead_code)] +pub enum TonePolarity { + Darker, + Lighter, + Nearer, + Farther, +} + +pub struct DynamicColor { + name: String, + palette: TonalPaletteGenerator, + tone: ToneSearcher, + is_background: Option, + background: Option, + secondary_background: Option, + contrast_curve: Option, + tone_delta_pairs: Option, +} + +pub struct ToneDeltaPair { + pub role_a: DynamicColor, + pub role_b: DynamicColor, + pub delta: f32, + pub polarity: TonePolarity, + pub togather: bool, +} + +impl DynamicColor { + pub fn new( + name: Option<&str>, + palette: TonalPaletteGenerator, + tone: ToneSearcher, + is_background: Option, + background: Option, + secondary_background: Option, + contrast_curve: Option, + tone_delta_pairs: Option, + ) -> Self { + DynamicColor { + name: name.unwrap_or("").to_string(), + palette, + tone, + is_background: is_background.or(Some(false)), + background, + secondary_background, + contrast_curve, + tone_delta_pairs, + } + } + + pub fn get_lch(&self, scheme: &DynamicScheme) -> Lch { + let tone = self.get_tone(scheme); + (self.palette)(scheme).tone(tone) + } + + pub fn get_tone(&self, scheme: &DynamicScheme) -> f32 { + let decreasing_contrast = scheme.contrast_level < 0.0; + + if let Some(pair_generator) = &self.tone_delta_pairs { + let tone_delta = pair_generator(scheme); + let bg = (self.background.as_ref().unwrap())(scheme); + let bg_tone = bg.get_tone(scheme); + let is_nearer = tone_delta.polarity == TonePolarity::Nearer + || (tone_delta.polarity == TonePolarity::Lighter && !scheme.is_dark) + || (tone_delta.polarity == TonePolarity::Darker && scheme.is_dark); + let (nearer, farther) = if is_nearer { + (&tone_delta.role_a, &tone_delta.role_b) + } else { + (&tone_delta.role_b, &tone_delta.role_a) + }; + let expansion_factor = if scheme.is_dark { 1.0 } else { -1.0 }; + + let n_contrast = (nearer.contrast_curve.as_ref().unwrap()).get(scheme.contrast_level); + let f_contrast = (farther.contrast_curve.as_ref().unwrap()).get(scheme.contrast_level); + + let n_initial_tone = nearer.get_tone(scheme); + let mut n_tone = + if super::contrast::ratio_of_tones(bg_tone, n_initial_tone) >= n_contrast { + n_initial_tone + } else { + foreground_tone(bg_tone, n_contrast) + }; + let f_initial_tone = farther.get_tone(scheme); + let mut f_tone = + if super::contrast::ratio_of_tones(bg_tone, f_initial_tone) >= f_contrast { + f_initial_tone + } else { + foreground_tone(bg_tone, f_contrast) + }; + + if decreasing_contrast { + n_tone = foreground_tone(bg_tone, n_contrast); + f_tone = foreground_tone(bg_tone, f_contrast); + } + + if (f_tone - n_tone) * expansion_factor < tone_delta.delta { + f_tone = (n_tone + tone_delta.delta * expansion_factor).clamp(0.0, 100.0); + } + + if n_tone >= 50.0 && n_tone < 60.0 { + if expansion_factor > 0.0 { + n_tone = 60.0; + f_tone = (n_tone + tone_delta.delta * expansion_factor).max(f_tone); + } else { + n_tone = 49.0; + f_tone = (n_tone + tone_delta.delta * expansion_factor).min(f_tone); + } + } else if f_tone >= 50.0 && f_tone < 60.0 { + if tone_delta.togather { + if expansion_factor > 0.0 { + n_tone = 60.0; + f_tone = (n_tone + tone_delta.delta * expansion_factor).max(f_tone); + } else { + n_tone = 49.0; + f_tone = (n_tone + tone_delta.delta * expansion_factor).min(f_tone); + } + } else { + if expansion_factor > 0.0 { + f_tone = 60.0; + } else { + f_tone = 49.0; + } + } + } + + if self.name.eq_ignore_ascii_case(&nearer.name) { + n_tone + } else { + f_tone + } + } else { + let mut result = (self.tone)(scheme); + + if self.background.is_none() { + return result; + } + + let bg_tone = (self.background.as_ref().unwrap())(scheme).get_tone(scheme); + let desired_ratio = (self.contrast_curve.as_ref().unwrap()).get(scheme.contrast_level); + + if super::contrast::ratio_of_tones(bg_tone, result) < desired_ratio { + result = foreground_tone(bg_tone, desired_ratio); + } + if decreasing_contrast { + result = foreground_tone(bg_tone, desired_ratio); + } + + if self.is_background.unwrap_or(false) && result >= 50.0 && result < 60.0 { + if super::contrast::ratio_of_tones(49.0, bg_tone) >= desired_ratio { + result = 49.0; + } else { + result = 60.0; + } + } + + if let Some(secondary_background) = &self.secondary_background { + let (bg_tone_1, bg_tone_2) = ( + (self.background.as_ref().unwrap())(scheme).get_tone(scheme), + secondary_background(scheme).get_tone(scheme), + ); + let (upper, lower) = (bg_tone_1.max(bg_tone_2), bg_tone_1.min(bg_tone_2)); + + if super::contrast::ratio_of_tones(upper, result) >= desired_ratio + && super::contrast::ratio_of_tones(lower, result) >= desired_ratio + { + return result; + } + + let light_option = super::contrast::lighter(upper, desired_ratio); + let dark_option = super::contrast::darker(lower, desired_ratio); + + let mut availables = vec![]; + if light_option != -1.0 { + availables.push(light_option); + } + if dark_option != -1.0 { + availables.push(dark_option); + } + + if prefer_light_foreground(bg_tone_1) || prefer_light_foreground(bg_tone_2) { + return if light_option < 0.0 { + 100.0 + } else { + light_option + }; + } + if availables.len() == 1 { + return availables[0]; + } + + return if dark_option < 0.0 { 0.0 } else { dark_option }; + } + + result + } + } +} + +#[inline] +fn prefer_light_foreground(tone: f32) -> bool { + tone.round() < 60.0 +} + +pub fn foreground_tone(background_tone: f32, ratio: f32) -> f32 { + let lighter_tone = super::contrast::unsafe_lighter(background_tone, ratio); + let darker_tone = super::contrast::unsafe_darker(background_tone, ratio); + let lighter_ratio = super::contrast::ratio_of_tones(lighter_tone, background_tone); + let darker_ratio = super::contrast::ratio_of_tones(darker_tone, background_tone); + + if prefer_light_foreground(background_tone) { + let difference = (lighter_ratio - darker_ratio).abs() < 0.1 + && lighter_ratio < ratio + && darker_ratio < ratio; + if lighter_ratio >= ratio || lighter_ratio >= darker_ratio || difference { + lighter_tone + } else { + darker_tone + } + } else { + if darker_ratio >= ratio || darker_ratio >= lighter_ratio { + darker_tone + } else { + lighter_tone + } + } +} diff --git a/color-module/src/schemes/material_design_3_dynamic/dynamic_scheme.rs b/color-module/src/schemes/material_design_3_dynamic/dynamic_scheme.rs new file mode 100644 index 0000000..18b7fc1 --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/dynamic_scheme.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use palette::Lch; + +use crate::schemes::material_design_3::TonalPalette; + +use super::constants::Variant; + +#[allow(dead_code)] +pub struct DynamicScheme { + pub source_color: Lch, + pub error_palette: TonalPalette, + pub contrast_level: f32, + pub variant: Variant, + pub is_dark: bool, + pub harmonize_customs: bool, + pub primary_palette: TonalPalette, + pub secondary_palette: TonalPalette, + pub tertiary_palette: TonalPalette, + pub neutral_palette: TonalPalette, + pub neutral_variant_palette: TonalPalette, + pub custom_palettes: HashMap, +} + +impl DynamicScheme { + pub fn new( + source_color: Lch, + error_color: Option, + custom_colors: HashMap, + variant: Variant, + contrast_level: f32, + is_dark: bool, + harmonize_customs: bool, + ) -> Self { + let ( + primary_palette, + secondary_palette, + tertiary_palette, + neutral_palette, + neutral_variant_palette, + custom_generator, + ) = variant.build_palette(source_color, harmonize_customs); + + let custom_palettes = custom_colors + .into_iter() + .map(|(name, color)| (name, custom_generator(&color))) + .collect(); + + DynamicScheme { + source_color, + error_palette: error_color + .map(|error_color| { + TonalPalette::from_hue_and_chroma( + error_color.hue.into_positive_degrees(), + error_color.chroma, + ) + }) + .unwrap_or_else(|| TonalPalette::from_hue_and_chroma(25.0, 48.0)), + contrast_level, + variant, + is_dark, + harmonize_customs, + primary_palette, + secondary_palette, + tertiary_palette, + neutral_palette, + neutral_variant_palette, + custom_palettes, + } + } +} diff --git a/color-module/src/schemes/material_design_3_dynamic/material_colors.rs b/color-module/src/schemes/material_design_3_dynamic/material_colors.rs new file mode 100644 index 0000000..415df33 --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/material_colors.rs @@ -0,0 +1,867 @@ +use crate::cond; + +use super::{ + constants::{fix_disliked, Variant}, + contrast_curve::ContrastCurve, + dynamic_color::{foreground_tone, DynamicColor, ToneDeltaPair, TonePolarity}, + dynamic_scheme::DynamicScheme, +}; + +macro_rules! dynamic_gen { + ($name: ident, $palette: expr, $tone: expr) => { + pub fn $name() -> DynamicColor { + DynamicColor::new( + Some(stringify!($name)), + Box::new($palette), + Box::new($tone), + None, + None, + None, + None, + None, + ) + } + }; + ($name: ident, $palette: expr, $tone: expr, $is_background: expr) => { + pub fn $name() -> DynamicColor { + DynamicColor::new( + Some(stringify!($name)), + Box::new($palette), + Box::new($tone), + Some($is_background), + None, + None, + None, + None, + ) + } + }; + ($name: ident, $palette: expr, $tone: expr, $background: expr, $contrast_curve: expr) => { + pub fn $name() -> DynamicColor { + DynamicColor::new( + Some(stringify!($name)), + Box::new($palette), + Box::new($tone), + None, + Some(Box::new($background)), + None, + Some($contrast_curve), + None, + ) + } + }; + ($name: ident, $palette: expr, $tone: expr, $is_background: expr, $background: expr, $secondary_background: expr, $contrast_curve: expr, $tone_delta_pairs: expr) => { + pub fn $name() -> DynamicColor { + DynamicColor::new( + Some(stringify!($name)), + Box::new($palette), + Box::new($tone), + $is_background, + $background, + $secondary_background, + $contrast_curve, + $tone_delta_pairs, + ) + } + }; +} + +#[inline] +fn is_fidelity(scheme: &DynamicScheme) -> bool { + scheme.variant == Variant::Fidelity || scheme.variant == Variant::Content +} + +#[inline] +fn is_monochrome(scheme: &DynamicScheme) -> bool { + scheme.variant == Variant::Monochrome +} + +fn highest_surface(s: &DynamicScheme) -> DynamicColor { + cond!(s.is_dark, surface_bright(), surface()) +} + +dynamic_gen!( + surface, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 6.0, 98.0), + true +); +dynamic_gen!( + surface_dim, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + 6.0, + ContrastCurve::new(87.0, 87.0, 80.0, 75.0).get(s.contrast_level) + ), + true +); +dynamic_gen!( + surface_bright, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + ContrastCurve::new(24.0, 24.0, 29.0, 34.0).get(s.contrast_level), + 98.0 + ), + true +); +dynamic_gen!( + surface_container_lowest, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + ContrastCurve::new(4.0, 4.0, 2.0, 0.0).get(s.contrast_level), + 100.0 + ), + true +); +dynamic_gen!( + surface_container_low, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + ContrastCurve::new(10.0, 10.0, 11.0, 12.0).get(s.contrast_level), + ContrastCurve::new(96.0, 96.0, 96.0, 95.0).get(s.contrast_level) + ), + true +); +dynamic_gen!( + surface_container, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + ContrastCurve::new(12.0, 12.0, 16.0, 20.0).get(s.contrast_level), + ContrastCurve::new(94.0, 94.0, 92.0, 90.0).get(s.contrast_level) + ), + true +); +dynamic_gen!( + surface_container_high, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + ContrastCurve::new(17.0, 17.0, 21.0, 25.0).get(s.contrast_level), + ContrastCurve::new(92.0, 92.0, 88.0, 85.0).get(s.contrast_level) + ), + true +); +dynamic_gen!( + surface_container_highest, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!( + s.is_dark, + ContrastCurve::new(22.0, 22.0, 26.0, 30.0).get(s.contrast_level), + ContrastCurve::new(90.0, 90.0, 84.0, 80.0).get(s.contrast_level) + ), + true +); +dynamic_gen!( + on_surface, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 90.0, 10.0), + highest_surface, + ContrastCurve::new(4.5, 7.0, 11.0, 21.0) +); +dynamic_gen!( + surface_variant, + |s: &DynamicScheme| s.neutral_variant_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 30.0, 90.0), + true +); +dynamic_gen!( + on_surface_variant, + |s: &DynamicScheme| s.neutral_variant_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 80.0, 30.0), + highest_surface, + ContrastCurve::new(3.0, 4.5, 7.0, 11.0) +); +dynamic_gen!( + inverse_surface, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 20.0, 95.0) +); +dynamic_gen!( + inverse_on_surface, + |s: &DynamicScheme| s.neutral_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 20.0, 95.0), + |_| inverse_surface(), + ContrastCurve::new(4.5, 7.0, 11.0, 21.0) +); +dynamic_gen!( + outline, + |s: &DynamicScheme| s.neutral_variant_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 60.0, 50.0), + highest_surface, + ContrastCurve::new(1.5, 3.0, 4.5, 7.0) +); +dynamic_gen!( + outline_variant, + |s: &DynamicScheme| s.neutral_variant_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 30.0, 80.0), + highest_surface, + ContrastCurve::new(1.0, 1.0, 3.0, 4.5) +); +dynamic_gen!( + shadow, + |s: &DynamicScheme| s.neutral_palette.clone(), + |_| 0.0 +); +dynamic_gen!(scrim, |s: &DynamicScheme| s.neutral_palette.clone(), |_| { + 0.0 +}); +dynamic_gen!( + primary, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + cond!(s.is_dark, 100.0, 0.0) + } else { + cond!(s.is_dark, 80.0, 40.0) + } + }, + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)), + Some(Box::new(|_| ToneDeltaPair { + role_a: primary_container(), + role_b: primary(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_primary, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + cond!(s.is_dark, 10.0, 90.0) + } else { + cond!(s.is_dark, 20.0, 100.0) + } + }, + |_| primary(), + ContrastCurve::new(4.5, 7.0, 11.0, 21.0) +); +dynamic_gen!( + primary_container, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| { + if is_fidelity(s) { + return s.source_color.l; + } + if is_monochrome(s) { + cond!(s.is_dark, 85.0, 25.0) + } else { + cond!(s.is_dark, 30.0, 90.0) + } + }, + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: primary_container(), + role_b: primary(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_primary_container, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| { + if is_fidelity(s) { + return foreground_tone(primary_container().get_tone(s), 4.5); + } + if is_monochrome(s) { + cond!(s.is_dark, 0.0, 100.0) + } else { + cond!(s.is_dark, 90.0, 30.0) + } + }, + |_| primary_container(), + ContrastCurve::new(3.0, 4.5, 7.0, 11.0) +); +dynamic_gen!( + inverse_primary, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 40.0, 80.0), + |_| inverse_surface(), + ContrastCurve::new(3.0, 4.5, 7.0, 7.0) +); +dynamic_gen!( + secondary, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 80.0, 40.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)), + None +); +dynamic_gen!( + on_secondary, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + cond!(s.is_dark, 10.0, 100.0) + } else { + cond!(s.is_dark, 20.0, 100.0) + } + }, + |_| secondary(), + ContrastCurve::new(4.5, 7.0, 11.0, 21.0) +); +dynamic_gen!( + secondary_container, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| { + let initial_tone = cond!(s.is_dark, 30.0, 90.0); + if is_monochrome(s) { + return cond!(s.is_dark, 30.0, 85.0); + } + initial_tone + }, + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: secondary_container(), + role_b: secondary(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_secondary_container, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + return cond!(s.is_dark, 90.0, 10.0); + } + if !is_fidelity(s) { + return cond!(s.is_dark, 90.0, 30.0); + } + foreground_tone(secondary_container().get_tone(s), 4.5) + }, + |_| secondary_container(), + ContrastCurve::new(3.0, 4.5, 7.0, 11.0) +); +dynamic_gen!( + tertiary, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + return cond!(s.is_dark, 90.0, 25.0); + } + cond!(s.is_dark, 80.0, 40.0) + }, + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)), + Some(Box::new(|_| ToneDeltaPair { + role_a: tertiary_container(), + role_b: tertiary(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_tertiary, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + return cond!(s.is_dark, 10.0, 90.0); + } + cond!(s.is_dark, 20.0, 100.0) + }, + |_| tertiary(), + ContrastCurve::new(4.5, 7.0, 11.0, 21.0) +); +dynamic_gen!( + tertiary_container, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + return cond!(s.is_dark, 60.0, 49.0); + } + if !is_fidelity(s) { + return cond!(s.is_dark, 30.0, 90.0); + } + let proposed_lch = s.tertiary_palette.tone(s.source_color.l); + fix_disliked(&proposed_lch).l + }, + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: tertiary_container(), + role_b: tertiary(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_tertiary_container, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + return cond!(s.is_dark, 0.0, 100.0); + } + if !is_fidelity(s) { + return cond!(s.is_dark, 90.0, 30.0); + } + foreground_tone(tertiary_container().get_tone(s), 4.5) + }, + |_| tertiary_container(), + ContrastCurve::new(3.0, 4.5, 7.0, 11.0) +); +dynamic_gen!( + error, + |s: &DynamicScheme| s.error_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 80.0, 40.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)), + Some(Box::new(|_| ToneDeltaPair { + role_a: error_container(), + role_b: error(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_error, + |s: &DynamicScheme| s.error_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 20.0, 100.0), + |_| error(), + ContrastCurve::new(4.5, 7.0, 11.0, 21.0) +); +dynamic_gen!( + error_container, + |s: &DynamicScheme| s.error_palette.clone(), + |s: &DynamicScheme| cond!(s.is_dark, 30.0, 90.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: error_container(), + role_b: error(), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) +); +dynamic_gen!( + on_error_container, + |s: &DynamicScheme| s.error_palette.clone(), + |s: &DynamicScheme| { + if is_monochrome(s) { + cond!(s.is_dark, 90.0, 10.0) + } else { + cond!(s.is_dark, 90.0, 30.0) + } + }, + |_| error_container(), + ContrastCurve::new(3.0, 4.5, 7.0, 11.0) +); +dynamic_gen!( + primary_fixed, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 40.0, 90.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: primary_fixed(), + role_b: primary_fixed_dim(), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) +); +dynamic_gen!( + primary_fixed_dim, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 30.0, 80.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: primary_fixed(), + role_b: primary_fixed_dim(), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) +); +dynamic_gen!( + on_primary_fixed, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 100.0, 10.0), + None, + Some(Box::new(|_| primary_fixed_dim())), + Some(Box::new(|_| primary_fixed())), + Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)), + None +); +dynamic_gen!( + on_primary_fixed_variant, + |s: &DynamicScheme| s.primary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 90.0, 30.0), + None, + Some(Box::new(|_| primary_fixed_dim())), + Some(Box::new(|_| primary_fixed())), + Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)), + None +); +dynamic_gen!( + secondary_fixed, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 80.0, 90.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: secondary_fixed(), + role_b: secondary_fixed_dim(), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) +); +dynamic_gen!( + secondary_fixed_dim, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 70.0, 80.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: secondary_fixed(), + role_b: secondary_fixed_dim(), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) +); +dynamic_gen!( + on_secondary_fixed, + |s: &DynamicScheme| s.secondary_palette.clone(), + |_| 10.0, + None, + Some(Box::new(|_| secondary_fixed_dim())), + Some(Box::new(|_| secondary_fixed())), + Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)), + None +); +dynamic_gen!( + on_secondary_fixed_variant, + |s: &DynamicScheme| s.secondary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 25.0, 30.0), + None, + Some(Box::new(|_| secondary_fixed_dim())), + Some(Box::new(|_| secondary_fixed())), + Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)), + None +); +dynamic_gen!( + tertiary_fixed, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 40.0, 90.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: tertiary_fixed(), + role_b: tertiary_fixed_dim(), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) +); +dynamic_gen!( + tertiary_fixed_dim, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 30.0, 80.0), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + Some(Box::new(|_| ToneDeltaPair { + role_a: tertiary_fixed(), + role_b: tertiary_fixed_dim(), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) +); +dynamic_gen!( + on_tertiary_fixed, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 100.0, 10.0), + None, + Some(Box::new(|_| tertiary_fixed_dim())), + Some(Box::new(|_| tertiary_fixed())), + Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)), + None +); +dynamic_gen!( + on_tertiary_fixed_variant, + |s: &DynamicScheme| s.tertiary_palette.clone(), + |s: &DynamicScheme| cond!(is_monochrome(s), 90.0, 30.0), + None, + Some(Box::new(|_| tertiary_fixed_dim())), + Some(Box::new(|_| tertiary_fixed())), + Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)), + None +); + +pub fn custom(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("{}", name)), + { + let name = name.clone(); + Box::new(move |s: &DynamicScheme| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s: &DynamicScheme| { + if is_monochrome(s) { + cond!(s.is_dark, 100.0, 0.0) + } else { + cond!(s.is_dark, 80.0, 40.0) + } + }), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)), + { + let name = name.clone(); + Some(Box::new(move |_| ToneDeltaPair { + role_a: custom_container(String::from(&name)), + role_b: custom(String::from(&name)), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) + }, + ) +} + +pub fn on_custom(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("on_{}", name)), + { + let name = name.clone(); + Box::new(move |s| { + let custom_palette = s.custom_palettes.get(&name).unwrap().clone(); + custom_palette + }) + }, + Box::new(|s: &DynamicScheme| { + if is_monochrome(s) { + cond!(s.is_dark, 10.0, 90.0) + } else { + cond!(s.is_dark, 20.0, 100.0) + } + }), + None, + { + let name = name.clone(); + Some(Box::new(move |_| custom(String::from(&name)))) + }, + None, + Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)), + None, + ) +} + +pub fn custom_container(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("{}_container", name)), + { + let name = name.clone(); + Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s| { + if is_fidelity(s) { + return s.source_color.l; + } + if is_monochrome(s) { + cond!(s.is_dark, 85.0, 25.0) + } else { + cond!(s.is_dark, 30.0, 90.0) + } + }), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + { + let name = name.clone(); + Some(Box::new(move |_| ToneDeltaPair { + role_a: custom_container(String::from(&name)), + role_b: custom(String::from(&name)), + delta: 10.0, + polarity: TonePolarity::Nearer, + togather: false, + })) + }, + ) +} + +pub fn on_custom_container(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("on_{}_container", name)), + { + let name = name.clone(); + Box::new(move |s| { + let custom_palette = s.custom_palettes.get(&name).unwrap().clone(); + custom_palette + }) + }, + { + let name = name.clone(); + Box::new(move |s| { + if is_fidelity(s) { + return foreground_tone(custom_container(String::from(&name)).get_tone(s), 4.5); + } + if is_monochrome(s) { + cond!(s.is_dark, 0.0, 100.0) + } else { + cond!(s.is_dark, 90.0, 30.0) + } + }) + }, + None, + { + let name = name.clone(); + Some(Box::new(move |_| custom_container(String::from(&name)))) + }, + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)), + None, + ) +} + +pub fn inverse_custom(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("inverse_{}", name)), + { + let name = name.clone(); + Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s| cond!(s.is_dark, 20.0, 95.0)), + None, + Some(Box::new(|_| inverse_surface())), + None, + Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)), + None, + ) +} + +pub fn custom_fixed(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("{}_fixed", name)), + { + let name = name.clone(); + Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s| cond!(is_monochrome(s), 40.0, 90.0)), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + { + let name = name.clone(); + Some(Box::new(move |_| ToneDeltaPair { + role_a: custom_fixed(String::from(&name)), + role_b: custom_fixed_dim(String::from(&name)), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) + }, + ) +} + +pub fn custom_fixed_dim(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("{}_fixed_dim", name)), + { + let name = name.clone(); + Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s| cond!(is_monochrome(s), 30.0, 80.0)), + Some(true), + Some(Box::new(highest_surface)), + None, + Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)), + { + let name = name.clone(); + Some(Box::new(move |_| ToneDeltaPair { + role_a: custom_fixed(String::from(&name)), + role_b: custom_fixed_dim(String::from(&name)), + delta: 10.0, + polarity: TonePolarity::Lighter, + togather: true, + })) + }, + ) +} + +pub fn on_custom_fixed(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("on_{}_fixed", name)), + { + let name = name.clone(); + Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s| cond!(is_monochrome(s), 100.0, 10.0)), + None, + { + let name = name.clone(); + Some(Box::new(move |_| custom_fixed_dim(String::from(&name)))) + }, + Some(Box::new(move |_| custom_fixed(String::from(&name)))), + Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)), + None, + ) +} + +pub fn on_custom_fixed_variant(name: String) -> DynamicColor { + DynamicColor::new( + Some(&format!("on_{}_fixed_variant", name)), + { + let name = name.clone(); + Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone()) + }, + Box::new(|s| cond!(is_monochrome(s), 90.0, 30.0)), + None, + { + let name = name.clone(); + Some(Box::new(move |_| custom_fixed_dim(String::from(&name)))) + }, + Some(Box::new(move |_| custom_fixed(String::from(&name)))), + Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)), + None, + ) +} diff --git a/color-module/src/schemes/material_design_3_dynamic/mod.rs b/color-module/src/schemes/material_design_3_dynamic/mod.rs new file mode 100644 index 0000000..b8cb2da --- /dev/null +++ b/color-module/src/schemes/material_design_3_dynamic/mod.rs @@ -0,0 +1,187 @@ +use std::{collections::HashMap, str::FromStr}; + +use dynamic_scheme::DynamicScheme; +use material_colors::*; +use palette::{IntoColor, Lch, Srgb}; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +use crate::{cond, convert::map_lch_to_srgb_hex, errors}; + +use super::material_design_3::{M3BaselineColors, M3ColorSet, M3SurfaceSet}; +pub use constants::Variant; + +mod constants; +mod contrast; +mod contrast_curve; +mod dynamic_color; +mod dynamic_scheme; +mod material_colors; + +#[wasm_bindgen] +pub fn material_design_3_dynamic_variant() -> Result { + let variants = enum_iterator::all::() + .map(|variant| { + serde_json::json!({ + "label": variant.label(), + "value": variant as u8, + }) + }) + .collect::>(); + + Ok(serde_wasm_bindgen::to_value(&variants).map_err(|e| e.to_string())?) +} + +fn build_primary_color_set(scheme: &DynamicScheme) -> M3ColorSet { + M3ColorSet { + root: map_lch_to_srgb_hex(&primary().get_lch(scheme)), + on_root: map_lch_to_srgb_hex(&on_primary().get_lch(scheme)), + container: map_lch_to_srgb_hex(&primary_container().get_lch(scheme)), + on_container: map_lch_to_srgb_hex(&on_primary_container().get_lch(scheme)), + fixed: map_lch_to_srgb_hex(&primary_fixed().get_lch(scheme)), + on_fixed: map_lch_to_srgb_hex(&on_primary_fixed().get_lch(scheme)), + fixed_variant: map_lch_to_srgb_hex(&on_primary_fixed_variant().get_lch(scheme)), + fixed_dim: map_lch_to_srgb_hex(&primary_fixed_dim().get_lch(scheme)), + inverse: map_lch_to_srgb_hex(&inverse_primary().get_lch(scheme)), + } +} + +fn build_secondary_color_set(scheme: &DynamicScheme) -> M3ColorSet { + M3ColorSet { + root: map_lch_to_srgb_hex(&secondary().get_lch(scheme)), + on_root: map_lch_to_srgb_hex(&on_secondary().get_lch(scheme)), + container: map_lch_to_srgb_hex(&secondary_container().get_lch(scheme)), + on_container: map_lch_to_srgb_hex(&on_secondary_container().get_lch(scheme)), + fixed: map_lch_to_srgb_hex(&secondary_fixed().get_lch(scheme)), + on_fixed: map_lch_to_srgb_hex(&on_secondary_fixed().get_lch(scheme)), + fixed_variant: map_lch_to_srgb_hex(&on_secondary_fixed_variant().get_lch(scheme)), + fixed_dim: map_lch_to_srgb_hex(&secondary_fixed_dim().get_lch(scheme)), + ..cond!( + scheme.is_dark, + M3ColorSet::new_dark_set(&scheme.secondary_palette), + M3ColorSet::new_light_set(&scheme.secondary_palette) + ) + } +} + +fn build_tertiary_color_set(scheme: &DynamicScheme) -> M3ColorSet { + M3ColorSet { + root: map_lch_to_srgb_hex(&tertiary().get_lch(scheme)), + on_root: map_lch_to_srgb_hex(&on_tertiary().get_lch(scheme)), + container: map_lch_to_srgb_hex(&tertiary_container().get_lch(scheme)), + on_container: map_lch_to_srgb_hex(&on_tertiary_container().get_lch(scheme)), + fixed: map_lch_to_srgb_hex(&tertiary_fixed().get_lch(scheme)), + on_fixed: map_lch_to_srgb_hex(&on_tertiary_fixed().get_lch(scheme)), + fixed_variant: map_lch_to_srgb_hex(&on_tertiary_fixed_variant().get_lch(scheme)), + fixed_dim: map_lch_to_srgb_hex(&tertiary_fixed_dim().get_lch(scheme)), + ..cond!( + scheme.is_dark, + M3ColorSet::new_dark_set(&scheme.tertiary_palette), + M3ColorSet::new_light_set(&scheme.tertiary_palette) + ) + } +} + +fn build_surface_color_set(scheme: &DynamicScheme) -> M3SurfaceSet { + M3SurfaceSet { + root: map_lch_to_srgb_hex(&surface().get_lch(scheme)), + dim: map_lch_to_srgb_hex(&surface_dim().get_lch(scheme)), + bright: map_lch_to_srgb_hex(&surface_bright().get_lch(scheme)), + variant: map_lch_to_srgb_hex(&surface_variant().get_lch(scheme)), + container: map_lch_to_srgb_hex(&surface_container().get_lch(scheme)), + container_lowest: map_lch_to_srgb_hex(&surface_container_lowest().get_lch(scheme)), + container_low: map_lch_to_srgb_hex(&surface_container_low().get_lch(scheme)), + container_high: map_lch_to_srgb_hex(&surface_container_high().get_lch(scheme)), + container_highest: map_lch_to_srgb_hex(&surface_container_highest().get_lch(scheme)), + on_root: map_lch_to_srgb_hex(&on_surface().get_lch(scheme)), + on_root_variant: map_lch_to_srgb_hex(&on_surface_variant().get_lch(scheme)), + inverse: map_lch_to_srgb_hex(&inverse_surface().get_lch(scheme)), + on_inverse: map_lch_to_srgb_hex(&inverse_on_surface().get_lch(scheme)), + } +} + +fn build_error_color_set(scheme: &DynamicScheme) -> M3ColorSet { + M3ColorSet { + root: map_lch_to_srgb_hex(&error().get_lch(scheme)), + on_root: map_lch_to_srgb_hex(&on_error().get_lch(scheme)), + container: map_lch_to_srgb_hex(&error_container().get_lch(scheme)), + on_container: map_lch_to_srgb_hex(&on_error_container().get_lch(scheme)), + ..cond!( + scheme.is_dark, + M3ColorSet::new_dark_set(&scheme.error_palette), + M3ColorSet::new_light_set(&scheme.error_palette) + ) + } +} + +fn build_custom_color_set(scheme: &DynamicScheme, name: String) -> M3ColorSet { + M3ColorSet { + root: map_lch_to_srgb_hex(&custom(name.clone()).get_lch(scheme)), + on_root: map_lch_to_srgb_hex(&on_custom(name.clone()).get_lch(scheme)), + container: map_lch_to_srgb_hex(&custom_container(name.clone()).get_lch(scheme)), + on_container: map_lch_to_srgb_hex(&on_custom_container(name.clone()).get_lch(scheme)), + fixed: map_lch_to_srgb_hex(&custom_fixed(name.clone()).get_lch(scheme)), + on_fixed: map_lch_to_srgb_hex(&on_custom_fixed(name.clone()).get_lch(scheme)), + fixed_variant: map_lch_to_srgb_hex(&on_custom_fixed_variant(name.clone()).get_lch(scheme)), + fixed_dim: map_lch_to_srgb_hex(&custom_fixed_dim(name.clone()).get_lch(scheme)), + inverse: map_lch_to_srgb_hex(&inverse_custom(name.clone()).get_lch(scheme)), + } +} + +pub fn build_baseline(scheme: &DynamicScheme) -> M3BaselineColors { + M3BaselineColors::full_custom( + build_primary_color_set(scheme), + build_secondary_color_set(scheme), + build_tertiary_color_set(scheme), + build_error_color_set(scheme), + build_surface_color_set(scheme), + map_lch_to_srgb_hex(&outline().get_lch(scheme)), + map_lch_to_srgb_hex(&outline_variant().get_lch(scheme)), + map_lch_to_srgb_hex(&scrim().get_lch(scheme)), + map_lch_to_srgb_hex(&shadow().get_lch(scheme)), + scheme + .custom_palettes + .keys() + .map(|name| (name.clone(), build_custom_color_set(scheme, name.clone()))) + .collect(), + scheme.is_dark, + ) +} + +pub fn build_dynamic_scheme( + source_color: &str, + error_color: Option, + custom_colors: HashMap, + variant: constants::Variant, + contrast_level: f32, + is_dark: bool, + harmonize_customs: bool, +) -> Result { + let source_color = Srgb::from_str(source_color) + .map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))? + .into_format::() + .into_color(); + let error_color = error_color + .map(|color| Srgb::from_str(&color)) + .transpose() + .map_err(|_| errors::ColorError::UnrecogniazedRGB("error color".to_string()))? + .map(|color| color.into_format::().into_color()); + let custom_colors = custom_colors + .into_iter() + .map(|(name, color)| { + let color = Srgb::from_str(&color) + .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))? + .into_format::() + .into_color(); + Ok((name, color)) + }) + .collect::, errors::ColorError>>()?; + Ok(DynamicScheme::new( + source_color, + error_color, + custom_colors, + variant, + contrast_level, + is_dark, + harmonize_customs, + )) +} diff --git a/color-module/src/schemes/mod.rs b/color-module/src/schemes/mod.rs index 4c02f66..7b6c111 100644 --- a/color-module/src/schemes/mod.rs +++ b/color-module/src/schemes/mod.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use material_design_2::MaterialDesign2Scheme; use material_design_3::MaterialDesign3Scheme; +use material_design_3_dynamic::{build_baseline, build_dynamic_scheme, Variant}; use q_style::{QScheme, SchemeSetting}; use swatch_style::{SwatchEntry, SwatchSchemeSetting}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; @@ -10,6 +11,7 @@ use crate::errors; pub mod material_design_2; pub mod material_design_3; +pub mod material_design_3_dynamic; pub mod q_style; pub mod swatch_style; @@ -142,3 +144,48 @@ pub fn generate_swatch_scheme( )) .map_err(|_| errors::ColorError::UnableToAssembleOutput)?) } + +#[wasm_bindgen] +pub fn generate_material_design_3_dynamic_scheme( + source_color: &str, + error_color: Option, + variant: Variant, + contrast_level: f32, + harmonize_customs: bool, + custom_colors: JsValue, +) -> Result { + let custom_colors: HashMap = serde_wasm_bindgen::from_value(custom_colors) + .map_err(|_| errors::ColorError::UnableToParseArgument)?; + + let light_scheme = build_dynamic_scheme( + source_color, + error_color.clone(), + custom_colors.clone(), + variant, + contrast_level, + false, + harmonize_customs, + )?; + let dark_scheme = build_dynamic_scheme( + source_color, + error_color, + custom_colors, + variant, + contrast_level, + true, + harmonize_customs, + )?; + + let scheme = MaterialDesign3Scheme::full_custom( + build_baseline(&light_scheme), + build_baseline(&dark_scheme), + ); + + 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)?) +} diff --git a/color-module/src/theory.rs b/color-module/src/theory.rs index 9eccde8..73d07fd 100644 --- a/color-module/src/theory.rs +++ b/color-module/src/theory.rs @@ -106,3 +106,33 @@ pub fn triadic(color: &str) -> Result, errors::ColorError> { format!("{:x}", srgb_p240.into_format::()), ]) } + +pub fn sanitize_hue_degrees(degrees: f32) -> f32 { + let degrees = degrees % 360.0; + if degrees < 0.0 { + degrees + 360.0 + } else { + degrees + } +} + +#[inline] +fn difference_in_degrees(a: f32, b: f32) -> f32 { + 180.0 - ((a - b).abs() - 180.0).abs() +} + +#[inline] +fn rotation_direction(from: f32, to: f32) -> f32 { + let difference = sanitize_hue_degrees(to - from); + if difference <= 180.0 { + 1.0 + } else { + -1.0 + } +} + +pub fn harmonize_hue(design_hue: f32, source_hue: f32) -> f32 { + let difference = difference_in_degrees(design_hue, source_hue); + let rotation_degrees = (difference * 0.5).min(15.0); + sanitize_hue_degrees(design_hue + rotation_degrees * rotation_direction(design_hue, source_hue)) +}