From bd4a2c9b4934538f5cd5c1ae6756ece1c0812a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Mon, 14 Jul 2025 09:03:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(q=5Fstyle=5F2):=20=E6=96=B0=E5=A2=9EQStyle?= =?UTF-8?q?2=E9=A2=9C=E8=89=B2=E6=96=B9=E6=A1=88=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加QStyle2颜色方案模块,包含基础颜色集、色板生成和自动配色功能 实现颜色方案的CSS、SCSS和JavaScript输出支持 新增generate_q_scheme_2_manually函数用于手动生成QStyle2方案 --- color-module/src/schemes/mod.rs | 47 +- color-module/src/schemes/q_style/mod.rs | 4 +- .../src/schemes/q_style_2/baseline.rs | 545 ++++++++++++++++++ .../src/schemes/q_style_2/color_set.rs | 291 ++++++++++ color-module/src/schemes/q_style_2/mod.rs | 157 +++++ color-module/src/schemes/q_style_2/swatch.rs | 93 +++ 6 files changed, 1134 insertions(+), 3 deletions(-) create mode 100644 color-module/src/schemes/q_style_2/baseline.rs create mode 100644 color-module/src/schemes/q_style_2/color_set.rs create mode 100644 color-module/src/schemes/q_style_2/mod.rs create mode 100644 color-module/src/schemes/q_style_2/swatch.rs diff --git a/color-module/src/schemes/mod.rs b/color-module/src/schemes/mod.rs index 873f319..f710f68 100644 --- a/color-module/src/schemes/mod.rs +++ b/color-module/src/schemes/mod.rs @@ -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, + tertiary_color: Option, + accent_color: Option, + danger_color: &str, + success_color: &str, + warn_color: &str, + info_color: &str, + fg_color: &str, + bg_color: &str, + custom_colors: JsValue, + setting: SchemeSetting, +) -> Result { + 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 = 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, diff --git a/color-module/src/schemes/q_style/mod.rs b/color-module/src/schemes/q_style/mod.rs index c44e0a7..df027ca 100644 --- a/color-module/src/schemes/q_style/mod.rs +++ b/color-module/src/schemes/q_style/mod.rs @@ -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 { diff --git a/color-module/src/schemes/q_style_2/baseline.rs b/color-module/src/schemes/q_style_2/baseline.rs new file mode 100644 index 0000000..93bbdee --- /dev/null +++ b/color-module/src/schemes/q_style_2/baseline.rs @@ -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, + foreground_lightness: f32, + settings: &Arc, + ) -> 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 { + 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 { + 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 { + 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 { + 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(&self, serializer: S) -> Result + 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, + pub tertiary: Option, + pub accent: Option, + pub neutral: ColorSet, + pub neutral_variant: ColorSet, + pub surface: ColorSet, + pub surface_variant: ColorSet, + pub neutral_swatch: Arc, + pub danger: ColorUnit, + pub success: ColorUnit, + pub warn: ColorUnit, + pub info: ColorUnit, + pub custom_colors: HashMap, + pub shadow: Oklch, + pub overlay: Oklch, + pub outline: Oklch, + pub outline_variant: Oklch, + pub neutral_lightness: f32, + pub scheme_settings: Arc, + 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, + 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 { + 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 { + 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 { + 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 { + 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(&self, serializer: S) -> Result + 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() + } +} diff --git a/color-module/src/schemes/q_style_2/color_set.rs b/color-module/src/schemes/q_style_2/color_set.rs new file mode 100644 index 0000000..a3dcc66 --- /dev/null +++ b/color-module/src/schemes/q_style_2/color_set.rs @@ -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, + pub neutral_lightness: f32, + pub scheme_settings: Arc, +} + +#[inline] +fn match_wacg(original: &Oklch, 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, + neutral_lightness: f32, + settings: &Arc, + ) -> 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 { + 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 { + 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 { + 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 { + 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(&self, serializer: S) -> Result + 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() + } +} diff --git a/color-module/src/schemes/q_style_2/mod.rs b/color-module/src/schemes/q_style_2/mod.rs new file mode 100644 index 0000000..feacded --- /dev/null +++ b/color-module/src/schemes/q_style_2/mod.rs @@ -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, +} + +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 { + 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") + } +} diff --git a/color-module/src/schemes/q_style_2/swatch.rs b/color-module/src/schemes/q_style_2/swatch.rs new file mode 100644 index 0000000..6b20c9f --- /dev/null +++ b/color-module/src/schemes/q_style_2/swatch.rs @@ -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>(&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>(&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 { + 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 { + 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 { + 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 { + 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(&self, serializer: S) -> Result + 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() + } +}