增加QScheme主题样式的生成与导出。

This commit is contained in:
徐涛 2025-01-22 14:59:40 +08:00
parent 826e526ba2
commit 86ecb5a258
7 changed files with 1075 additions and 1 deletions

View File

@ -1,7 +1,8 @@
use palette::{
cam16::{Cam16Jch, Parameters},
convert::FromColorUnclamped,
Hsl, IsWithinBounds, Srgb,
luma::Luma,
Hsl, IntoColor, IsWithinBounds, Oklab, Oklch, Srgb,
};
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
@ -44,3 +45,43 @@ pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb {
pub fn map_hsl_to_srgb_hex(origin: &Hsl) -> String {
format!("{:x}", map_hsl_to_srgb(origin).into_format::<u8>())
}
pub fn map_oklch_to_luma(origin: &Oklch) -> Luma {
let lab_color: Oklab = (*origin).into_color();
let linear_rgb = Srgb::from_linear(lab_color.into_color());
let luma_color: Luma = linear_rgb.into_color();
luma_color
}
pub fn map_oklch_to_srgb(origin: &Oklch) -> Srgb {
Srgb::from_linear::<f32>((*origin).into_color())
}
pub fn map_oklch_to_srgb_hex(origin: &Oklch) -> String {
format!("{:x}", map_oklch_to_srgb(origin).into_format::<u8>())
}
#[macro_export]
macro_rules! parse_to_oklch {
($origin: ident) => {
palette::Oklch::from_color(
palette::Srgb::from_str($origin)
.map_err(|_| crate::errors::ColorError::UnrecogniazedRGB($origin.to_string()))?
.into_format::<f32>(),
)
};
}
#[macro_export]
macro_rules! parse_option_to_oklch {
($origin: ident) => {
$origin
.map(|color| {
let rgb = palette::Srgb::from_str(color)
.map_err(|_| crate::errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
Ok(palette::Oklch::from_color(rgb))
})
.transpose()?
};
}

View File

@ -2,12 +2,14 @@ use std::collections::HashMap;
use material_design_2::MaterialDesign2Scheme;
use material_design_3::MaterialDesign3Scheme;
use q_style::{QScheme, SchemeSetting};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::errors;
pub mod material_design_2;
pub mod material_design_3;
pub mod q_style;
pub trait SchemeExport {
fn output_css_variables(&self) -> String;
@ -57,3 +59,73 @@ pub fn generate_material_design_2_scheme(
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_automatically(
primary_color: &str,
danger_color: &str,
success_color: &str,
warning_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
setting: JsValue,
) -> Result<JsValue, errors::ColorError> {
let settings: SchemeSetting = serde_wasm_bindgen::from_value(setting)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let scheme = QScheme::new(
primary_color,
danger_color,
success_color,
warning_color,
info_color,
fg_color,
bg_color,
settings,
)?;
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)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_manually(
primary_color: &str,
secondary_color: Option<String>,
tertiary_color: Option<String>,
accent_color: Option<String>,
danger_color: &str,
success_color: &str,
warning_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
setting: JsValue,
) -> Result<JsValue, errors::ColorError> {
let settings: SchemeSetting = serde_wasm_bindgen::from_value(setting)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let scheme = QScheme::custom(
primary_color,
secondary_color.as_deref(),
tertiary_color.as_deref(),
accent_color.as_deref(),
danger_color,
success_color,
warning_color,
info_color,
fg_color,
bg_color,
settings,
)?;
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}

View File

@ -0,0 +1,241 @@
use palette::{
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
Oklch, ShiftHue,
};
use serde::{ser::SerializeStruct, Serialize};
use crate::convert::map_oklch_to_srgb_hex;
use super::{
color_set::ColorSet,
neutral_swatch::NeutralSwatch,
scheme_setting::{ColorExpand, SchemeSetting},
};
#[derive(Debug, Clone)]
pub struct Baseline {
pub primary: ColorSet,
pub secondary: Option<ColorSet>,
pub tertiary: Option<ColorSet>,
pub accent: Option<ColorSet>,
pub neutral: ColorSet,
pub danger: ColorSet,
pub success: ColorSet,
pub warning: ColorSet,
pub info: ColorSet,
pub outline: ColorSet,
pub foreground: Oklch,
pub background: Oklch,
_neutral_swatch: NeutralSwatch,
}
impl Baseline {
pub fn custom(
primary: &Oklch,
secondary: &Option<Oklch>,
tertiary: &Option<Oklch>,
accent: &Option<Oklch>,
danger: &Oklch,
success: &Oklch,
warning: &Oklch,
info: &Oklch,
foreground: &Oklch,
background: &Oklch,
setting: SchemeSetting,
) -> Self {
let neutral_swatch = NeutralSwatch::new(*foreground, *background);
let neutral_color = neutral_swatch.get(primary.l);
let outline_color = neutral_swatch.get(background.l * 0.7);
Self {
primary: ColorSet::new(primary, &neutral_swatch, &setting),
secondary: secondary.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)),
tertiary: tertiary.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)),
accent: accent.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)),
neutral: ColorSet::new(&neutral_color, &neutral_swatch, &setting),
danger: ColorSet::new(danger, &neutral_swatch, &setting),
success: ColorSet::new(success, &neutral_swatch, &setting),
warning: ColorSet::new(warning, &neutral_swatch, &setting),
info: ColorSet::new(info, &neutral_swatch, &setting),
outline: ColorSet::new(&outline_color, &neutral_swatch, &setting),
foreground: *foreground,
background: *background,
_neutral_swatch: neutral_swatch,
}
}
pub fn new(
primary: &Oklch,
danger: &Oklch,
success: &Oklch,
warning: &Oklch,
info: &Oklch,
foreground: &Oklch,
background: &Oklch,
setting: SchemeSetting,
) -> Self {
let (secondary, tertiary, accent) = match setting.expand_method {
ColorExpand::Complementary => (Some(primary.complementary()), None, None),
ColorExpand::Analogous => {
let analogous_color = primary.analogous();
(Some(analogous_color.0), Some(analogous_color.1), None)
}
ColorExpand::AnalogousAndComplementary => {
let analogous_color = primary.analogous();
(
Some(analogous_color.0),
Some(analogous_color.1),
Some(primary.complementary()),
)
}
ColorExpand::Triadic => {
let triadic_color = primary.triadic();
(Some(triadic_color.0), Some(triadic_color.1), None)
}
ColorExpand::SplitComplementary => {
let split_complementary_color = primary.split_complementary();
(
Some(split_complementary_color.0),
None,
Some(split_complementary_color.1),
)
}
ColorExpand::Tetradic => {
let tetradic_color = primary.tetradic();
(
Some(tetradic_color.0),
Some(tetradic_color.2),
Some(tetradic_color.1),
)
}
ColorExpand::Square => {
let c_90 = primary.shift_hue(90.0);
let complementary = primary.complementary();
let c_270 = primary.shift_hue(270.0);
(Some(c_90), Some(c_270), Some(complementary))
}
};
Self::custom(
primary, &secondary, &tertiary, &accent, danger, success, warning, info, foreground,
background, setting,
)
}
pub fn to_css_variables(&self, prefix: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.extend(self.primary.to_css_variables(prefix, "primary"));
if let Some(secondary) = &self.secondary {
variables.extend(secondary.to_css_variables(prefix, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
variables.extend(tertiary.to_css_variables(prefix, "tertiary"));
}
if let Some(accent) = &self.accent {
variables.extend(accent.to_css_variables(prefix, "accent"));
}
variables.extend(self.neutral.to_css_variables(prefix, "neutral"));
variables.extend(self.danger.to_css_variables(prefix, "danger"));
variables.extend(self.success.to_css_variables(prefix, "success"));
variables.extend(self.warning.to_css_variables(prefix, "warning"));
variables.extend(self.info.to_css_variables(prefix, "info"));
variables.extend(self.outline.to_css_variables(prefix, "outline"));
variables.push(format!(
"--color-{}-foreground: #{};",
prefix,
map_oklch_to_srgb_hex(&self.foreground)
));
variables.push(format!(
"--color-{}-background: #{};",
prefix,
map_oklch_to_srgb_hex(&self.background)
));
variables
}
pub fn to_scss_variables(&self, prefix: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.extend(self.primary.to_scss_variables(prefix, "primary"));
if let Some(secondary) = &self.secondary {
variables.extend(secondary.to_scss_variables(prefix, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
variables.extend(tertiary.to_scss_variables(prefix, "tertiary"));
}
if let Some(accent) = &self.accent {
variables.extend(accent.to_scss_variables(prefix, "accent"));
}
variables.extend(self.neutral.to_scss_variables(prefix, "neutral"));
variables.extend(self.danger.to_scss_variables(prefix, "danger"));
variables.extend(self.success.to_scss_variables(prefix, "success"));
variables.extend(self.warning.to_scss_variables(prefix, "warning"));
variables.extend(self.info.to_scss_variables(prefix, "info"));
variables.extend(self.outline.to_scss_variables(prefix, "outline"));
variables.push(format!(
"$color-{}-foreground: #{};",
prefix,
map_oklch_to_srgb_hex(&self.foreground)
));
variables.push(format!(
"$color-{}-background: #{};",
prefix,
map_oklch_to_srgb_hex(&self.background)
));
variables
}
pub fn to_javascript_fields(&self) -> Vec<String> {
let mut variables = Vec::new();
variables.extend(self.primary.to_javascript_fields("primary"));
if let Some(secondary) = &self.secondary {
variables.extend(secondary.to_javascript_fields("secondary"));
}
if let Some(tertiary) = &self.tertiary {
variables.extend(tertiary.to_javascript_fields("tertiary"));
}
if let Some(accent) = &self.accent {
variables.extend(accent.to_javascript_fields("accent"));
}
variables.extend(self.neutral.to_javascript_fields("neutral"));
variables.extend(self.danger.to_javascript_fields("danger"));
variables.extend(self.success.to_javascript_fields("success"));
variables.extend(self.warning.to_javascript_fields("warning"));
variables.extend(self.info.to_javascript_fields("info"));
variables.extend(self.outline.to_javascript_fields("outline"));
variables.push(format!(
"foreground: '#{}',",
map_oklch_to_srgb_hex(&self.foreground)
));
variables.push(format!(
"background: '#{}',",
map_oklch_to_srgb_hex(&self.background)
));
variables
}
}
impl Serialize for Baseline {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_struct("Baseline", 13)?;
map.serialize_field("primary", &self.primary)?;
map.serialize_field("secondary", &self.secondary)?;
map.serialize_field("tertiary", &self.tertiary)?;
map.serialize_field("accent", &self.accent)?;
map.serialize_field("neutral", &self.neutral)?;
map.serialize_field("danger", &self.danger)?;
map.serialize_field("success", &self.success)?;
map.serialize_field("warning", &self.warning)?;
map.serialize_field("info", &self.info)?;
map.serialize_field("outline", &self.outline)?;
map.serialize_field("foreground", &map_oklch_to_srgb_hex(&self.foreground))?;
map.serialize_field("background", &map_oklch_to_srgb_hex(&self.background))?;
map.end()
}
}

View File

@ -0,0 +1,327 @@
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};
use super::{
neutral_swatch::NeutralSwatch,
scheme_setting::{SchemeSetting, WACGSetting},
};
#[derive(Debug, Clone)]
pub struct ColorSet {
pub root: Oklch,
pub hover: Oklch,
pub active: Oklch,
pub focus: Oklch,
pub disabled: Oklch,
pub on_root: Oklch,
pub on_hover: Oklch,
pub on_active: Oklch,
pub on_focus: Oklch,
pub on_disabled: Oklch,
}
fn fit_to_wacg(reference: &Oklch, neutral_swatch: &NeutralSwatch, ratio: f32) -> Oklch {
let reference_luma = map_oklch_to_luma(reference);
let mut new_target = neutral_swatch.get(reference.l);
let quick_factor: f32 = if reference.l <= 0.5 { 0.05 } else { -0.05 };
let fine_factor: f32 = if reference.l <= 0.5 { 0.01 } else { -0.01 };
let match_wacg = |original: &Oklch<f32>, reference: &Luma| {
let luma = map_oklch_to_luma(original);
luma.relative_contrast(*reference)
};
while match_wacg(&new_target, &reference_luma) < ratio {
new_target.l = new_target.l * (1.0 + quick_factor);
if new_target.l > 1.0 {
new_target.l = 1.0;
break;
}
}
while match_wacg(&new_target, &reference_luma) < ratio {
new_target.l = new_target.l * (1.0 + fine_factor);
if new_target.l > 1.0 {
new_target.l = 1.0;
break;
}
}
new_target
}
impl ColorSet {
pub fn new(color: &Oklch, neutral_swatch: &NeutralSwatch, setting: &SchemeSetting) -> Self {
let root = color.clone();
let hover = color * setting.hover;
let active = color * setting.active;
let focus = color * setting.focus;
let disabled = color * setting.disabled;
let (on_root, on_hover, on_active, on_focus, on_disabled) = match setting.wacg_follows {
WACGSetting::Fixed => (
neutral_swatch.get(root.l),
neutral_swatch.get(hover.l),
neutral_swatch.get(active.l),
neutral_swatch.get(focus.l),
neutral_swatch.get(disabled.l),
),
WACGSetting::AutomaticAA => (
fit_to_wacg(&root, neutral_swatch, 4.5),
fit_to_wacg(&hover, neutral_swatch, 4.5),
fit_to_wacg(&active, neutral_swatch, 4.5),
fit_to_wacg(&focus, neutral_swatch, 4.5),
fit_to_wacg(&disabled, neutral_swatch, 4.5),
),
WACGSetting::AutomaticAAA => (
fit_to_wacg(&root, neutral_swatch, 7.0),
fit_to_wacg(&hover, neutral_swatch, 7.0),
fit_to_wacg(&active, neutral_swatch, 7.0),
fit_to_wacg(&focus, neutral_swatch, 7.0),
fit_to_wacg(&disabled, neutral_swatch, 7.0),
),
};
Self {
root,
hover,
active,
focus,
disabled,
on_root,
on_hover,
on_active,
on_focus,
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,
map_oklch_to_srgb_hex(&self.root)
));
variables.push(format!(
"--color-{}-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.hover)
));
variables.push(format!(
"--color-{}-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.active)
));
variables.push(format!(
"--color-{}-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.focus)
));
variables.push(format!(
"--color-{}-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.disabled)
));
variables.push(format!(
"--color-{}-on-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_root)
));
variables.push(format!(
"--color-{}-on-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_hover)
));
variables.push(format!(
"--color-{}-on-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_active)
));
variables.push(format!(
"--color-{}-on-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_focus)
));
variables.push(format!(
"--color-{}-on-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_disabled)
));
variables
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!(
"$color-{}-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.root)
));
variables.push(format!(
"$color-{}-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.hover)
));
variables.push(format!(
"$color-{}-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.active)
));
variables.push(format!(
"$color-{}-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.focus)
));
variables.push(format!(
"$color-{}-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.disabled)
));
variables.push(format!(
"$color-{}-on-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_root)
));
variables.push(format!(
"$color-{}-on-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_hover)
));
variables.push(format!(
"$color-{}-on-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_active)
));
variables.push(format!(
"$color-{}-on-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_focus)
));
variables.push(format!(
"$color-{}-on-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_disabled)
));
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,
map_oklch_to_srgb_hex(&self.root)
));
variables.push(format!(
"{}Hover: '#{}',",
name,
map_oklch_to_srgb_hex(&self.hover)
));
variables.push(format!(
"{}Active: '#{}',",
name,
map_oklch_to_srgb_hex(&self.active)
));
variables.push(format!(
"{}Focus: '#{}',",
name,
map_oklch_to_srgb_hex(&self.focus)
));
variables.push(format!(
"{}Disabled: '#{}',",
name,
map_oklch_to_srgb_hex(&self.disabled)
));
variables.push(format!(
"on{}: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_root)
));
variables.push(format!(
"on{}Hover: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_hover)
));
variables.push(format!(
"on{}Active: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_active)
));
variables.push(format!(
"on{}Focus: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_focus)
));
variables.push(format!(
"on{}Disabled: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_disabled)
));
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 hover = map_oklch_to_srgb_hex(&self.hover);
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_hover = map_oklch_to_srgb_hex(&self.on_hover);
let on_active = map_oklch_to_srgb_hex(&self.on_active);
let on_focus = map_oklch_to_srgb_hex(&self.on_focus);
let on_disabled = map_oklch_to_srgb_hex(&self.on_disabled);
let mut state = serializer.serialize_struct("ColorSet", 10)?;
state.serialize_field("root", &root)?;
state.serialize_field("hover", &hover)?;
state.serialize_field("active", &active)?;
state.serialize_field("focus", &focus)?;
state.serialize_field("disabled", &disabled)?;
state.serialize_field("onRoot", &on_root)?;
state.serialize_field("onHover", &on_hover)?;
state.serialize_field("onActive", &on_active)?;
state.serialize_field("onFocus", &on_focus)?;
state.serialize_field("onDisabled", &on_disabled)?;
state.end()
}
}

