Compare commits

..

7 Commits

24 changed files with 2400 additions and 18 deletions

View File

@ -135,3 +135,14 @@ pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, err
.into_format::<f32>(); .into_format::<f32>();
Ok(fg_srgb.relative_contrast(bg_srgb)) Ok(fg_srgb.relative_contrast(bg_srgb))
} }
#[macro_export]
macro_rules! cond {
($s: expr, $a: expr, $b: expr) => {
if $s {
$a
} else {
$b
}
};
}

View File

@ -80,6 +80,34 @@ impl M3BaselineColors {
self.customs.insert(name, color_set_generator(c)); 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<String, M3ColorSet>,
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<String> { pub fn to_css_variables(&self) -> Vec<String> {
let mut css_variables = Vec::new(); let mut css_variables = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" }; let prefix = if self.dark_set { "dark" } else { "light" };

View File

@ -1,9 +1,11 @@
use std::str::FromStr; use std::str::FromStr;
use baseline::M3BaselineColors; pub use baseline::M3BaselineColors;
pub use color_set::M3ColorSet;
use palette::{IntoColor, Lch, Srgb}; use palette::{IntoColor, Lch, Srgb};
use serde::Serialize; 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::convert::map_lch_to_srgb_hex;
use crate::errors; use crate::errors;
@ -64,6 +66,15 @@ impl MaterialDesign3Scheme {
self.dark_baseline.add_custom_set(name.clone(), &palette); self.dark_baseline.add_custom_set(name.clone(), &palette);
Ok(()) 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 { impl SchemeExport for MaterialDesign3Scheme {

View File

@ -9,6 +9,7 @@ pub struct M3SurfaceSet {
pub root: String, pub root: String,
pub dim: String, pub dim: String,
pub bright: String, pub bright: String,
pub variant: String,
pub container: String, pub container: String,
pub container_lowest: String, pub container_lowest: String,
pub container_low: String, pub container_low: String,
@ -25,6 +26,7 @@ impl M3SurfaceSet {
let root = neutral.tone(98.0); let root = neutral.tone(98.0);
let dim = neutral.tone(87.0); let dim = neutral.tone(87.0);
let bright = neutral.tone(98.0); let bright = neutral.tone(98.0);
let variant = neutral_variant.tone(90.0);
let container = neutral.tone(94.0); let container = neutral.tone(94.0);
let container_lowest = neutral.tone(100.0); let container_lowest = neutral.tone(100.0);
let container_low = neutral.tone(96.0); let container_low = neutral.tone(96.0);
@ -39,6 +41,7 @@ impl M3SurfaceSet {
root: map_lch_to_srgb_hex(&root), root: map_lch_to_srgb_hex(&root),
dim: map_lch_to_srgb_hex(&dim), dim: map_lch_to_srgb_hex(&dim),
bright: map_lch_to_srgb_hex(&bright), bright: map_lch_to_srgb_hex(&bright),
variant: map_lch_to_srgb_hex(&variant),
container: map_lch_to_srgb_hex(&container), container: map_lch_to_srgb_hex(&container),
container_lowest: map_lch_to_srgb_hex(&container_lowest), container_lowest: map_lch_to_srgb_hex(&container_lowest),
container_low: map_lch_to_srgb_hex(&container_low), container_low: map_lch_to_srgb_hex(&container_low),
@ -55,6 +58,7 @@ impl M3SurfaceSet {
let root = neutral.tone(6.0); let root = neutral.tone(6.0);
let dim = neutral.tone(6.0); let dim = neutral.tone(6.0);
let bright = neutral.tone(24.0); let bright = neutral.tone(24.0);
let variant = neutral_variant.tone(30.0);
let container = neutral.tone(12.0); let container = neutral.tone(12.0);
let container_lowest = neutral.tone(4.0); let container_lowest = neutral.tone(4.0);
let container_low = neutral.tone(10.0); let container_low = neutral.tone(10.0);
@ -69,6 +73,7 @@ impl M3SurfaceSet {
root: map_lch_to_srgb_hex(&root), root: map_lch_to_srgb_hex(&root),
dim: map_lch_to_srgb_hex(&dim), dim: map_lch_to_srgb_hex(&dim),
bright: map_lch_to_srgb_hex(&bright), bright: map_lch_to_srgb_hex(&bright),
variant: map_lch_to_srgb_hex(&variant),
container: map_lch_to_srgb_hex(&container), container: map_lch_to_srgb_hex(&container),
container_lowest: map_lch_to_srgb_hex(&container_lowest), container_lowest: map_lch_to_srgb_hex(&container_lowest),
container_low: map_lch_to_srgb_hex(&container_low), container_low: map_lch_to_srgb_hex(&container_low),
@ -90,6 +95,10 @@ impl M3SurfaceSet {
"--color-{}-surface-bright: #{};", "--color-{}-surface-bright: #{};",
prefix, self.bright prefix, self.bright
)); ));
css_variables.push(format!(
"--color-{}-surface-variant: #{};",
prefix, self.variant
));
css_variables.push(format!( css_variables.push(format!(
"--color-{}-surface-container: #{};", "--color-{}-surface-container: #{};",
prefix, self.container prefix, self.container
@ -136,6 +145,10 @@ impl M3SurfaceSet {
"$color-{}-surface-bright: #{};", "$color-{}-surface-bright: #{};",
prefix, self.bright prefix, self.bright
)); ));
scss_variables.push(format!(
"$color-{}-surface-variant: #{};",
prefix, self.variant
));
scss_variables.push(format!( scss_variables.push(format!(
"$color-{}-surface-container: #{};", "$color-{}-surface-container: #{};",
prefix, self.container prefix, self.container
@ -179,6 +192,7 @@ impl M3SurfaceSet {
js_object_fields.push(format!("{}Surface: '#{}',", prefix, self.root)); js_object_fields.push(format!("{}Surface: '#{}',", prefix, self.root));
js_object_fields.push(format!("{}SurfaceDim: '#{}',", prefix, self.dim)); js_object_fields.push(format!("{}SurfaceDim: '#{}',", prefix, self.dim));
js_object_fields.push(format!("{}SurfaceBright: '#{}',", prefix, self.bright)); js_object_fields.push(format!("{}SurfaceBright: '#{}',", prefix, self.bright));
js_object_fields.push(format!("{}SurfaceVariant: '#{}',", prefix, self.variant));
js_object_fields.push(format!( js_object_fields.push(format!(
"{}SurfaceContainer: '#{}',", "{}SurfaceContainer: '#{}',",
prefix, self.container prefix, self.container

View File

@ -0,0 +1,379 @@
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 from_u8(value: u8) -> Variant {
match value {
0 => Variant::Monochrome,
1 => Variant::Neutral,
2 => Variant::TonalSpot,
3 => Variant::Vibrant,
4 => Variant::Expressive,
5 => Variant::Fidelity,
6 => Variant::Content,
7 => Variant::Rainbow,
8 => Variant::FruitSalad,
_ => Variant::Expressive,
}
}
pub fn hues(&self) -> Vec<f32> {
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<f32> {
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<f32> {
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<f32>, rotations: &Vec<f32>) -> 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()
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,245 @@
use std::rc::Rc;
use palette::Lch;
use crate::schemes::material_design_3::TonalPalette;
use super::{contrast_curve::ContrastCurve, dynamic_scheme::DynamicScheme};
pub type TonalPaletteGenerator = Box<dyn Fn(&DynamicScheme) -> TonalPalette>;
pub type ToneSearcher = Box<dyn Fn(&DynamicScheme) -> f32>;
pub type DynamicColorSearcher = Box<dyn Fn(&DynamicScheme) -> Rc<DynamicColor>>;
pub type ToneDeltaPairGenerator = Box<dyn Fn(&DynamicScheme) -> ToneDeltaPair>;
pub type CustomPaletteGenerator = Box<dyn Fn(&Lch) -> 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<bool>,
background: Option<DynamicColorSearcher>,
secondary_background: Option<DynamicColorSearcher>,
contrast_curve: Option<ContrastCurve>,
tone_delta_pairs: Option<ToneDeltaPairGenerator>,
}
pub struct ToneDeltaPair {
pub role_a: Rc<DynamicColor>,
pub role_b: Rc<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<bool>,
background: Option<DynamicColorSearcher>,
secondary_background: Option<DynamicColorSearcher>,
contrast_curve: Option<ContrastCurve>,
tone_delta_pairs: Option<ToneDeltaPairGenerator>,
) -> 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 tone(&self, scheme: &DynamicScheme) -> f32 {
(self.tone)(scheme)
}
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.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.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
}
}
}

View File

@ -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<String, TonalPalette>,
}
impl DynamicScheme {
pub fn new(
source_color: Lch,
error_color: Option<Lch>,
custom_colors: HashMap<String, Lch>,
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,
}
}
}

View File

@ -0,0 +1,917 @@
use std::{cell::LazyCell, rc::Rc};
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 {
($variable: ident, $name: ident, $palette: expr, $tone: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
None,
None,
None,
None,
None,
))
});
};
($variable: ident, $name: ident, $palette: expr, $tone: expr, $is_background: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
Some($is_background),
None,
None,
None,
None,
))
});
};
($variable: ident, $name: ident, $palette: expr, $tone: expr, $background: expr, $contrast_curve: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
None,
Some(Box::new($background)),
None,
Some($contrast_curve),
None,
))
});
};
($variable: ident, $name: ident, $palette: expr, $tone: expr, $is_background: expr, $background: expr, $secondary_background: expr, $contrast_curve: expr, $tone_delta_pairs: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(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) -> Rc<DynamicColor> {
cond!(s.is_dark, Rc::clone(&SURFACE_BRIGHT), Rc::clone(&SURFACE))
}
dynamic_gen!(
SURFACE,
surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 6.0, 98.0),
true
);
dynamic_gen!(
SURFACE_DIM,
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,
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,
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,
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,
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,
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,
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,
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,
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,
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,
inverse_surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 20.0, 95.0)
);
dynamic_gen!(
INVERSE_ON_SURFACE,
inverse_on_surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 20.0, 95.0),
|_| Rc::clone(&INVERSE_SURFACE),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
OUTLINE,
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,
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,
shadow,
|s: &DynamicScheme| s.neutral_palette.clone(),
|_| 0.0
);
dynamic_gen!(
SCRIM,
scrim,
|s: &DynamicScheme| s.neutral_palette.clone(),
|_| { 0.0 }
);
dynamic_gen!(
PRIMARY,
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: Rc::clone(&PRIMARY_CONTAINER),
role_b: Rc::clone(&PRIMARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_PRIMARY,
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)
}
},
|_| Rc::clone(&PRIMARY),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
PRIMARY_CONTAINER,
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: Rc::clone(&PRIMARY_CONTAINER),
role_b: Rc::clone(&PRIMARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_PRIMARY_CONTAINER,
on_primary_container,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| {
if is_fidelity(s) {
return foreground_tone(Rc::clone(&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)
}
},
|_| Rc::clone(&PRIMARY_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
INVERSE_PRIMARY,
inverse_primary,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 40.0, 80.0),
|_| Rc::clone(&INVERSE_SURFACE),
ContrastCurve::new(3.0, 4.5, 7.0, 7.0)
);
dynamic_gen!(
SECONDARY,
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,
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)
}
},
|_| Rc::clone(&SECONDARY),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
SECONDARY_CONTAINER,
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: Rc::clone(&SECONDARY_CONTAINER),
role_b: Rc::clone(&SECONDARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_SECONDARY_CONTAINER,
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(Rc::clone(&SECONDARY_CONTAINER).get_tone(s), 4.5)
},
|_| Rc::clone(&SECONDARY_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
TERTIARY,
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: Rc::clone(&TERTIARY_CONTAINER),
role_b: Rc::clone(&TERTIARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_TERTIARY,
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)
},
|_| Rc::clone(&TERTIARY),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
TERTIARY_CONTAINER,
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: Rc::clone(&TERTIARY_CONTAINER),
role_b: Rc::clone(&TERTIARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_TERTIARY_CONTAINER,
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(Rc::clone(&TERTIARY_CONTAINER).get_tone(s), 4.5)
},
|_| Rc::clone(&TERTIARY_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
ERROR,
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: Rc::clone(&ERROR_CONTAINER),
role_b: Rc::clone(&ERROR),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_ERROR,
on_error,
|s: &DynamicScheme| s.error_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 20.0, 100.0),
|_| Rc::clone(&ERROR),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
ERROR_CONTAINER,
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: Rc::clone(&ERROR_CONTAINER),
role_b: Rc::clone(&ERROR),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_ERROR_CONTAINER,
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)
}
},
|_| Rc::clone(&ERROR_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
PRIMARY_FIXED,
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: Rc::clone(&PRIMARY_FIXED),
role_b: Rc::clone(&PRIMARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
PRIMARY_FIXED_DIM,
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: Rc::clone(&PRIMARY_FIXED),
role_b: Rc::clone(&PRIMARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
ON_PRIMARY_FIXED,
on_primary_fixed,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 100.0, 10.0),
None,
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None
);
dynamic_gen!(
ON_PRIMARY_FIXED_VARIANT,
on_primary_fixed_variant,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 90.0, 30.0),
None,
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None
);
dynamic_gen!(
SECONDARY_FIXED,
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: Rc::clone(&SECONDARY_FIXED),
role_b: Rc::clone(&SECONDARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
SECONDARY_FIXED_DIM,
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: Rc::clone(&SECONDARY_FIXED),
role_b: Rc::clone(&SECONDARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
ON_SECONDARY_FIXED,
on_secondary_fixed,
|s: &DynamicScheme| s.secondary_palette.clone(),
|_| 10.0,
None,
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None
);
dynamic_gen!(
ON_SECONDARY_FIXED_VARIANT,
on_secondary_fixed_variant,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 25.0, 30.0),
None,
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None
);
dynamic_gen!(
TERTIARY_FIXED,
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: Rc::clone(&TERTIARY_FIXED),
role_b: Rc::clone(&TERTIARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
TERTIARY_FIXED_DIM,
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: Rc::clone(&TERTIARY_FIXED),
role_b: Rc::clone(&TERTIARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
ON_TERTIARY_FIXED,
on_tertiary_fixed,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 100.0, 10.0),
None,
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None
);
dynamic_gen!(
ON_TERTIARY_FIXED_VARIANT,
on_tertiary_fixed_variant,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 90.0, 30.0),
None,
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None
);
pub fn custom(name: String) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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(|_| Rc::clone(&INVERSE_SURFACE))),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None,
))
}
pub fn custom_fixed(name: String) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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) -> Rc<DynamicColor> {
Rc::new(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,
))
}

