增加Material Design 2主题样式的生成和导出。

This commit is contained in:
徐涛 2025-01-20 11:04:16 +08:00
parent d07fe9d41a
commit 119436608a
6 changed files with 686 additions and 1 deletions

View File

@ -1,7 +1,7 @@
use palette::{
cam16::{Cam16Jch, Parameters},
convert::FromColorUnclamped,
IsWithinBounds, Srgb,
Hsl, IsWithinBounds, Srgb,
};
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
@ -24,3 +24,23 @@ pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch<f32>) -> String {
format!("{:x}", map_cam16jch_to_srgb(origin).into_format::<u8>())
}
pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb {
let mut new_original = Hsl::new(origin.hue, origin.saturation, origin.lightness);
const FACTOR: f32 = 0.99;
loop {
let new_srgb = Srgb::from_color_unclamped(new_original);
if new_srgb.is_within_bounds() {
break new_srgb;
}
new_original = Hsl::new(
new_original.hue,
new_original.saturation * FACTOR,
new_original.lightness,
);
}
}
pub fn map_hsl_to_srgb_hex(origin: &Hsl) -> String {
format!("{:x}", map_hsl_to_srgb(origin).into_format::<u8>())
}

View File

@ -0,0 +1,146 @@
use std::collections::HashMap;
use serde::{ser::SerializeStruct, Serialize};
use crate::{convert::map_hsl_to_srgb_hex, errors, schemes::material_design_2::swatch::M2Swatch};
use super::{color_set::M2ColorSet, swatch::SwatchIndex};
#[derive(Debug, Clone)]
pub struct M2BaselineColors {
pub primary: M2ColorSet,
pub secondary: M2ColorSet,
pub error: M2ColorSet,
pub background: M2ColorSet,
pub surface: M2ColorSet,
pub shadow: String,
pub custom_colors: HashMap<String, M2ColorSet>,
neutral_swatch: M2Swatch,
dark_set: bool,
}
impl M2BaselineColors {
pub fn new(
primary_color: &str,
secondary_color: &str,
error_color: &str,
dark_baseline: bool,
) -> Result<Self, errors::ColorError> {
let primary_swatch = M2Swatch::from_color(primary_color)?;
let secondary_swatch = M2Swatch::from_color(secondary_color)?;
let error_swatch = M2Swatch::from_color(error_color)?;
let neutral_swatch = M2Swatch::default_neutral();
Ok(Self {
primary: M2ColorSet::from_swatch(
&primary_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
secondary: M2ColorSet::from_swatch(
&secondary_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
error: M2ColorSet::from_swatch(&error_swatch, &neutral_swatch, dark_baseline, None)?,
background: M2ColorSet::from_swatch(
&neutral_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
surface: M2ColorSet::from_swatch(
&neutral_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
shadow: map_hsl_to_srgb_hex(&neutral_swatch.tone(SwatchIndex::SI900)),
custom_colors: HashMap::new(),
neutral_swatch,
dark_set: dark_baseline,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
let swatch = M2Swatch::from_color(color)?;
self.custom_colors.insert(
name.to_string(),
M2ColorSet::from_swatch(&swatch, &self.neutral_swatch, self.dark_set, None)?,
);
Ok(())
}
pub fn to_css_variable(&self) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
variable_lines.extend(self.primary.to_css_variable(&prefix, "primary"));
variable_lines.extend(self.secondary.to_css_variable(&prefix, "secondary"));
variable_lines.extend(self.error.to_css_variable(&prefix, "error"));
variable_lines.extend(self.background.to_css_variable(&prefix, "background"));
variable_lines.extend(self.surface.to_css_variable(&prefix, "surface"));
variable_lines.push(format!("--color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.custom_colors {
variable_lines.extend(color_set.to_css_variable(&prefix, name));
}
variable_lines
}
pub fn to_scss_variable(&self) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
variable_lines.extend(self.primary.to_scss_variable(&prefix, "primary"));
variable_lines.extend(self.secondary.to_scss_variable(&prefix, "secondary"));
variable_lines.extend(self.error.to_scss_variable(&prefix, "error"));
variable_lines.extend(self.background.to_scss_variable(&prefix, "background"));
variable_lines.extend(self.surface.to_scss_variable(&prefix, "surface"));
variable_lines.push(format!("$color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.custom_colors {
variable_lines.extend(color_set.to_scss_variable(&prefix, name));
}
variable_lines
}
pub fn to_javascript_object(&self) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
variable_lines.extend(self.primary.to_javascript_object(&prefix, "primary"));
variable_lines.extend(self.secondary.to_javascript_object(&prefix, "secondary"));
variable_lines.extend(self.error.to_javascript_object(&prefix, "error"));
variable_lines.extend(self.background.to_javascript_object(&prefix, "background"));
variable_lines.extend(self.surface.to_javascript_object(&prefix, "surface"));
variable_lines.push(format!("{}Shadow: '#{}',", prefix, self.shadow));
for (name, color_set) in &self.custom_colors {
variable_lines.extend(color_set.to_javascript_object(&prefix, name));
}
variable_lines
}
}
impl Serialize for M2BaselineColors {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_struct("baseline", 7)?;
map.serialize_field("primary", &self.primary)?;
map.serialize_field("secondary", &self.secondary)?;
map.serialize_field("error", &self.error)?;
map.serialize_field("background", &self.background)?;
map.serialize_field("surface", &self.surface)?;
map.serialize_field("shadow", &self.shadow)?;
map.serialize_field("custom_colors", &self.custom_colors)?;
map.end()
}
}

View File

@ -0,0 +1,94 @@
use serde::Serialize;
use crate::{convert::map_hsl_to_srgb_hex, errors};
use super::swatch::M2Swatch;
#[derive(Debug, Clone, Serialize)]
pub struct M2ColorSet {
pub root: String,
pub variant: String,
pub on: String,
}
impl M2ColorSet {
pub fn from_swatch(
swatch: &M2Swatch,
neutral: &M2Swatch,
dark: bool,
required_wacg_ratio: Option<f32>,
) -> Result<Self, errors::ColorError> {
let root_color = if dark {
map_hsl_to_srgb_hex(&swatch.desaturated_key_tone())
} else {
map_hsl_to_srgb_hex(&swatch.key_tone())
};
if dark {
Ok(Self {
variant: map_hsl_to_srgb_hex(&swatch.desaturated_tone(&swatch.key_index - 2)),
on: match required_wacg_ratio {
Some(ratio) => map_hsl_to_srgb_hex(
&neutral.desaturated_wacg_min_above(&root_color, ratio)?,
),
None => map_hsl_to_srgb_hex(&neutral.desaturated_wacg_max_tone(&root_color)?),
},
root: root_color,
})
} else {
Ok(Self {
variant: map_hsl_to_srgb_hex(&swatch.tone(&swatch.key_index + 2)),
on: match required_wacg_ratio {
Some(ratio) => {
map_hsl_to_srgb_hex(&neutral.wacg_min_above(&root_color, ratio)?)
}
None => map_hsl_to_srgb_hex(&neutral.wacg_max_tone(&root_color)?),
},
root: root_color,
})
}
}
pub fn to_css_variable(&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-{}-{}-variant: #{};",
prefix, name, self.variant
));
variable_lines.push(format!("--color-{}-on-{}: #{};", prefix, name, self.on));
variable_lines
}
pub fn to_scss_variable(&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-{}-{}-variant: #{};",
prefix, name, self.variant
));
variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on));
variable_lines
}
pub fn to_javascript_object(&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!("{}{}Variant: '#{}',", prefix, name, self.variant));
variable_lines.push(format!("{}On{}: '#{}',", prefix, name, self.on));
variable_lines
}
}