View File

@ -0,0 +1,196 @@
use std::str::FromStr;
use baseline::Baseline;
use palette::FromColor;
use scheme_setting::{ColorExpand, WACGSetting};
use serde::Serialize;
use strum::IntoEnumIterator;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::{errors, parse_option_to_oklch, parse_to_oklch};
use super::SchemeExport;
mod baseline;
mod color_set;
mod neutral_swatch;
mod scheme_setting;
pub use scheme_setting::SchemeSetting;
#[derive(Debug, Clone, Serialize)]
pub struct QScheme {
pub light: Baseline,
pub dark: Baseline,
}
impl QScheme {
pub fn new(
primary: &str,
danger: &str,
success: &str,
warning: &str,
info: &str,
foreground: &str,
background: &str,
setting: SchemeSetting,
) -> Result<Self, errors::ColorError> {
let primary = parse_to_oklch!(primary);
let danger = parse_to_oklch!(danger);
let success = parse_to_oklch!(success);
let warning = parse_to_oklch!(warning);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
Ok(Self {
light: Baseline::new(
&primary,
&danger,
&success,
&warning,
&info,
&foreground,
&background,
setting.clone(),
),
dark: Baseline::new(
&(primary * setting.dark_convert),
&(danger * setting.dark_convert),
&(success * setting.dark_convert),
&(warning * setting.dark_convert),
&(info * setting.dark_convert),
&(foreground * setting.dark_convert),
&(background * setting.dark_convert),
setting.clone(),
),
})
}
pub fn custom(
primary: &str,
secondary: Option<&str>,
tertiary: Option<&str>,
accent: Option<&str>,
danger: &str,
success: &str,
warning: &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 warning = parse_to_oklch!(warning);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
Ok(Self {
light: Baseline::custom(
&primary,
&secondary,
&tertiary,
&accent,
&danger,
&success,
&warning,
&info,
&foreground,
&background,
setting.clone(),
),
dark: Baseline::custom(
&(primary * setting.dark_convert),
&secondary.map(|color| color * setting.dark_convert),
&tertiary.map(|color| color * setting.dark_convert),
&accent.map(|color| color * setting.dark_convert),
&(danger * setting.dark_convert),
&(success * setting.dark_convert),
&(warning * setting.dark_convert),
&(info * setting.dark_convert),
&(foreground * setting.dark_convert),
&(background * setting.dark_convert),
setting.clone(),
),
})
}
}
impl SchemeExport for QScheme {
fn output_css_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_css_variables("light"));
variables.extend(self.dark.to_css_variables("dark"));
variables.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_scss_variables("light"));
variables.extend(self.dark.to_scss_variables("dark"));
variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut javascript_object = Vec::new();
javascript_object.push("{".to_string());
javascript_object.push(" light: {".to_string());
javascript_object.extend(
self.light
.to_javascript_fields()
.into_iter()
.map(|c| format!(" {}", c))
.collect::<Vec<_>>(),
);
javascript_object.push(" },".to_string());
javascript_object.push(" dark: {".to_string());
javascript_object.extend(
self.dark
.to_javascript_fields()
.into_iter()
.map(|c| format!(" {}", c))
.collect::<Vec<_>>(),
);
javascript_object.push("}".to_string());
javascript_object.join("\n")
}
}
#[wasm_bindgen]
pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> {
let methods = ColorExpand::iter()
.map(|variant| {
serde_json::json!({
"label": variant.label(),
"value": variant.to_string(),
})
})
.collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&methods).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn q_scheme_wacg_settings() -> Result<JsValue, String> {
let settings = WACGSetting::iter()
.map(|setting| {
serde_json::json!({
"label": setting.label(),
"value": setting.to_string(),
})
})
.collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string())
}

