feat(q_style_2): 新增QStyle2颜色方案模块

添加QStyle2颜色方案模块,包含基础颜色集、色板生成和自动配色功能
实现颜色方案的CSS、SCSS和JavaScript输出支持
新增generate_q_scheme_2_manually函数用于手动生成QStyle2方案
This commit is contained in:
徐涛 2025-07-14 09:03:59 +08:00
parent 2bbb46ced1
commit bd4a2c9b49
6 changed files with 1134 additions and 3 deletions

View File

@ -7,12 +7,13 @@ use q_style::{QScheme, SchemeSetting};
use swatch_style::{SwatchEntry, SwatchSchemeSetting};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::errors;
use crate::{errors, schemes::q_style_2::QScheme2};
pub mod material_design_2;
pub mod material_design_3;
pub mod material_design_3_dynamic;
pub mod q_style;
pub mod q_style_2;
pub mod swatch_style;
pub trait SchemeExport {
@ -135,6 +136,50 @@ pub fn generate_q_scheme_manually(
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_2_manually(
primary_color: &str,
secondary_color: Option<String>,
tertiary_color: Option<String>,
accent_color: Option<String>,
danger_color: &str,
success_color: &str,
warn_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
custom_colors: JsValue,
setting: SchemeSetting,
) -> Result<JsValue, errors::ColorError> {
let mut scheme = QScheme2::new(
primary_color,
secondary_color.as_deref(),
tertiary_color.as_deref(),
accent_color.as_deref(),
danger_color,
success_color,
warn_color,
info_color,
fg_color,
bg_color,
setting,
)?;
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
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_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_swatch_scheme(
colors: Vec<SwatchEntry>,

View File

@ -3,7 +3,6 @@ use std::str::FromStr;
use baseline::Baseline;
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use scheme_setting::{ColorExpand, WACGSetting};
use serde::Serialize;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
@ -16,7 +15,8 @@ mod color_set;
mod neutral_swatch;
mod scheme_setting;
pub use scheme_setting::{ColorShifting, SchemeSetting};
pub use neutral_swatch::NeutralSwatch;
pub use scheme_setting::{ColorExpand, ColorShifting, SchemeSetting, WACGSetting};
#[derive(Debug, Clone, Serialize)]
pub struct QScheme {

View File

@ -0,0 +1,545 @@
use std::{collections::HashMap, sync::Arc};
use linked_hash_map::LinkedHashMap;
use palette::{
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
Oklch, ShiftHue,
};
use serde::{ser::SerializeStruct, Serialize};
use crate::{
convert::map_oklch_to_srgb_hex,
errors,
schemes::{
q_style::{ColorExpand, NeutralSwatch, SchemeSetting},
q_style_2::{color_set::ColorSet, swatch::Swatch},
},
};
#[derive(Debug, Clone)]
pub struct ColorUnit {
pub root: ColorSet,
pub surface: ColorSet,
pub swatch: Swatch,
}
impl ColorUnit {
pub fn new(
color: &Oklch,
neutral_swatch: &Arc<NeutralSwatch>,
foreground_lightness: f32,
settings: &Arc<SchemeSetting>,
) -> Self {
let root = ColorSet::new(color, neutral_swatch, foreground_lightness, settings);
let surface = root.generate_surface_set();
let swatch = root.generate_swatch();
Self {
root,
surface,
swatch,
}
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut css_variables = Vec::new();
css_variables.extend(self.root.to_css_variables(prefix, name));
css_variables.extend(self.surface.to_css_variables(prefix, name));
css_variables.extend(self.swatch.to_css_variables(prefix, name));
css_variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut css_auto_scheme_collection = LinkedHashMap::new();
css_auto_scheme_collection.extend(self.root.to_css_auto_scheme_collection(name));
css_auto_scheme_collection.extend(self.surface.to_css_auto_scheme_collection(name));
css_auto_scheme_collection.extend(self.swatch.to_css_auto_scheme_collection(name));
css_auto_scheme_collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut scss_variables = Vec::new();
scss_variables.extend(self.root.to_scss_variables(prefix, name));
scss_variables.extend(self.surface.to_scss_variables(prefix, name));
scss_variables.extend(self.swatch.to_scss_variables(prefix, name));
scss_variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut js_object_fields = Vec::new();
js_object_fields.extend(self.root.to_javascript_fields(name));
js_object_fields.extend(self.surface.to_javascript_fields(name));
js_object_fields.extend(self.swatch.to_javascript_fields(name));
js_object_fields
}
}
impl Serialize for ColorUnit {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("ColorUnit", 3)?;
state.serialize_field("root", &self.root)?;
state.serialize_field("surface", &self.surface)?;
state.serialize_field("swatch", &self.swatch)?;
state.end()
}
}
#[derive(Debug, Clone)]
pub struct Baseline {
pub primary: ColorUnit,
pub secondary: Option<ColorUnit>,
pub tertiary: Option<ColorUnit>,
pub accent: Option<ColorUnit>,
pub neutral: ColorSet,
pub neutral_variant: ColorSet,
pub surface: ColorSet,
pub surface_variant: ColorSet,
pub neutral_swatch: Arc<NeutralSwatch>,
pub danger: ColorUnit,
pub success: ColorUnit,
pub warn: ColorUnit,
pub info: ColorUnit,
pub custom_colors: HashMap<String, ColorUnit>,
pub shadow: Oklch,
pub overlay: Oklch,
pub outline: Oklch,
pub outline_variant: Oklch,
pub neutral_lightness: f32,
pub scheme_settings: Arc<SchemeSetting>,
pub is_dark: bool,
}
impl Baseline {
pub fn new(
primary: &Oklch,
secondary: Option<&Oklch>,
tertiary: Option<&Oklch>,
accent: Option<&Oklch>,
danger: &Oklch,
success: &Oklch,
warn: &Oklch,
info: &Oklch,
neutral_lightest: &Oklch,
neutral_darkest: &Oklch,
settings: &Arc<SchemeSetting>,
is_dark: bool,
) -> Self {
let (final_secondary, final_tertiary, final_accent) = match settings.expand_method {
ColorExpand::Complementary => {
let sec_color = secondary.cloned().or(Some(primary.complementary()));
(sec_color, tertiary.cloned(), accent.cloned())
}
ColorExpand::Analogous => {
let analogous_color = primary.analogous();
(
secondary.cloned().or(Some(analogous_color.0)),
tertiary.cloned().or(Some(analogous_color.1)),
accent.cloned(),
)
}
ColorExpand::AnalogousAndComplementary => {
let analogous_color = primary.analogous();
let complementary_color = primary.complementary();
(
secondary.cloned().or(Some(analogous_color.0)),
tertiary.cloned().or(Some(analogous_color.1)),
accent.cloned().or(Some(complementary_color)),
)
}
ColorExpand::Triadic => {
let triadic_color = primary.triadic();
(
secondary.cloned().or(Some(triadic_color.0)),
tertiary.cloned().or(Some(triadic_color.1)),
accent.cloned(),
)
}
ColorExpand::SplitComplementary => {
let split_complementary_color = primary.split_complementary();
(
secondary.cloned().or(Some(split_complementary_color.0)),
tertiary.cloned(),
accent.cloned().or(Some(split_complementary_color.1)),
)
}
ColorExpand::Tetradic => {
let tetradic_color = primary.tetradic();
(
secondary.cloned().or(Some(tetradic_color.0)),
tertiary.cloned().or(Some(tetradic_color.2)),
accent.cloned().or(Some(tetradic_color.1)),
)
}
ColorExpand::Square => {
let c_90 = primary.shift_hue(90.0);
let complementary_color = primary.complementary();
let c_270 = primary.shift_hue(270.0);
(
secondary.cloned().or(Some(c_90)),
tertiary.cloned().or(Some(c_270)),
accent.cloned().or(Some(complementary_color)),
)
}
};
let reference_lightness = if is_dark {
neutral_lightest.l
} else {
neutral_darkest.l
};
let neutral_swatch = Arc::new(NeutralSwatch::new(*neutral_lightest, *neutral_darkest));
let outline_color = neutral_swatch.get(reference_lightness * 0.8);
let outline_variant_color = neutral_swatch.get(reference_lightness * 0.6);
let shadow_color = neutral_swatch.get(10.0);
let overlay_color = neutral_swatch.get(reference_lightness * 0.2);
let neutral_color = neutral_swatch.get(75.0);
let neutral_variant_color = neutral_swatch.get(55.0);
let surface_color = neutral_swatch.get(98.0);
let surface_variant_color = neutral_swatch.get(90.0);
let neutral_set = ColorSet::new(
&neutral_color,
&neutral_swatch,
reference_lightness,
settings,
);
let neutral_variant_set = ColorSet::new(
&neutral_variant_color,
&neutral_swatch,
reference_lightness,
settings,
);
let surface_set = ColorSet::new(
&surface_color,
&neutral_swatch,
reference_lightness,
settings,
);
let surface_variant_set = ColorSet::new(
&surface_variant_color,
&neutral_swatch,
reference_lightness,
settings,
);
let primary_unit = ColorUnit::new(primary, &neutral_swatch, reference_lightness, settings);
let secondary_unit = final_secondary
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let tertiary_unit = final_tertiary
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let accent_unit = final_accent
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let danger_unit = ColorUnit::new(danger, &neutral_swatch, reference_lightness, settings);
let success_unit = ColorUnit::new(success, &neutral_swatch, reference_lightness, settings);
let warn_unit = ColorUnit::new(warn, &neutral_swatch, reference_lightness, settings);
let info_unit = ColorUnit::new(info, &neutral_swatch, reference_lightness, settings);
Self {
primary: primary_unit,
secondary: secondary_unit,
tertiary: tertiary_unit,
accent: accent_unit,
neutral: neutral_set,
neutral_variant: neutral_variant_set,
surface: surface_set,
surface_variant: surface_variant_set,
neutral_swatch,
danger: danger_unit,
success: success_unit,
warn: warn_unit,
info: info_unit,
custom_colors: HashMap::new(),
shadow: shadow_color,
overlay: overlay_color,
outline: outline_color,
outline_variant: outline_variant_color,
neutral_lightness: reference_lightness,
scheme_settings: settings.clone(),
is_dark,
}
}
pub fn add_custom_color(
&mut self,
name: &str,
color: &Oklch,
) -> Result<(), errors::ColorError> {
let custom_color = ColorUnit::new(
color,
&self.neutral_swatch,
self.neutral_lightness,
&self.scheme_settings,
);
self.custom_colors.insert(name.to_string(), custom_color);
Ok(())
}
pub fn to_css_variables(&self) -> Vec<String> {
let mut css_variables = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
css_variables.extend(self.primary.to_css_variables(scheme_mode, "primary"));
if let Some(secondary) = &self.secondary {
css_variables.extend(secondary.to_css_variables(scheme_mode, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
css_variables.extend(tertiary.to_css_variables(scheme_mode, "tertiary"));
}
if let Some(accent) = &self.accent {
css_variables.extend(accent.to_css_variables(scheme_mode, "accent"));
}
css_variables.extend(self.neutral.to_css_variables(scheme_mode, "neutral"));
css_variables.extend(
self.neutral_variant
.to_css_variables(scheme_mode, "neutral-variant"),
);
css_variables.extend(self.surface.to_css_variables(scheme_mode, "surface"));
css_variables.extend(
self.surface_variant
.to_css_variables(scheme_mode, "surface-variant"),
);
css_variables.extend(self.danger.to_css_variables(scheme_mode, "danger"));
css_variables.extend(self.success.to_css_variables(scheme_mode, "success"));
css_variables.extend(self.warn.to_css_variables(scheme_mode, "warn"));
css_variables.extend(self.info.to_css_variables(scheme_mode, "info"));
css_variables.push(format!(
"--color-{scheme_mode}-shadow: #{};",
map_oklch_to_srgb_hex(&self.shadow)
));
css_variables.push(format!(
"--color-{scheme_mode}-overlay: #{};",
map_oklch_to_srgb_hex(&self.overlay)
));
css_variables.push(format!(
"--color-{scheme_mode}-outlint: #{};",
map_oklch_to_srgb_hex(&self.outline)
));
css_variables.push(format!(
"--color-{scheme_mode}-outline-variant: #{};",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color_unit) in &self.custom_colors {
css_variables.extend(color_unit.to_css_variables(scheme_mode, name));
}
css_variables
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut css_variables = LinkedHashMap::new();
css_variables.extend(self.primary.to_css_auto_scheme_collection("primary"));
if let Some(secondary) = &self.secondary {
css_variables.extend(secondary.to_css_auto_scheme_collection("secondary"));
}
if let Some(tertiary) = &self.tertiary {
css_variables.extend(tertiary.to_css_auto_scheme_collection("tertiary"));
}
if let Some(accent) = &self.accent {
css_variables.extend(accent.to_css_auto_scheme_collection("accent"));
}
css_variables.extend(self.neutral.to_css_auto_scheme_collection("neutral"));
css_variables.extend(
self.neutral_variant
.to_css_auto_scheme_collection("neutral-variant"),
);
css_variables.extend(self.surface.to_css_auto_scheme_collection("surface"));
css_variables.extend(
self.surface_variant
.to_css_auto_scheme_collection("surface-variant"),
);
css_variables.insert("shadow".to_string(), map_oklch_to_srgb_hex(&self.shadow));
css_variables.insert("overlay".to_string(), map_oklch_to_srgb_hex(&self.overlay));
css_variables.insert("outline".to_string(), map_oklch_to_srgb_hex(&self.outline));
css_variables.insert(
"outline-variant".to_string(),
map_oklch_to_srgb_hex(&self.outline_variant),
);
for (name, color) in &self.custom_colors {
css_variables.extend(color.to_css_auto_scheme_collection(name));
}
css_variables
}
pub fn to_scss_variables(&self) -> Vec<String> {
let mut scss_variables = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
scss_variables.extend(self.primary.to_scss_variables(scheme_mode, "primary"));
if let Some(secondary) = &self.secondary {
scss_variables.extend(secondary.to_scss_variables(scheme_mode, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
scss_variables.extend(tertiary.to_scss_variables(scheme_mode, "tertiary"));
}
if let Some(accent) = &self.accent {
scss_variables.extend(accent.to_scss_variables(scheme_mode, "accent"));
}
scss_variables.extend(self.neutral.to_scss_variables(scheme_mode, "neutral"));
scss_variables.extend(
self.neutral_variant
.to_scss_variables(scheme_mode, "neutral-variant"),
);
scss_variables.extend(self.surface.to_scss_variables(scheme_mode, "surface"));
scss_variables.extend(
self.surface_variant
.to_scss_variables(scheme_mode, "surface-variant"),
);
scss_variables.push(format!(
"--color-{scheme_mode}-shadow: #{};",
map_oklch_to_srgb_hex(&self.shadow)
));
scss_variables.push(format!(
"--color-{scheme_mode}-overlay: #{};",
map_oklch_to_srgb_hex(&self.overlay)
));
scss_variables.push(format!(
"--color-{scheme_mode}-outlint: #{};",
map_oklch_to_srgb_hex(&self.outline)
));
scss_variables.push(format!(
"--color-{scheme_mode}-outline-variant: #{};",
map_oklch_to_srgb_hex(&self.outline_variant)
));
scss_variables
}
pub fn to_javascript_fields(&self) -> Vec<String> {
let mut javascript_fields = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
javascript_fields.push(format!("{scheme_mode}: {{"));
let indent = " ".repeat(4);
for line in self.primary.to_javascript_fields("primary").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.secondary
.as_ref()
.map(|s| s.to_javascript_fields("secondary"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.tertiary
.as_ref()
.map(|s| s.to_javascript_fields("tertiary"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.accent
.as_ref()
.map(|s| s.to_javascript_fields("accent"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.neutral.to_javascript_fields("neutral").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.neutral_variant
.to_javascript_fields("neutral_variant")
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.surface.to_javascript_fields("surface").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.surface_variant
.to_javascript_fields("surface_variant")
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
javascript_fields.push(format!(
"{indent}shadow: '#{}',",
map_oklch_to_srgb_hex(&self.shadow)
));
javascript_fields.push(format!(
"{indent}overlay: '#{}',",
map_oklch_to_srgb_hex(&self.overlay)
));
javascript_fields.push(format!(
"{indent}outline: '#{}',",
map_oklch_to_srgb_hex(&self.outline)
));
javascript_fields.push(format!(
"{indent}outlineVariant: '#{}',",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color) in &self.custom_colors {
let color_lines = color.to_javascript_fields(name);
javascript_fields.extend(color_lines.iter().map(|s| format!("{indent}{s}")));
}
javascript_fields.push("}".to_string());
javascript_fields
}
}
impl Serialize for Baseline {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("Baseline", 17)?;
state.serialize_field("primary", &self.primary)?;
state.serialize_field("secondary", &self.secondary)?;
state.serialize_field("tertiary", &self.tertiary)?;
state.serialize_field("accent", &self.accent)?;
state.serialize_field("neutral", &self.neutral)?;
state.serialize_field("neutralVariant", &self.neutral_variant)?;
state.serialize_field("surface", &self.surface)?;
state.serialize_field("surfaceVariant", &self.surface_variant)?;
state.serialize_field("danger", &self.danger)?;
state.serialize_field("success", &self.success)?;
state.serialize_field("warn", &self.warn)?;
state.serialize_field("info", &self.info)?;
state.serialize_field("shadow", &map_oklch_to_srgb_hex(&self.shadow))?;
state.serialize_field("overlay", &map_oklch_to_srgb_hex(&self.overlay))?;
state.serialize_field("outline", &map_oklch_to_srgb_hex(&self.outline))?;
state.serialize_field(
"outlineVariant",
&map_oklch_to_srgb_hex(&self.outline_variant),
)?;
state.serialize_field("custom", &self.custom_colors)?;
state.end()
}
}

View File

@ -0,0 +1,291 @@
use core::f32;
use std::sync::Arc;
use linked_hash_map::LinkedHashMap;
use palette::{color_difference::Wcag21RelativeContrast, luma::Luma, Oklch};
use serde::{ser::SerializeStruct, Serialize};
use crate::{
convert::{map_oklch_to_luma, map_oklch_to_srgb_hex},
schemes::{
q_style::{NeutralSwatch, SchemeSetting, WACGSetting},
q_style_2::swatch::Swatch,
},
};
#[derive(Debug, Clone)]
pub struct ColorSet {
pub root: Oklch,
pub active: Oklch,
pub focus: Oklch,
pub hover: Oklch,
pub disabled: Oklch,
pub on_root: Oklch,
pub on_disabled: Oklch,
pub neutral_swatch: Arc<NeutralSwatch>,
pub neutral_lightness: f32,
pub scheme_settings: Arc<SchemeSetting>,
}
#[inline]
fn match_wacg(original: &Oklch<f32>, reference: &Luma) -> f32 {
let luma_original = map_oklch_to_luma(original);
luma_original.relative_contrast(*reference)
}
fn search_for_common_wacg_color(
reference_colors: &[&Oklch],
neutral_swatch: &NeutralSwatch,
minium_ratio: f32,
) -> Oklch {
// store in: (lightness, min_wacg_abs)
let mut minium_match: (f32, f32) = (0.0, f32::INFINITY);
let mut closest_match: (f32, f32) = (f32::INFINITY, f32::INFINITY);
for scan_lightness in (0..=100).map(|x| x as f32 / 100.0) {
let new_target = neutral_swatch.get(scan_lightness);
let new_target_luma = map_oklch_to_luma(&new_target);
let reference_wacgs = reference_colors
.iter()
.map(|ref_color| match_wacg(&ref_color, &new_target_luma) - minium_ratio)
.min_by(|a, b| a.abs().total_cmp(&b.abs()))
.unwrap_or(f32::NEG_INFINITY);
if reference_wacgs.abs() < closest_match.1.abs() {
closest_match = (scan_lightness, reference_wacgs);
}
if reference_wacgs >= 0.0 && reference_wacgs.abs() < minium_match.1.abs() {
minium_match = (scan_lightness, reference_wacgs);
}
}
if minium_match.1 != f32::INFINITY {
neutral_swatch.get(minium_match.0)
} else {
neutral_swatch.get(closest_match.0)
}
}
impl ColorSet {
pub fn new(
color: &Oklch,
neutral_swatch: &Arc<NeutralSwatch>,
neutral_lightness: f32,
settings: &Arc<SchemeSetting>,
) -> Self {
let neutral_swatch = Arc::clone(neutral_swatch);
let settings = Arc::clone(settings);
let root = color.clone();
let hover = color * settings.hover;
let active = color * settings.active;
let focus = color * settings.focus;
let disabled = color * settings.disabled;
let color_list = &[&root, &hover, &active, &focus];
let (on_root, on_disabled) = match settings.wacg_follows {
WACGSetting::Fixed => (
neutral_swatch.get(neutral_lightness),
neutral_swatch.get(neutral_lightness),
),
WACGSetting::AutomaticAA => (
search_for_common_wacg_color(color_list, &neutral_swatch, 4.5),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 4.5),
),
WACGSetting::AutomaticAAA => (
search_for_common_wacg_color(color_list, &neutral_swatch, 7.0),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 7.0),
),
WACGSetting::HighContrast => (
search_for_common_wacg_color(color_list, &neutral_swatch, 21.0),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 21.0),
),
};
Self {
root,
active,
focus,
hover,
disabled,
on_root,
on_disabled,
neutral_swatch,
neutral_lightness,
scheme_settings: settings,
}
}
pub fn generate_surface_set(&self) -> Self {
let root_swatch = Swatch::new(&self.root);
let root_lightness = self.root.l;
let surface_lightness = if root_lightness + 40.0 > 90.0 {
root_lightness - 40.0
} else {
root_lightness + 40.0
};
let surface_color = root_swatch.get(surface_lightness);
Self::new(
&surface_color,
&self.neutral_swatch,
self.neutral_lightness,
&self.scheme_settings,
)
}
pub fn generate_swatch(&self) -> Swatch {
Swatch::new(&self.root)
}
fn root_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.root)
}
fn hover_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.hover)
}
fn active_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.active)
}
fn focus_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.focus)
}
fn disabled_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.disabled)
}
fn on_root_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.on_root)
}
fn on_disabled_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.on_disabled)
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!("--color-{prefix}-{name}: #{};", self.root_hex()));
variables.push(format!(
"--color-{prefix}-{name}-hover: #{};",
self.hover_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-active: #{};",
self.active_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-focus: #{};",
self.focus_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-disabled: ${};",
self.disabled_hex()
));
variables.push(format!(
"--color-{prefix}-on-{name}: #{};",
self.on_root_hex()
));
variables.push(format!(
"--color-{prefix}-on-{name}-disabled: #{};",
self.on_disabled_hex()
));
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
collection.insert(format!("{name}"), self.root_hex());
collection.insert(format!("{name}-hover"), self.hover_hex());
collection.insert(format!("{name}-active"), self.active_hex());
collection.insert(format!("{name}-focus"), self.focus_hex());
collection.insert(format!("{name}-disabled"), self.disabled_hex());
collection.insert(format!("on-{name}"), self.on_root_hex());
collection.insert(format!("on-{name}-disabled"), self.on_disabled_hex());
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!("&color-{prefix}-{name}: #{};", self.root_hex()));
variables.push(format!(
"$color-{prefix}-{name}-hover: #{};",
self.hover_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-active: #{};",
self.active_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-focus: ${};",
self.focus_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-disabled: #{};",
self.disabled_hex()
));
variables.push(format!(
"$color-{prefix}-on-{name}: #{};",
self.on_root_hex()
));
variables.push(format!(
"$color-{prefix}-on-{name}-disabled: #{};",
self.on_disabled_hex()
));
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
let capitalized_name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variables.push(format!("{name}: '#{}',", self.root_hex()));
variables.push(format!("{name}Hover: '#{}',", self.hover_hex()));
variables.push(format!("{name}Active: '#{}',", self.active_hex()));
variables.push(format!("{name}Focus: '#{}',", self.focus_hex()));
variables.push(format!("{name}Disabled: '#{}',", self.disabled_hex()));
variables.push(format!("on{capitalized_name}: '#{}',", self.on_root_hex()));
variables.push(format!(
"on{capitalized_name}Disabled: '#{}',",
self.on_disabled_hex()
));
variables
}
}
impl Serialize for ColorSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let root = map_oklch_to_srgb_hex(&self.root);
let active = map_oklch_to_srgb_hex(&self.active);
let focus = map_oklch_to_srgb_hex(&self.focus);
let disabled = map_oklch_to_srgb_hex(&self.disabled);
let on_root = map_oklch_to_srgb_hex(&self.on_root);
let on_disabled = map_oklch_to_srgb_hex(&self.on_disabled);
let mut state = serializer.serialize_struct("ColorSet", 6)?;
state.serialize_field("root", &root)?;
state.serialize_field("active", &active)?;
state.serialize_field("focus", &focus)?;
state.serialize_field("disabled", &disabled)?;
state.serialize_field("onRoot", &on_root)?;
state.serialize_field("onDisabled", &on_disabled)?;
state.end()
}
}