View File

@ -0,0 +1,95 @@
use baseline::M2BaselineColors;
use palette::Hsl;
use serde::Serialize;
use crate::{convert::map_hsl_to_srgb_hex, errors};
use super::SchemeExport;
pub mod baseline;
pub mod color_set;
pub mod swatch;
#[derive(Debug, Clone, Serialize)]
pub struct MaterialDesign2Scheme {
pub white: String,
pub black: String,
pub light: M2BaselineColors,
pub dark: M2BaselineColors,
}
impl MaterialDesign2Scheme {
pub fn new(primary: &str, secondary: &str, error: &str) -> Result<Self, errors::ColorError> {
Ok(Self {
white: map_hsl_to_srgb_hex(&Hsl::new(0.0, 0.0, 1.0)),
black: map_hsl_to_srgb_hex(&Hsl::new(0.0, 0.0, 0.0)),
light: M2BaselineColors::new(primary, secondary, error, false)?,
dark: M2BaselineColors::new(primary, secondary, error, true)?,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
self.light.add_custom_color(name, color)?;
self.dark.add_custom_color(name, color)?;
Ok(())
}
}
impl SchemeExport for MaterialDesign2Scheme {
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.to_css_variable());
css_variables.extend(self.dark.to_css_variable());
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.to_scss_variable());
scss_variables.extend(self.dark.to_scss_variable());
scss_variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut js_object = Vec::new();
js_object.push("{".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
.to_javascript_object()
.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
.to_javascript_object()
.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,306 @@
use std::{
ops::{Add, Sub},
str::FromStr,
};
use palette::{color_difference::Wcag21RelativeContrast, FromColor, Hsl, Srgb};
use crate::{convert::map_hsl_to_srgb, errors};
#[derive(Debug, Clone)]
pub struct M2Swatch {
_key_color: Hsl,
swatch: Vec<Hsl>,
desaturated_swatch: Vec<Hsl>,
pub key_index: SwatchIndex,
}
#[derive(Debug, Clone, Copy)]
pub enum SwatchIndex {
SI900,
SI800,
SI700,
SI600,
SI500,
SI400,
SI300,
SI200,
SI100,
SI50,
}
impl SwatchIndex {
pub fn to_usize(&self) -> usize {
match self {
SwatchIndex::SI900 => 0,
SwatchIndex::SI800 => 1,
SwatchIndex::SI700 => 2,
SwatchIndex::SI600 => 3,
SwatchIndex::SI500 => 4,
SwatchIndex::SI400 => 5,
SwatchIndex::SI300 => 6,
SwatchIndex::SI200 => 7,
SwatchIndex::SI100 => 8,
SwatchIndex::SI50 => 9,
}
}
pub fn from_usize(index: usize) -> Self {
match index {
0 => SwatchIndex::SI900,
1 => SwatchIndex::SI800,
2 => SwatchIndex::SI700,
3 => SwatchIndex::SI600,
4 => SwatchIndex::SI500,
5 => SwatchIndex::SI400,
6 => SwatchIndex::SI300,
7 => SwatchIndex::SI200,
8 => SwatchIndex::SI100,
9 => SwatchIndex::SI50,
_ => panic!("Invalid index"),
}
}
}
impl Add<i16> for SwatchIndex {
type Output = Self;
fn add(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 + rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
impl Add<i16> for &SwatchIndex {
type Output = SwatchIndex;
fn add(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 + rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
impl Sub<i16> for SwatchIndex {
type Output = Self;
fn sub(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 - rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
impl Sub<i16> for &SwatchIndex {
type Output = SwatchIndex;
fn sub(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 - rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
fn find_key_color_place(value: f32, min_value: f32, max_value: f32) -> usize {
let interval = (max_value - min_value) / 9.0;
let values = (0..=9)
.map(|i| min_value + interval * i as f32)
.collect::<Vec<f32>>();
let mut closest_index = 0;
let mut min_diff = (value - values[closest_index]).abs();
for (i, v) in values.iter().enumerate() {
let diff = (value - v).abs();
if diff < min_diff {
min_diff = diff;
closest_index = i;
}
}
closest_index
}
impl M2Swatch {
pub fn default_neutral() -> Self {
let key_color = Hsl::new(0.0, 0.0, 0.0);
let mut swatch = Vec::new();
let interval: f32 = 1.0 / 9.0;
for i in 0..=9 {
swatch.push(Hsl::new(0.0, 0.0, interval * i as f32));
}
Self {
_key_color: key_color,
desaturated_swatch: swatch.clone(),
swatch,
key_index: SwatchIndex::SI900,
}
}
pub fn from_color(color: &str) -> Result<Self, errors::ColorError> {
let key_color = Hsl::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let mut swatch = Vec::new();
let mut desaturated_swatch = Vec::new();
let max_lightness = key_color.lightness.max(0.95);
let min_lightness = key_color.lightness.min(0.2);
let max_staturation = key_color.saturation.max(0.80);
let min_staturation = key_color.saturation.min(0.45);
let key_place = find_key_color_place(key_color.lightness, min_lightness, max_lightness);
if key_place > 0 {
let lightness_interval = (key_color.lightness - min_lightness) / key_place as f32;
let saturation_interval = (max_staturation - key_color.saturation) / key_place as f32;
for i in 0..key_place {
swatch.push(Hsl::new(
key_color.hue,
max_staturation - saturation_interval * i as f32,
min_lightness + lightness_interval * i as f32,
));
desaturated_swatch.push(Hsl::new(
key_color.hue,
(min_staturation + saturation_interval * i as f32) * 0.7_f32,
min_lightness + lightness_interval * i as f32,
));
}
}
swatch.push(key_color.clone());
if key_place < 9 {
let lightness_interval = (max_lightness - key_color.lightness) / (9 - key_place) as f32;
let saturation_interval =
(key_color.saturation - min_staturation) / (9 - key_place) as f32;
for i in 1..=9 - key_place {
swatch.push(Hsl::new(
key_color.hue,
key_color.saturation - saturation_interval * i as f32,
key_color.lightness + lightness_interval * i as f32,
));
desaturated_swatch.push(Hsl::new(
key_color.hue,
(min_staturation + saturation_interval * i as f32) * 0.7_f32,
key_color.lightness + lightness_interval * i as f32,
))
}
}
Ok(Self {
_key_color: key_color,
swatch,
desaturated_swatch,
key_index: SwatchIndex::from_usize(key_place),
})
}
pub fn tone(&self, index: SwatchIndex) -> Hsl {
self.swatch[index.to_usize()].clone()
}
pub fn key_tone(&self) -> Hsl {
self.swatch[self.key_index.to_usize()].clone()
}
pub fn desaturated_tone(&self, index: SwatchIndex) -> Hsl {
self.desaturated_swatch[index.to_usize()].clone()
}
pub fn desaturated_key_tone(&self) -> Hsl {
self.desaturated_swatch[self.key_index.to_usize()].clone()
}
pub fn wacg_max_tone(&self, reference_color: &str) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut max_wacg_index = 0;
let mut max_wacg = 0.0;
for (i, tone) in self.swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg > max_wacg {
max_wacg = wacg;
max_wacg_index = i;
}
}
Ok(self.swatch[max_wacg_index].clone())
}
pub fn desaturated_wacg_max_tone(
&self,
reference_color: &str,
) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut max_wacg_index = 0;
let mut max_wacg = 0.0;
for (i, tone) in self.desaturated_swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg > max_wacg {
max_wacg = wacg;
max_wacg_index = i;
}
}
Ok(self.desaturated_swatch[max_wacg_index].clone())
}
pub fn wacg_min_above(
&self,
reference_color: &str,
ratio: f32,
) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut min_wacg_index = 0;
let mut min_wacg = 21.0;
for (i, tone) in self.swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg < min_wacg && wacg > ratio {
min_wacg = wacg;
min_wacg_index = i;
}
}
Ok(self.swatch[min_wacg_index].clone())
}
pub fn desaturated_wacg_min_above(
&self,
reference_color: &str,
ratio: f32,
) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut min_wacg_index = 0;
let mut min_wacg = 21.0;
for (i, tone) in self.desaturated_swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg < min_wacg && wacg > ratio {
min_wacg = wacg;
min_wacg_index = i;
}
}
Ok(self.desaturated_swatch[min_wacg_index].clone())
}
}

View File

@ -1,10 +1,12 @@
use std::collections::HashMap;
use material_design_2::MaterialDesign2Scheme;
use material_design_3::MaterialDesign3Scheme;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::errors;
pub mod material_design_2;
pub mod material_design_3;
pub trait SchemeExport {
@ -33,3 +35,25 @@ pub fn generate_material_design_3_scheme(
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_material_design_2_scheme(
primary_color: &str,
secondary_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 = MaterialDesign2Scheme::new(primary_color, secondary_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)?)
}