View File

@ -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<JsValue, String> {
let variants = enum_iterator::all::<constants::Variant>()
.map(|variant| {
serde_json::json!({
"label": variant.label(),
"value": variant as u8,
})
})
.collect::<Vec<_>>();
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<String>,
custom_colors: HashMap<String, String>,
variant: constants::Variant,
contrast_level: f32,
is_dark: bool,
harmonize_customs: bool,
) -> Result<DynamicScheme, errors::ColorError> {
let source_color = Srgb::from_str(source_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
.into_format::<f32>()
.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::<f32>().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::<f32>()
.into_color();
Ok((name, color))
})
.collect::<Result<HashMap<String, Lch>, errors::ColorError>>()?;
Ok(DynamicScheme::new(
source_color,
error_color,
custom_colors,
variant,
contrast_level,
is_dark,
harmonize_customs,
))
}

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use material_design_2::MaterialDesign2Scheme; use material_design_2::MaterialDesign2Scheme;
use material_design_3::MaterialDesign3Scheme; use material_design_3::MaterialDesign3Scheme;
use material_design_3_dynamic::{build_baseline, build_dynamic_scheme, Variant};
use q_style::{QScheme, SchemeSetting}; use q_style::{QScheme, SchemeSetting};
use swatch_style::{SwatchEntry, SwatchSchemeSetting}; use swatch_style::{SwatchEntry, SwatchSchemeSetting};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
@ -10,6 +11,7 @@ use crate::errors;
pub mod material_design_2; pub mod material_design_2;
pub mod material_design_3; pub mod material_design_3;
pub mod material_design_3_dynamic;
pub mod q_style; pub mod q_style;
pub mod swatch_style; pub mod swatch_style;
@ -142,3 +144,49 @@ pub fn generate_swatch_scheme(
)) ))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?) .map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
} }
#[wasm_bindgen]
pub fn generate_material_design_3_dynamic_scheme(
source_color: &str,
error_color: Option<String>,
variant: u8,
contrast_level: f32,
harmonize_customs: bool,
custom_colors: JsValue,
) -> Result<JsValue, errors::ColorError> {
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let variant = Variant::from_u8(variant);
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)?)
}