View File

@ -0,0 +1,157 @@
use std::{str::FromStr, sync::Arc};
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use serde::Serialize;
use crate::{
errors, parse_option_to_oklch, parse_to_oklch,
schemes::{q_style::SchemeSetting, q_style_2::baseline::Baseline, SchemeExport},
};
mod baseline;
mod color_set;
mod swatch;
#[derive(Debug, Clone, Serialize)]
pub struct QScheme2 {
pub light: Baseline,
pub dark: Baseline,
#[serde(skip)]
_settings: Arc<SchemeSetting>,
}
impl QScheme2 {
pub fn new(
primary: &str,
secondary: Option<&str>,
tertiary: Option<&str>,
accent: Option<&str>,
danger: &str,
success: &str,
warn: &str,
info: &str,
foreground: &str,
background: &str,
setting: SchemeSetting,
) -> Result<Self, errors::ColorError> {
let primary = parse_to_oklch!(primary);
let secondary = parse_option_to_oklch!(secondary);
let tertiary = parse_option_to_oklch!(tertiary);
let accent = parse_option_to_oklch!(accent);
let danger = parse_to_oklch!(danger);
let success = parse_to_oklch!(success);
let warn = parse_to_oklch!(warn);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
let settings = Arc::new(setting);
let light_scheme = Baseline::new(
&primary,
secondary.as_ref(),
tertiary.as_ref(),
accent.as_ref(),
&danger,
&success,
&warn,
&info,
&foreground,
&background,
&settings,
false,
);
let dark_scheme = Baseline::new(
&(&primary * settings.dark_convert),
secondary.map(|c| c * settings.dark_convert).as_ref(),
tertiary.map(|c| c * settings.dark_convert).as_ref(),
accent.map(|c| c * settings.dark_convert).as_ref(),
&(danger * settings.dark_convert),
&(success * settings.dark_convert),
&(warn * settings.dark_convert),
&(info * settings.dark_convert),
&foreground,
&background,
&settings,
true,
);
Ok(Self {
light: light_scheme,
dark: dark_scheme,
_settings: settings,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
let custom_color = parse_to_oklch!(color);
self.light.add_custom_color(name, &custom_color)?;
self.dark
.add_custom_color(name, &(custom_color * self._settings.dark_convert))?;
Ok(())
}
}
impl SchemeExport for QScheme2 {
fn output_css_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_css_variables());
variables.extend(self.dark.to_css_variables());
variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut collection = Vec::new();
let mut keys = LinkedHashSet::new();
let light_collection = self.light.to_css_auto_scheme_collection();
let dark_collection = self.dark.to_css_auto_scheme_collection();
keys.extend(light_collection.keys().cloned());
keys.extend(dark_collection.keys().cloned());
for key in keys {
match (light_collection.get(&key), dark_collection.get(&key)) {
(Some(light), Some(dark)) => {
collection.push(format!("--color-{key}: light-dark(#{light}, #{dark});"));
}
(Some(color), None) | (None, Some(color)) => {
collection.push(format!("--color-{key}: #{color}"));
}
(None, None) => {}
}
}
collection.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_scss_variables());
variables.extend(self.dark.to_scss_variables());
variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut javascript_object = Vec::new();
let indent = " ".repeat(4);
javascript_object.push("{".to_string());
for line in self.light.to_javascript_fields() {
javascript_object.push(format!("{indent}{line}"));
}
for line in self.dark.to_javascript_fields() {
javascript_object.push(format!("{indent}{line}"));
}
javascript_object.push("}".to_string());
javascript_object.join("\n")
}
}

