增加从WASM中生成Material Design 3主题样式的功能。

This commit is contained in:
徐涛 2025-01-17 15:41:44 +08:00
parent c9626f3b8e
commit d07fe9d41a
9 changed files with 845 additions and 0 deletions

View File

@ -0,0 +1,26 @@
use palette::{
cam16::{Cam16Jch, Parameters},
convert::FromColorUnclamped,
IsWithinBounds, Srgb,
};
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
let mut new_original = Cam16Jch::new(origin.lightness, origin.chroma, origin.hue);
const FACTOR: f32 = 0.99;
loop {
let new_srgb =
Srgb::from_color_unclamped(new_original.into_xyz(Parameters::default_static_wp(40.0)));
if new_srgb.is_within_bounds() {
break new_srgb;
}
new_original = Cam16Jch::new(
new_original.lightness,
new_original.chroma * FACTOR,
new_original.hue,
);
}
}
pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch<f32>) -> String {
format!("{:x}", map_cam16jch_to_srgb(origin).into_format::<u8>())
}

View File

@ -7,6 +7,10 @@ pub enum ColorError {
UnrecogniazedRGB(String),
#[error("Some color component is out of bounds")]
ComponentOutOfBounds,
#[error("Unable to parse argument")]
UnableToParseArgument,
#[error("Unable to assemble output")]
UnableToAssembleOutput,
}
impl Into<JsValue> for ColorError {

View File

@ -14,9 +14,11 @@ use wasm_bindgen::prelude::*;
mod color_card;
mod color_differ;
mod convert;
mod errors;
mod palettes;
mod reversing;
mod schemes;
#[wasm_bindgen]
pub fn represent_rgb(color: &str) -> Result<Box<[u8]>, errors::ColorError> {

View File

@ -0,0 +1,157 @@
use std::collections::HashMap;
use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex;
use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette};
#[derive(Debug, Clone, Serialize)]
pub struct M3BaselineColors {
pub primary: M3ColorSet,
pub secondary: M3ColorSet,
pub tertiary: M3ColorSet,
pub error: M3ColorSet,
pub surface: M3SurfaceSet,
pub outline: String,
pub outline_variant: String,
pub scrim: String,
pub shadown: String,
pub customs: HashMap<String, M3ColorSet>,
dark_set: bool,
}
impl M3BaselineColors {
pub fn new(
p: &TonalPalette,
s: &TonalPalette,
t: &TonalPalette,
n: &TonalPalette,
nv: &TonalPalette,
e: &TonalPalette,
dark_set: bool,
) -> Self {
let color_set_generator = if dark_set {
M3ColorSet::new_dark_set
} else {
M3ColorSet::new_light_set
};
let surface_set_generator = if dark_set {
M3SurfaceSet::new_dark_set
} else {
M3SurfaceSet::new_light_set
};
let primary = color_set_generator(p);
let secondary = color_set_generator(s);
let tertiary = color_set_generator(t);
let surface = surface_set_generator(n, nv);
let error = color_set_generator(e);
let outline = if dark_set { n.tone(60.0) } else { n.tone(50.0) };
let outline_variant = if dark_set {
nv.tone(30.0)
} else {
nv.tone(80.0)
};
let scrim = n.tone(0.0);
let shadow = n.tone(0.0);
Self {
primary,
secondary,
tertiary,
error,
surface,
outline: map_cam16jch_to_srgb_hex(&outline),
outline_variant: map_cam16jch_to_srgb_hex(&outline_variant),
scrim: map_cam16jch_to_srgb_hex(&scrim),
shadown: map_cam16jch_to_srgb_hex(&shadow),
customs: HashMap::new(),
dark_set,
}
}
pub fn add_custom_set(&mut self, name: String, c: &TonalPalette) {
let color_set_generator = if self.dark_set {
M3ColorSet::new_dark_set
} else {
M3ColorSet::new_light_set
};
self.customs.insert(name, color_set_generator(c));
}
pub fn to_css_variables(&self) -> Vec<String> {
let mut css_variables = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
css_variables.extend(self.primary.to_css_variables(prefix, "primary"));
css_variables.extend(self.secondary.to_css_variables(prefix, "secondary"));
css_variables.extend(self.tertiary.to_css_variables(prefix, "tertiary"));
css_variables.extend(self.error.to_css_variables(prefix, "error"));
css_variables.extend(self.surface.to_css_variables(prefix));
css_variables.push(format!("--color-{}-outline: #{};", prefix, self.outline));
css_variables.push(format!(
"--color-{}-outline-variant: #{};",
prefix, self.outline_variant
));
css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim));
css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadown));
for (name, color_set) in &self.customs {
css_variables.extend(color_set.to_css_variables(prefix, name));
}
css_variables
}
pub fn to_scss_variables(&self) -> Vec<String> {
let mut scss_variables = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
scss_variables.extend(self.primary.to_scss_variables(prefix, "primary"));
scss_variables.extend(self.secondary.to_scss_variables(prefix, "secondary"));
scss_variables.extend(self.tertiary.to_scss_variables(prefix, "tertiary"));
scss_variables.extend(self.error.to_scss_variables(prefix, "error"));
scss_variables.extend(self.surface.to_scss_variables(prefix));
scss_variables.push(format!("$color-{}-outline: #{};", prefix, self.outline));
scss_variables.push(format!(
"$color-{}-outline-variant: #{};",
prefix, self.outline_variant
));
scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim));
scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadown));
for (name, color_set) in &self.customs {
scss_variables.extend(color_set.to_scss_variables(prefix, name));
}
scss_variables
}
pub fn to_javascript_object_fields(&self) -> Vec<String> {
let mut js_object_fields = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
js_object_fields.extend(self.primary.to_javascript_object_fields(prefix, "primary"));
js_object_fields.extend(
self.secondary
.to_javascript_object_fields(prefix, "secondary"),
);
js_object_fields.extend(
self.tertiary
.to_javascript_object_fields(prefix, "tertiary"),
);
js_object_fields.extend(self.error.to_javascript_object_fields(prefix, "error"));
js_object_fields.extend(self.surface.to_javascript_object_fields(prefix));
js_object_fields.push(format!("{}Outline: '#{}',", prefix, self.outline));
js_object_fields.push(format!(
"{}OutlineVariant: '#{}',",
prefix, self.outline_variant
));
js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim));
js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadown));
for (name, color_set) in &self.customs {
js_object_fields.extend(color_set.to_javascript_object_fields(prefix, name));
}
js_object_fields
}
}