View File

@ -106,3 +106,33 @@ pub fn triadic(color: &str) -> Result<Vec<String>, errors::ColorError> {
format!("{:x}", srgb_p240.into_format::<u8>()), format!("{:x}", srgb_p240.into_format::<u8>()),
]) ])
} }
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))
}

View File

@ -17,5 +17,9 @@
background-color: #a78fff; background-color: #a78fff;
color: var(--color-qihei); color: var(--color-qihei);
} }
&.m3d {
background-color: #ffde3f;
color: var(--color-qihei);
}
} }
} }

View File

@ -22,6 +22,8 @@ export function SchemeSign({ scheme, short = false }: SchemeSignProps) {
return styles.m2; return styles.m2;
case 'material_3': case 'material_3':
return styles.m3; return styles.m3;
case 'material_3_dynamic':
return styles.m3d;
} }
}, [scheme]); }, [scheme]);

View File

@ -31,9 +31,7 @@ export function Switch({ name, checked = false, disabled = false, onChange }: Sw
<div <div
className={cx(styles.switch_handle, isChecked && styles.checked)} className={cx(styles.switch_handle, isChecked && styles.checked)}
onClick={handleSwitch}></div> onClick={handleSwitch}></div>
{!isNil(name) && ( {!isNil(name) && <input type="hidden" name={name} value={isChecked ? 'true' : 'false'} />}
<input type="hidden" name={name} value={isChecked ? 'checked' : undefined} />
)}
</div> </div>
); );
} }