View File

@ -0,0 +1,93 @@
use linked_hash_map::LinkedHashMap;
use palette::Oklch;
use serde::{ser::SerializeMap, Serialize};
use crate::convert::map_oklch_to_srgb_hex;
static SWATCH_LIGHTINGS: [u8; 16] = [
10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 75, 80, 85, 90, 95, 98,
];
#[derive(Debug, Clone)]
pub struct Swatch(Oklch);
impl Swatch {
pub fn new(color: &Oklch) -> Self {
Self(color.clone())
}
pub fn get<L: Into<f32>>(&self, lightness: L) -> Oklch {
let request_lightness: f32 = lightness.into();
Oklch {
l: request_lightness.clamp(10.0, 98.0),
..self.0.clone()
}
}
pub fn get_hex<L: Into<f32>>(&self, lightness: L) -> String {
let c = self.get(lightness.into());
map_oklch_to_srgb_hex(&c)
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!(
"--color-{prefix}-swatch-{name}-{l:02}: #{};",
self.get_hex(l)
));
}
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
for l in SWATCH_LIGHTINGS {
collection.insert(format!("{name}-{l:02}"), self.get_hex(l));
}
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!(
"$color-{prefix}-swatch-{name}-{l:02}: #{};",
self.get_hex(l)
));
}
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!("{name}{l:02}: '#{}',", self.get_hex(l)));
}
variables
}
}
impl Serialize for Swatch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_map(Some(SWATCH_LIGHTINGS.len()))?;
for l in SWATCH_LIGHTINGS {
let color = self.get_hex(l);
state.serialize_entry(&format!("{l:02}"), &color)?;
}
state.end()
}
}