View File

@ -0,0 +1,58 @@
use palette::Oklch;
#[derive(Debug, Clone)]
pub struct NeutralSwatch(Oklch, Oklch);
impl NeutralSwatch {
pub fn new(color1: Oklch, color2: Oklch) -> Self {
if color1.l < color2.l {
NeutralSwatch(
Oklch {
l: color1.l * 0.4,
chroma: color1.chroma * 0.1,
..color1
},
Oklch {
l: color2.l * 1.6,
chroma: color2.chroma * 0.1,
..color2
},
)
} else {
NeutralSwatch(
Oklch {
l: color2.l * 0.4,
chroma: color2.chroma * 0.1,
..color2
},
Oklch {
l: color1.l * 1.6,
chroma: color1.chroma * 0.1,
..color1
},
)
}
}
pub fn get(&self, percent: f32) -> Oklch {
let start_hue = self.0.hue.into_positive_degrees();
let end_hue = self.1.hue.into_positive_degrees();
let hue = if (start_hue - end_hue).abs() > 180.0 {
if end_hue > start_hue {
start_hue + (end_hue + 360.0 - start_hue) * percent
} else {
start_hue + (end_hue - start_hue + 360.0) * percent
}
} else {
start_hue + (end_hue - start_hue) * percent
}
.rem_euclid(360.0);
Oklch {
l: percent,
chroma: self.0.chroma + (self.1.chroma - self.0.chroma) * percent,
hue: hue.into(),
}
}
}