View File

@ -58,3 +58,20 @@ export type MaterialDesign3SchemeStorage = {
scssVariables?: string; scssVariables?: string;
jsVariables?: string; jsVariables?: string;
}; };
export type MaterialDesign3DynamicSchemeSource = {
source: string | null;
error: string | null;
custom_colors?: Record<string, string>;
variant: number | null;
constrastLevel: number | null;
harmonizeCustoms: boolean | null;
};
export type MaterialDesign3DynamicSchemeStorage = {
source?: MaterialDesign3DynamicSchemeSource;
scheme?: MaterialDesign3Scheme;
cssVariables?: string;
scssVariables?: string;
jsVariables?: string;
};

View File

@ -29,7 +29,12 @@ export type ColorDescription = {
oklch: [number, number, number]; oklch: [number, number, number];
}; };
export type SchemeType = 'q_scheme' | 'swatch_scheme' | 'material_2' | 'material_3'; export type SchemeType =
| 'q_scheme'
| 'swatch_scheme'
| 'material_2'
| 'material_3'
| 'material_3_dynamic';
export type SchemeTypeOption = { export type SchemeTypeOption = {
label: string; label: string;
short: string; short: string;
@ -40,6 +45,7 @@ export const SchemeTypeOptions: SchemeTypeOption[] = [
{ label: 'Swatch Scheme', short: 'Swatch', value: 'swatch_scheme' }, { label: 'Swatch Scheme', short: 'Swatch', value: 'swatch_scheme' },
{ label: 'Material Design 2 Scheme', short: 'M2', value: 'material_2' }, { label: 'Material Design 2 Scheme', short: 'M2', value: 'material_2' },
{ label: 'Material Design 3 Scheme', short: 'M3', value: 'material_3' }, { label: 'Material Design 3 Scheme', short: 'M3', value: 'material_3' },
{ label: 'Material Design 3 Dynamic Scheme', short: 'M3D', value: 'material_3_dynamic' },
]; ];
export function schemeType( export function schemeType(

View File

@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { MaterialDesign3DynamicSchemeStorage } from '../../material-3-scheme';
import { SchemeContent } from '../../models';
import { SchemeExport } from './Export';
import { M3DynamicSchemeBuilder } from './m3-dynamic-scheme/Builder';
import { M3SchemePreview } from './m3-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type M3SchemeProps = {
scheme: SchemeContent<MaterialDesign3DynamicSchemeStorage>;
};
export function M3DynamicScheme({ scheme }: M3SchemeProps) {
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
);
return (
<>
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
{isEqual(activeTab, 'overview') && <M3SchemePreview scheme={scheme.schemeStorage.scheme} />}
{isEqual(activeTab, 'builder') && (
<M3DynamicSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
}

View File

@ -25,7 +25,7 @@ export function M3Scheme({ scheme }: M3SchemeProps) {
return ( return (
<> <>
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} /> <Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
{isEqual(activeTab, 'overview') && <M3SchemePreview scheme={scheme} />} {isEqual(activeTab, 'overview') && <M3SchemePreview scheme={scheme.schemeStorage.scheme} />}
{isEqual(activeTab, 'builder') && ( {isEqual(activeTab, 'builder') && (
<M3SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} /> <M3SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)} )}

View File

@ -0,0 +1,52 @@
@layer pages {
.builder_layout {
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.3em;
display: grid;
grid-template-columns: repeat(3, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.parallel_row {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-s);
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
.delete_btn {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: oklch(from var(--color-danger) l c h / 0.25);
&:hover {
background-color: oklch(from var(--color-danger-hover) l c h / 0.65);
}
&:active {
background-color: oklch(from var(--color-danger-active) l c h / 0.65);
}
}
}
}

View File

@ -0,0 +1,186 @@
import { includes, isEmpty, isEqual, isNil } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { Switch } from '../../../components/Switch';
import { VSegmentedControl } from '../../../components/VSegmentedControl';
import { MaterialDesign3DynamicSchemeStorage } from '../../../material-3-scheme';
import { Option, SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type M3DynamicSchemeBuilderProps = {
scheme: SchemeContent<MaterialDesign3DynamicSchemeStorage>;
onBuildCompleted?: () => void;
};
export function M3DynamicSchemeBuilder({ scheme, onBuildCompleted }: M3DynamicSchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const originalColors = useMemo(() => {
return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map(
([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry),
);
}, [scheme.schemeStorage.source]);
const [newColors, setNewColors] = useState<IdenticalColorEntry[]>([]);
const [deleted, setDeleted] = useState<string[]>([]);
const addEntryAction = useCallback(() => {
setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]);
}, []);
const colorKeys = useMemo(
() =>
[...originalColors, ...newColors]
.map((color) => color.id)
.filter((c) => !includes(deleted, c)),
[originalColors, newColors, deleted],
);
const variantOptions = useMemo(() => {
if (!colorFn) return [];
try {
return colorFn.material_design_3_dynamic_variant() as Option[];
} catch (e) {
console.error('[m3 dynamic builder]', e);
}
return [];
}, []);
const [contrastLevel, setContrastLevel] = useState<number>(
scheme.schemeStorage.source?.constrastLevel ?? 0,
);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
const sourceColor = formData.get('source') as string;
if (isNil(sourceColor) || isEmpty(sourceColor)) {
errMsg.set('source', 'Source color is required');
}
if (!isEmpty(errMsg)) return errMsg;
try {
const dynamicVariant = Number(formData.get('variant'));
const contrastLevel = Number(formData.get('contrast_level'));
const harmonizeCustoms = isEqual(formData.get('harmonize_customs'), 'true');
const errorColor = formData.get('error') as string;
const customColors: Record<string, string> = {};
for (const key of colorKeys) {
const name = formData.get(`name_${key}`) as string;
const color = formData.get(`color_${key}`) as string;
if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue;
customColors[name] = color;
}
const generate_scheme = colorFn.generate_material_design_3_dynamic_scheme(
sourceColor,
isNil(errorColor) || isEmpty(errorColor) ? null : errorColor,
dynamicVariant,
contrastLevel,
harmonizeCustoms,
customColors,
);
console.debug('[generate m3d]', generate_scheme);
} catch (e) {
console.error('[generate m3d]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Required Colors</h5>
<label className={styles.label}>Source Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="source"
color={
isNil(scheme.schemeStorage.source?.source) ||
isEmpty(scheme.schemeStorage.source?.source)
? undefined
: scheme.schemeStorage.source?.source
}
/>
{errMsg.has('source') && <span className={styles.error_msg}>{errMsg.get('source')}</span>}
</div>
<label className={styles.label}>Error Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="error"
color={
isNil(scheme.schemeStorage.source?.error) ||
isEmpty(scheme.schemeStorage.source.error)
? undefined
: scheme.schemeStorage.source.error
}
/>
</div>
<h5 className={styles.segment_title}>Dynamic Settings</h5>
<label className={styles.label}>Dynamic Variant</label>
<div>
<VSegmentedControl
name="variant"
options={variantOptions}
defaultValue={scheme.schemeStorage.source?.variant}
/>
</div>
<label className={styles.label}>Contrast Level</label>
<div className={styles.parallel_row}>
<input
type="range"
className="picker"
name="contrast_level"
min={-1}
max={1}
step={0.25}
value={contrastLevel}
onChange={(e) => setContrastLevel(parseFloat(e.target.value))}
/>
<span>{contrastLevel}</span>
</div>
<label className={styles.label}>Harmonize Custom Colors</label>
<div>
<Switch
name="harmonize_customs"
checked={scheme.schemeStorage.source?.harmonizeCustoms ?? false}
/>
</div>
<h5 className={styles.segment_title}>Custom Colors</h5>
<label style={{ gridColumn: 1 }}>Name</label>
<label>Color</label>
<div>
<button type="button" className="small" onClick={addEntryAction}>
Add Color
</button>
</div>
{originalColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{newColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@ -1,11 +1,5 @@
import { ScrollArea } from '../../../components/ScrollArea'; import { ScrollArea } from '../../../components/ScrollArea';
import { import { Baseline, ColorSet, MaterialDesign3Scheme, Surface } from '../../../material-3-scheme';
Baseline,
ColorSet,
MaterialDesign3SchemeStorage,
Surface,
} from '../../../material-3-scheme';
import { SchemeContent } from '../../../models';
import styles from './Preview.module.css'; import styles from './Preview.module.css';
type ColorSetProps = { type ColorSetProps = {
@ -257,15 +251,15 @@ function PreviewBlock({ title, baseline }: PreviewBlockProps) {
} }
type M3SchemePreviewProps = { type M3SchemePreviewProps = {
scheme: SchemeContent<MaterialDesign3SchemeStorage>; scheme: MaterialDesign3Scheme;
}; };
export function M3SchemePreview({ scheme }: M3SchemePreviewProps) { export function M3SchemePreview({ scheme }: M3SchemePreviewProps) {
return ( return (
<ScrollArea enableY> <ScrollArea enableY>
<div className={styles.preview_layout}> <div className={styles.preview_layout}>
<PreviewBlock title="Light Scheme" baseline={scheme.schemeStorage.scheme!.light_baseline} /> <PreviewBlock title="Light Scheme" baseline={scheme.light_baseline} />
<PreviewBlock title="Dark Scheme" baseline={scheme.schemeStorage.scheme!.dark_baseline} /> <PreviewBlock title="Dark Scheme" baseline={scheme.dark_baseline} />
</div> </div>
</ScrollArea> </ScrollArea>
); );

View File

@ -6,10 +6,14 @@ import { EditableDescription } from '../components/EditableDescription';
import { EditableTitle } from '../components/EditableTitle'; import { EditableTitle } from '../components/EditableTitle';
import { SchemeSign } from '../components/SchemeSign'; import { SchemeSign } from '../components/SchemeSign';
import { MaterialDesign2SchemeStorage } from '../material-2-scheme'; import { MaterialDesign2SchemeStorage } from '../material-2-scheme';
import { MaterialDesign3SchemeStorage } from '../material-3-scheme'; import {
MaterialDesign3DynamicSchemeStorage,
MaterialDesign3SchemeStorage,
} from '../material-3-scheme';
import { SchemeContent } from '../models'; import { SchemeContent } from '../models';
import { CorruptedScheme } from '../page-components/scheme/CorruptedScheme'; import { CorruptedScheme } from '../page-components/scheme/CorruptedScheme';
import { M2Scheme } from '../page-components/scheme/M2Scheme'; import { M2Scheme } from '../page-components/scheme/M2Scheme';
import { M3DynamicScheme } from '../page-components/scheme/M3DynamicScheme';
import { M3Scheme } from '../page-components/scheme/M3Scheme'; import { M3Scheme } from '../page-components/scheme/M3Scheme';
import { QScheme } from '../page-components/scheme/QScheme'; import { QScheme } from '../page-components/scheme/QScheme';
import { SwatchScheme } from '../page-components/scheme/SwatchScheme'; import { SwatchScheme } from '../page-components/scheme/SwatchScheme';
@ -52,6 +56,10 @@ export function SchemeDetail() {
return <M2Scheme scheme={scheme as SchemeContent<MaterialDesign2SchemeStorage>} />; return <M2Scheme scheme={scheme as SchemeContent<MaterialDesign2SchemeStorage>} />;
case 'material_3': case 'material_3':
return <M3Scheme scheme={scheme as SchemeContent<MaterialDesign3SchemeStorage>} />; return <M3Scheme scheme={scheme as SchemeContent<MaterialDesign3SchemeStorage>} />;
case 'material_3_dynamic':
return (
<M3DynamicScheme scheme={scheme as SchemeContent<MaterialDesign3DynamicSchemeStorage>} />
);
default: default:
return <CorruptedScheme />; return <CorruptedScheme />;
} }