View File

@ -0,0 +1,181 @@
use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex;
use super::tonal_palette::TonalPalette;
#[derive(Debug, Clone, Serialize)]
pub struct M3ColorSet {
pub root: String,
pub on_root: String,
pub container: String,
pub on_conatiner: String,
pub fixed: String,
pub fixed_dim: String,
pub on_fixed: String,
pub fixed_variant: String,
pub inverse: String,
}
impl M3ColorSet {
pub fn new_light_set(palette: &TonalPalette) -> Self {
let root = palette.tone(40.0);
let on_root = palette.tone(100.0);
let container = palette.tone(90.0);
let on_container = palette.tone(30.0);
let fixed = palette.tone(90.0);
let fixed_dim = palette.tone(80.0);
let on_fixed = palette.tone(10.0);
let fixed_variant = palette.tone(30.0);
let inverse = palette.tone(80.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
on_root: map_cam16jch_to_srgb_hex(&on_root),
container: map_cam16jch_to_srgb_hex(&container),
on_conatiner: map_cam16jch_to_srgb_hex(&on_container),
fixed: map_cam16jch_to_srgb_hex(&fixed),
fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim),
on_fixed: map_cam16jch_to_srgb_hex(&on_fixed),
fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
}
}
pub fn new_dark_set(palette: &TonalPalette) -> Self {
let root = palette.tone(80.0);
let on_root = palette.tone(20.0);
let container = palette.tone(30.0);
let on_container = palette.tone(90.0);
let fixed = palette.tone(90.0);
let fixed_dim = palette.tone(80.0);
let on_fixed = palette.tone(10.0);
let fixed_variant = palette.tone(30.0);
let inverse = palette.tone(40.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
on_root: map_cam16jch_to_srgb_hex(&on_root),
container: map_cam16jch_to_srgb_hex(&container),
on_conatiner: map_cam16jch_to_srgb_hex(&on_container),
fixed: map_cam16jch_to_srgb_hex(&fixed),
fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim),
on_fixed: map_cam16jch_to_srgb_hex(&on_fixed),
fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
}
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
variable_lines.push(format!("--color-{}-{}: #{};", prefix, name, self.root));
variable_lines.push(format!(
"--color-{}-on-{}: #{};",
prefix, name, self.on_root
));
variable_lines.push(format!(
"--color-{}-{}-container: #{};",
prefix, name, self.container
));
variable_lines.push(format!(
"--color-{}-on-{}-container: #{};",
prefix, name, self.on_conatiner
));
variable_lines.push(format!(
"--color-{}-{}-fixed: #{};",
prefix, name, self.fixed
));
variable_lines.push(format!(
"--color-{}-{}-fixed-dim: #{};",
prefix, name, self.fixed_dim
));
variable_lines.push(format!(
"--color-{}-on-{}-fixed: #{};",
prefix, name, self.on_fixed
));
variable_lines.push(format!(
"--color-{}-on-{}-fixed-variant: #{};",
prefix, name, self.fixed_variant
));
variable_lines.push(format!(
"--color-{}-inverse-{}: #{};",
prefix, name, self.inverse
));
variable_lines
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
variable_lines.push(format!("$color-{}-{}: #{};", prefix, name, self.root));
variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on_root));
variable_lines.push(format!(
"$color-{}-{}-container: #{};",
prefix, name, self.container
));
variable_lines.push(format!(
"$color-{}-on-{}-container: #{};",
prefix, name, self.on_conatiner
));
variable_lines.push(format!(
"$color-{}-{}-fixed: #{};",
prefix, name, self.fixed
));
variable_lines.push(format!(
"$color-{}-{}-fixed-dim: #{};",
prefix, name, self.fixed_dim
));
variable_lines.push(format!(
"$color-{}-on-{}-fixed: #{};",
prefix, name, self.on_fixed
));
variable_lines.push(format!(
"$color-{}-on-{}-fixed-variant: #{};",
prefix, name, self.fixed_variant
));
variable_lines.push(format!(
"$color-{}-inverse-{}: #{};",
prefix, name, self.inverse
));
variable_lines
}
pub fn to_javascript_object_fields(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = prefix.to_ascii_lowercase();
let name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variable_lines.push(format!("{}{}: '#{}',", prefix, name, self.root));
variable_lines.push(format!("{}on{}: '#{}',", prefix, name, self.on_root));
variable_lines.push(format!(
"{}{}Container: '#{}',",
prefix, name, self.container
));
variable_lines.push(format!(
"{}On{}Container: '#{}',",
prefix, name, self.on_conatiner
));
variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed));
variable_lines.push(format!(
"{}{}FixedDim: '#{}',",
prefix, name, self.fixed_dim
));
variable_lines.push(format!("{}On{}Fixed: '#{}',", prefix, name, self.on_fixed));
variable_lines.push(format!(
"{}On{}FixedVariant: '#{}',",
prefix, name, self.fixed_variant
));
variable_lines.push(format!("{}Inverse{}: '#{}',", prefix, name, self.inverse));
variable_lines
}
}