View File

@ -0,0 +1,139 @@
use std::ops::Mul;
use palette::Oklch;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, EnumString};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ColorShifting {
pub chroma: f32,
pub lightness: f32,
}
impl Mul<ColorShifting> for Oklch<f32> {
type Output = Oklch<f32>;
fn mul(self, rhs: ColorShifting) -> Self::Output {
Oklch::new(
self.l
+ if rhs.lightness > 0.0 {
(1.0 - self.l) * rhs.lightness
} else {
self.l * rhs.lightness * -1.0
},
self.chroma
+ if rhs.chroma > 0.0 {
(100.0 - self.chroma) * rhs.chroma
} else {
-(self.chroma * rhs.chroma)
},
self.hue,
)
}
}
impl Mul<ColorShifting> for &Oklch<f32> {
type Output = Oklch<f32>;
fn mul(self, rhs: ColorShifting) -> Self::Output {
Oklch::new(
self.l
+ if rhs.lightness > 0.0 {
(1.0 - self.l) * rhs.lightness
} else {
self.l * rhs.lightness * -1.0
},
self.chroma
+ if rhs.chroma > 0.0 {
(100.0 - self.chroma) * rhs.chroma
} else {
-(self.chroma * rhs.chroma)
},
self.hue,
)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SchemeSetting {
pub hover: ColorShifting,
pub active: ColorShifting,
pub focus: ColorShifting,
pub disabled: ColorShifting,
pub dark_convert: ColorShifting,
pub expand_method: ColorExpand,
pub wacg_follows: WACGSetting,
}
#[derive(Debug, Clone, Copy, Display, EnumString, EnumIter, Serialize, Deserialize)]
#[strum(serialize_all = "lowercase")]
pub enum ColorExpand {
Complementary,
Analogous,
AnalogousAndComplementary,
Triadic,
SplitComplementary,
Tetradic,
Square,
}
impl ColorExpand {
pub fn label(&self) -> String {
match self {
ColorExpand::Complementary => "Complementary".to_string(),
ColorExpand::Analogous => "Analogous".to_string(),
ColorExpand::AnalogousAndComplementary => "Analogous and Complementary".to_string(),
ColorExpand::Triadic => "Triadic".to_string(),
ColorExpand::SplitComplementary => "Split Complementary".to_string(),
ColorExpand::Tetradic => "Tetradic".to_string(),
ColorExpand::Square => "Square".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, Display, EnumString, EnumIter, Serialize, Deserialize)]
#[strum(serialize_all = "lowercase")]
pub enum WACGSetting {
Fixed,
AutomaticAA,
AutomaticAAA,
}
impl WACGSetting {
pub fn label(&self) -> String {
match self {
WACGSetting::Fixed => "Fixed".to_string(),
WACGSetting::AutomaticAA => "Automatic AA".to_string(),
WACGSetting::AutomaticAAA => "Automatic AAA".to_string(),
}
}
}
impl Default for SchemeSetting {
fn default() -> Self {
SchemeSetting {
hover: ColorShifting {
chroma: 0.0,
lightness: 0.3,
},
active: ColorShifting {
chroma: 0.0,
lightness: -0.2,
},
focus: ColorShifting {
chroma: 0.0,
lightness: 0.5,
},
disabled: ColorShifting {
chroma: -0.9,
lightness: -0.2,
},
dark_convert: ColorShifting {
chroma: -0.3,
lightness: -0.3,
},
expand_method: ColorExpand::AnalogousAndComplementary,
wacg_follows: WACGSetting::AutomaticAAA,
}
}
}