View File

@ -0,0 +1,130 @@
use std::str::FromStr;
use baseline::M3BaselineColors;
use palette::cam16::{Cam16Jch, Parameters};
use palette::{IntoColor, Srgb};
use serde::Serialize;
use tonal_palette::TonalPalette;
use crate::convert::map_cam16jch_to_srgb_hex;
use crate::errors;
use super::SchemeExport;
mod baseline;
mod color_set;
mod surface;
mod tonal_palette;
#[derive(Debug, Clone, Serialize)]
pub struct MaterialDesign3Scheme {
pub white: String,
pub black: String,
pub light_baseline: M3BaselineColors,
pub dark_baseline: M3BaselineColors,
}
impl MaterialDesign3Scheme {
pub fn new(source_color: &str, error_color: &str) -> Result<Self, errors::ColorError> {
let source = Cam16Jch::from_xyz(
Srgb::from_str(source_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let error = Cam16Jch::from_xyz(
Srgb::from_str(error_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let source_hue = source.hue.into_positive_degrees();
let p = TonalPalette::from_hue_and_chroma(source_hue, source.chroma);
let s = TonalPalette::from_hue_and_chroma(source_hue, source.chroma / 3.0);
let t = TonalPalette::from_hue_and_chroma(source_hue + 60.0, source.chroma / 2.0);
let n = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 12.0).min(4.0));
let nv = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 6.0).min(8.0));
let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0);
Ok(Self {
white: map_cam16jch_to_srgb_hex(&Cam16Jch::new(100.0, 0.0, 0.0)),
black: map_cam16jch_to_srgb_hex(&Cam16Jch::new(0.0, 0.0, 0.0)),
light_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, false),
dark_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, true),
})
}
pub fn add_custom_color(
&mut self,
name: String,
color: String,
) -> Result<(), errors::ColorError> {
let custom_color = Cam16Jch::from_xyz(
Srgb::from_str(&color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let hue = custom_color.hue.into_positive_degrees();
let palette = TonalPalette::from_hue_and_chroma(hue, custom_color.chroma);
self.light_baseline.add_custom_set(name.clone(), &palette);
self.dark_baseline.add_custom_set(name.clone(), &palette);
Ok(())
}
}
impl SchemeExport for MaterialDesign3Scheme {
fn output_css_variables(&self) -> String {
let mut css_variables = Vec::new();
css_variables.push(format!("--color-white: #{};", self.white));
css_variables.push(format!("--color-black: #{};", self.black));
css_variables.extend(self.light_baseline.to_css_variables());
css_variables.extend(self.dark_baseline.to_css_variables());
css_variables.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut scss_variables = Vec::new();
scss_variables.push(format!("$color-white: #{};", self.white));
scss_variables.push(format!("$color-black: #{};", self.black));
scss_variables.extend(self.light_baseline.to_scss_variables());
scss_variables.extend(self.dark_baseline.to_scss_variables());
scss_variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut js_object = Vec::new();
js_object.push("const colorScheme = {".to_string());
js_object.push(format!(" white: '#{}'", self.white));
js_object.push(format!(" black: '#{}'", self.black));
js_object.push(" light: {".to_string());
js_object.extend(
self.light_baseline
.to_javascript_object_fields()
.into_iter()
.map(|s| format!(" {}", s))
.collect::<Vec<String>>(),
);
js_object.push(" },".to_string());
js_object.push(" dark: {".to_string());
js_object.extend(
self.dark_baseline
.to_javascript_object_fields()
.into_iter()
.map(|s| format!(" {}", s))
.collect::<Vec<String>>(),
);
js_object.push(" },".to_string());
js_object.push("}".to_string());
js_object.join(",\n")
}
}

View File

@ -0,0 +1,215 @@
use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex;
use super::tonal_palette::TonalPalette;
#[derive(Debug, Clone, Serialize)]
pub struct M3SurfaceSet {
pub root: String,
pub dim: String,
pub bright: String,
pub container: String,
pub container_lowest: String,
pub container_low: String,
pub container_high: String,
pub container_highest: String,
pub on_root: String,
pub on_root_variant: String,
pub inverse: String,
pub on_inverse: String,
}
impl M3SurfaceSet {
pub fn new_light_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self {
let root = neutral.tone(98.0);
let dim = neutral.tone(87.0);
let bright = neutral.tone(98.0);
let container = neutral.tone(94.0);
let container_lowest = neutral.tone(100.0);
let container_low = neutral.tone(96.0);
let container_high = neutral.tone(92.0);
let container_highest = neutral.tone(90.0);
let on_root = neutral_variant.tone(10.0);
let on_root_variant = neutral_variant.tone(30.0);
let inverse = neutral.tone(20.0);
let on_inverse = neutral_variant.tone(95.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
dim: map_cam16jch_to_srgb_hex(&dim),
bright: map_cam16jch_to_srgb_hex(&bright),
container: map_cam16jch_to_srgb_hex(&container),
container_lowest: map_cam16jch_to_srgb_hex(&container_lowest),
container_low: map_cam16jch_to_srgb_hex(&container_low),
container_high: map_cam16jch_to_srgb_hex(&container_high),
container_highest: map_cam16jch_to_srgb_hex(&container_highest),
on_root: map_cam16jch_to_srgb_hex(&on_root),
on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
on_inverse: map_cam16jch_to_srgb_hex(&on_inverse),
}
}
pub fn new_dark_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self {
let root = neutral.tone(6.0);
let dim = neutral.tone(6.0);
let bright = neutral.tone(24.0);
let container = neutral.tone(12.0);
let container_lowest = neutral.tone(4.0);
let container_low = neutral.tone(10.0);
let container_high = neutral.tone(17.0);
let container_highest = neutral.tone(22.0);
let on_root = neutral_variant.tone(90.0);
let on_root_variant = neutral_variant.tone(80.0);
let inverse = neutral.tone(90.0);
let on_inverse = neutral_variant.tone(20.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
dim: map_cam16jch_to_srgb_hex(&dim),
bright: map_cam16jch_to_srgb_hex(&bright),
container: map_cam16jch_to_srgb_hex(&container),
container_lowest: map_cam16jch_to_srgb_hex(&container_lowest),
container_low: map_cam16jch_to_srgb_hex(&container_low),
container_high: map_cam16jch_to_srgb_hex(&container_high),
container_highest: map_cam16jch_to_srgb_hex(&container_highest),
on_root: map_cam16jch_to_srgb_hex(&on_root),
on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
on_inverse: map_cam16jch_to_srgb_hex(&on_inverse),
}
}
pub fn to_css_variables(&self, prefix: &str) -> Vec<String> {
let mut css_variables = Vec::new();
css_variables.push(format!("--color-{}-surface: ${};", prefix, self.root));
css_variables.push(format!("--color-{}-surface-dim: ${};", prefix, self.dim));
css_variables.push(format!(
"--color-{}-surface-bright: ${};",
prefix, self.bright
));
css_variables.push(format!(
"--color-{}-surface-container: ${};",
prefix, self.container
));
css_variables.push(format!(
"--color-{}-surface-container-lowest: ${};",
prefix, self.container_lowest
));
css_variables.push(format!(
"--color-{}-surface-container-low: ${};",
prefix, self.container_low
));
css_variables.push(format!(
"--color-{}-surface-container-high: ${};",
prefix, self.container_high
));
css_variables.push(format!(
"--color-{}-surface-container-highest: ${};",
prefix, self.container_highest
));
css_variables.push(format!("--color-{}-on-surface: ${};", prefix, self.on_root));
css_variables.push(format!(
"--color-{}-on-surface-variant: ${};",
prefix, self.on_root_variant
));
css_variables.push(format!(
"--color-{}-inverse-surface: ${};",
prefix, self.inverse
));
css_variables.push(format!(
"--color-{}-inverse-on-surface: ${};",
prefix, self.on_inverse
));
css_variables
}
pub fn to_scss_variables(&self, prefix: &str) -> Vec<String> {
let mut scss_variables = Vec::new();
scss_variables.push(format!("$color-{}-surface: ${};", prefix, self.root));
scss_variables.push(format!("$color-{}-surface-dim: ${};", prefix, self.dim));
scss_variables.push(format!(
"$color-{}-surface-bright: ${};",
prefix, self.bright
));
scss_variables.push(format!(
"$color-{}-surface-container: ${};",
prefix, self.container
));
scss_variables.push(format!(
"$color-{}-surface-container-lowest: ${};",
prefix, self.container_lowest
));
scss_variables.push(format!(
"$color-{}-surface-container-low: ${};",
prefix, self.container_low
));
scss_variables.push(format!(
"$color-{}-surface-container-high: ${};",
prefix, self.container_high
));
scss_variables.push(format!(
"$color-{}-surface-container-highest: ${};",
prefix, self.container_highest
));
scss_variables.push(format!("$color-{}-on-surface: ${};", prefix, self.on_root));
scss_variables.push(format!(
"$color-{}-on-surface-variant: ${};",
prefix, self.on_root_variant
));
scss_variables.push(format!(
"$color-{}-inverse-surface: ${};",
prefix, self.inverse
));
scss_variables.push(format!(
"$color-{}-inverse-on-surface: ${};",
prefix, self.on_inverse
));
scss_variables
}
pub fn to_javascript_object_fields(&self, prefix: &str) -> Vec<String> {
let mut js_object_fields = Vec::new();
js_object_fields.push(format!("{}Surface: '#{}',", prefix, self.root));
js_object_fields.push(format!("{}SurfaceDim: '#{}',", prefix, self.dim));
js_object_fields.push(format!("{}SurfaceBright: '#{}',", prefix, self.bright));
js_object_fields.push(format!(
"{}SurfaceContainer: '#{}',",
prefix, self.container
));
js_object_fields.push(format!(
"{}SurfaceContainerLowest: '#{}',",
prefix, self.container_lowest
));
js_object_fields.push(format!(
"{}SurfaceContainerLow: '#{}',",
prefix, self.container_low
));
js_object_fields.push(format!(
"{}SurfaceContainerHigh: '#{}',",
prefix, self.container_high
));
js_object_fields.push(format!(
"{}SurfaceContainerHighest: '#{}',",
prefix, self.container_highest
));
js_object_fields.push(format!("{}OnSurface: '#{}',", prefix, self.on_root));
js_object_fields.push(format!(
"{}OnSurfaceVariant: '#{}',",
prefix, self.on_root_variant
));
js_object_fields.push(format!("{}InverseSurface: '#{}',", prefix, self.inverse));
js_object_fields.push(format!(
"{}InverseOnSurface: '#{}',",
prefix, self.on_inverse
));
js_object_fields
}
}

View File

@ -0,0 +1,95 @@
use std::str::FromStr;
use palette::{
cam16::{Cam16Jch, Parameters},
IntoColor, Srgb,
};
use crate::errors;
#[derive(Debug, Clone)]
pub struct TonalPalette {
pub key_color: Cam16Jch<f32>,
}
#[inline]
fn approximately_equal(a: f32, b: f32) -> bool {
const EPSILON: f32 = 0.000001;
(a - b).abs() < EPSILON
}
#[inline]
fn find_max_chroma(cache: &mut Vec<(f32, f32)>, hue: f32, tone: f32) -> f32 {
for (k, v) in cache.iter() {
if approximately_equal(*k, tone) {
return *v;
}
}
let chroma = Cam16Jch::new(tone, 200.0, hue).chroma;
cache.push((tone, chroma));
chroma
}
fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> {
let mut max_chroma_cache = Vec::new();
let hue = if hue >= 360.0 { hue - 360.0 } else { hue };
const PIVOT_TONE: f32 = 50.0;
const TONE_STEP_SIZE: f32 = 1.0;
const EPSILON: f32 = 0.01;
let mut lower_tone = 0.0_f32;
let mut upper_tone = 100.0_f32;
while lower_tone < upper_tone {
let mid_tone = ((lower_tone + upper_tone) / 2.0).floor();
let is_ascending = find_max_chroma(&mut max_chroma_cache, hue, mid_tone)
< find_max_chroma(&mut max_chroma_cache, hue, mid_tone + TONE_STEP_SIZE);
let sufficient_chroma =
find_max_chroma(&mut max_chroma_cache, hue, mid_tone) >= chroma - EPSILON;
if sufficient_chroma {
if (lower_tone - PIVOT_TONE).abs() < (upper_tone - PIVOT_TONE).abs() {
upper_tone = mid_tone;
} else {
if approximately_equal(lower_tone, mid_tone) {
return Cam16Jch::new(lower_tone, chroma, hue);
}
lower_tone = mid_tone;
}
} else {
if is_ascending {
lower_tone = mid_tone + TONE_STEP_SIZE;
} else {
upper_tone = mid_tone;
}
}
}
Cam16Jch::new(lower_tone, chroma, hue)
}
impl TryFrom<String> for TonalPalette {
type Error = errors::ColorError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let key_color = Cam16Jch::from_xyz(
Srgb::from_str(&value)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(value))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
Ok(TonalPalette { key_color })
}
}
impl TonalPalette {
pub fn from_hue_and_chroma(hue: f32, chroma: f32) -> Self {
let key_color = from_hue_and_chroma(hue, chroma);
TonalPalette { key_color }
}
pub fn tone(&self, tone: f32) -> Cam16Jch<f32> {
let toned_color = Cam16Jch::new(tone, self.key_color.chroma, self.key_color.hue);
toned_color
}
}

View File

@ -0,0 +1,35 @@
use std::collections::HashMap;
use material_design_3::MaterialDesign3Scheme;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::errors;
pub mod material_design_3;
pub trait SchemeExport {
fn output_css_variables(&self) -> String;
fn output_scss_variables(&self) -> String;
fn output_javascript_object(&self) -> String;
}
#[wasm_bindgen]
pub fn generate_material_design_3_scheme(
source_color: &str,
error_color: &str,
custom_colors: JsValue,
) -> Result<JsValue, errors::ColorError> {
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let mut scheme = MaterialDesign3Scheme::new(source_color, error_color)?;
for (name, color) in custom_colors {
scheme.add_custom_color(name, color)?;
}
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}