Compare commits
131 Commits
a94626c192
...
wasm-load
Author | SHA1 | Date | |
---|---|---|---|
|
2ddffe1b12 | ||
|
3eae8116e7 | ||
|
12e76b658e | ||
|
2ec3578e1c | ||
|
f944d48e1b | ||
|
88e3d1f928 | ||
|
2144cd548a | ||
|
2c47369772 | ||
|
2f51a80c91 | ||
|
546ca97b10 | ||
|
6b262f536d | ||
|
6aa3875919 | ||
|
0369f238f2 | ||
|
c60aefaaff | ||
|
7bfe9a7620 | ||
|
1b044c66d7 | ||
|
e74ffc9721 | ||
|
1553c51621 | ||
|
14d775e956 | ||
|
e2806a0cc5 | ||
|
5d3fc2903b | ||
|
71feeb4efc | ||
|
2b9547a7c2 | ||
|
131c43c5cf | ||
|
505af1c67e | ||
|
320b750834 | ||
|
6728ca1be2 | ||
|
83dcb3f80f | ||
|
b8018e323d | ||
|
d68ac6a3df | ||
|
0f5805bb7f | ||
|
a3fb9b656b | ||
|
d817024bf3 | ||
|
d98e3a69d9 | ||
|
8b0e9699c7 | ||
|
8e71d3c555 | ||
|
e9c2d4cb16 | ||
|
2acb69da20 | ||
|
9664983b5c | ||
|
fc340f3f74 | ||
|
f9f855e818 | ||
|
853b9b6b75 | ||
|
41788c4944 | ||
|
2bc250fc3d | ||
|
7468e28928 | ||
|
ca83ce082b | ||
|
89b2a2f9d9 | ||
|
74dd9e7354 | ||
|
592244911f | ||
|
08fabb53a2 | ||
|
0350380df6 | ||
|
32d8457802 | ||
|
b124bb4eda | ||
|
92229b0de4 | ||
|
e3642cad97 | ||
|
59519e1408 | ||
|
56a4786675 | ||
|
838f0c0fa0 | ||
|
9fa05824d2 | ||
|
14851c8284 | ||
|
50646ffccf | ||
|
a9ad4dea5d | ||
|
7b26c95a9a | ||
|
8efb3ec318 | ||
|
c4f703906e | ||
|
6dba92a2c5 | ||
|
4b4428fd3b | ||
|
1b41fb4d22 | ||
|
a3de0f961a | ||
|
79794ed0f7 | ||
|
20757a789a | ||
|
f9f984a1b4 | ||
|
9606106c45 | ||
|
3882ae764f | ||
|
2b17a5de0f | ||
|
b2357811b6 | ||
|
32273256c0 | ||
|
7e2132662f | ||
|
cff2ad0439 | ||
|
dc411987bf | ||
|
26ebc3c7e3 | ||
|
ab4e0b440c | ||
|
e0d35d279f | ||
|
2a4fb7f043 | ||
|
2d91f45809 | ||
|
3ad637e1fa | ||
|
c78a2f0183 | ||
|
4a9dfbb664 | ||
|
86ecb5a258 | ||
|
826e526ba2 | ||
|
119436608a | ||
|
d07fe9d41a | ||
|
c9626f3b8e | ||
|
b3cdb517a5 | ||
|
b126858d8e | ||
|
7a8bbaa826 | ||
|
49bed04748 | ||
|
8573a5d372 | ||
|
b32884a31c | ||
|
a56d473148 | ||
|
8a55c73c83 | ||
|
4bcd84c358 | ||
|
4ba51e018d | ||
|
e90e88954a | ||
|
a980db505b | ||
|
44023fed29 | ||
|
bfd179c4aa | ||
|
b7a5c4a109 | ||
|
60d1f82e09 | ||
|
bd3f6d02ba | ||
|
67aca6926f | ||
|
3ad5babacb | ||
|
665821700b | ||
|
2cb39adc8e | ||
|
7c91f50173 | ||
|
1edc74daaf | ||
|
0eb00122c8 | ||
|
deed113eae | ||
|
8ceeac545d | ||
|
a012f28c47 | ||
|
0f343c606e | ||
|
612f1ba751 | ||
|
c737712d3f | ||
|
67164e35fa | ||
|
9fec4a31e9 | ||
|
6708c40ffb | ||
|
12d6b04ddc | ||
|
f2031f3d8c | ||
|
5e7b1e709d | ||
|
f775c3b78f | ||
|
6bc0779f26 |
51
README.md
51
README.md
@@ -1,50 +1,5 @@
|
||||
# React + TypeScript + Vite
|
||||
# Color Q
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
提供专业的多角度颜色选择、搭配、研究的网站。
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
基于React和WASM构建,不使用任何服务端程序支持。内建常见颜色色卡。
|
||||
|
@@ -8,11 +8,17 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
color-name = "1.1.0"
|
||||
enum-iterator = "2.1.0"
|
||||
palette = { version = "0.7.6", features = ["serde"] }
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
serde_json = "1.0.134"
|
||||
serde_repr = "0.1.19"
|
||||
strum = { version = "0.26.3", features = ["derive", "strum_macros"] }
|
||||
strum_macros = "0.26.4"
|
||||
thiserror = "2.0.9"
|
||||
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
|
||||
web-sys = {version = "0.3.77", features = ["console", "Window"]}
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.49"
|
||||
|
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
wasm-pack build --release --target web -d ../src/color_functions
|
||||
wasm-pack build --release --target web -d ../color_functions
|
||||
|
190
color-module/src/analysis.rs
Normal file
190
color-module/src/analysis.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use palette::{
|
||||
cam16::{Cam16Jch, Parameters},
|
||||
FromColor, Hsl, IntoColor, Oklch, Srgb,
|
||||
};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::{
|
||||
color_differ::{self, ColorDifference},
|
||||
errors, reversing,
|
||||
};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn differ_in_rgb(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::rgb::RGBDifference, errors::ColorError> {
|
||||
let srgb = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let other_srgb = Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>();
|
||||
Ok(srgb.difference(&other_srgb))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn relative_differ_in_rgb(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::rgb::RGBDifference, errors::ColorError> {
|
||||
let srgb = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let other_srgb = Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>();
|
||||
Ok(srgb.difference_relative(&other_srgb))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn differ_in_hsl(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::hsl::HSLDifference, errors::ColorError> {
|
||||
let hsl = Hsl::from_color(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
let other_hsl = Hsl::from_color(
|
||||
Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
Ok(hsl.difference(&other_hsl))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn relative_differ_in_hsl(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::hsl::HSLDifference, errors::ColorError> {
|
||||
let hsl = Hsl::from_color(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
let other_hsl = Hsl::from_color(
|
||||
Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
Ok(hsl.difference_relative(&other_hsl))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn differ_in_hct(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::cam16jch::HctDiffference, errors::ColorError> {
|
||||
let hct = Cam16Jch::from_xyz(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>()
|
||||
.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
let other_hct = Cam16Jch::from_xyz(
|
||||
Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>()
|
||||
.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
Ok(hct.difference(&other_hct))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn relative_differ_in_hct(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::cam16jch::HctDiffference, errors::ColorError> {
|
||||
let hct = Cam16Jch::from_xyz(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>()
|
||||
.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
let other_hct = Cam16Jch::from_xyz(
|
||||
Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>()
|
||||
.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
Ok(hct.difference_relative(&other_hct))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn differ_in_oklch(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::oklch::OklchDifference, errors::ColorError> {
|
||||
let oklch = Oklch::from_color(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
let other_oklch = Oklch::from_color(
|
||||
Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
Ok(oklch.difference(&other_oklch))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn relative_differ_in_oklch(
|
||||
color: &str,
|
||||
other: &str,
|
||||
) -> Result<color_differ::oklch::OklchDifference, errors::ColorError> {
|
||||
let oklch = Oklch::from_color(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
let other_oklch = Oklch::from_color(
|
||||
Srgb::from_str(other)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
Ok(oklch.difference_relative(&other_oklch))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tint_scale(
|
||||
basic_color: &str,
|
||||
mixed_color: &str,
|
||||
) -> Result<reversing::MixReversing, errors::ColorError> {
|
||||
let basic_color = Srgb::from_str(basic_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let mixed_color = Srgb::from_str(mixed_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
Ok(reversing::MixReversing::from_tint_rgb(
|
||||
basic_color,
|
||||
mixed_color,
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn shade_scale(
|
||||
basic_color: &str,
|
||||
mixed_color: &str,
|
||||
) -> Result<reversing::MixReversing, errors::ColorError> {
|
||||
let basic_color = Srgb::from_str(basic_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let mixed_color = Srgb::from_str(mixed_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
Ok(reversing::MixReversing::from_shade_rgb(
|
||||
basic_color,
|
||||
mixed_color,
|
||||
))
|
||||
}
|
132
color-module/src/color_card.rs
Normal file
132
color-module/src/color_card.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::{str::FromStr, sync::LazyLock};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ColorDescription {
|
||||
pub name: String,
|
||||
pub pinyin: Vec<String>,
|
||||
pub hue: f32,
|
||||
pub lightness: f32,
|
||||
pub category: String,
|
||||
pub tags: Vec<String>,
|
||||
pub rgb: [u8; 3],
|
||||
pub hsl: [f32; 3],
|
||||
pub lab: [f32; 3],
|
||||
pub oklch: [f32; 3],
|
||||
}
|
||||
|
||||
const COLOR_CARDS_JSON: &str = include_str!("colorcards.json");
|
||||
pub const COLOR_CARDS: LazyLock<Vec<ColorDescription>> =
|
||||
LazyLock::new(|| serde_json::from_str(COLOR_CARDS_JSON).expect("Failed to parse color cards"));
|
||||
const CHROMA_EPSILON: f32 = 0.02;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum Category {
|
||||
Red,
|
||||
Orange,
|
||||
Yellow,
|
||||
Green,
|
||||
Cyan,
|
||||
Blue,
|
||||
Purple,
|
||||
Magenta,
|
||||
White,
|
||||
Black,
|
||||
Gray,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn from_oklch_components(lightness: f32, chroma: f32, hue: f32) -> Self {
|
||||
if chroma < CHROMA_EPSILON {
|
||||
if lightness < 0.15 {
|
||||
Category::Black
|
||||
} else if lightness > 0.9 {
|
||||
Category::White
|
||||
} else {
|
||||
Category::Gray
|
||||
}
|
||||
} else {
|
||||
let processed_hue = hue % 360.0;
|
||||
match processed_hue {
|
||||
0.0..=15.0 => Category::Magenta,
|
||||
15.0..=45.0 => Category::Red,
|
||||
45.0..=75.0 => Category::Orange,
|
||||
75.0..=120.0 => Category::Yellow,
|
||||
120.0..=180.0 => Category::Green,
|
||||
180.0..=210.0 => Category::Cyan,
|
||||
210.0..=270.0 => Category::Blue,
|
||||
270.0..=345.0 => Category::Purple,
|
||||
345.0..=360.0 => Category::Magenta,
|
||||
_ => Category::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_oklch(oklch: &[f32; 3]) -> Self {
|
||||
Category::from_oklch_components(oklch[0], oklch[1], oklch[2])
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Category::Red => "Red".to_string(),
|
||||
Category::Orange => "Orange".to_string(),
|
||||
Category::Yellow => "Yellow".to_string(),
|
||||
Category::Green => "Green".to_string(),
|
||||
Category::Cyan => "Cyan".to_string(),
|
||||
Category::Blue => "Blue".to_string(),
|
||||
Category::Purple => "Purple".to_string(),
|
||||
Category::Magenta => "Magenta".to_string(),
|
||||
Category::White => "White".to_string(),
|
||||
Category::Black => "Black".to_string(),
|
||||
Category::Gray => "Gray".to_string(),
|
||||
Category::Unknown => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn color_categories() -> Result<JsValue, String> {
|
||||
let categories = Category::iter()
|
||||
.map(|variant| {
|
||||
serde_json::json!({
|
||||
"label": variant.label(),
|
||||
"value": variant.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
serde_wasm_bindgen::to_value(&categories).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn search_color_cards(tag: String, category: Option<String>) -> Result<JsValue, String> {
|
||||
let selected_category = category.and_then(|c| Category::from_str(&c).ok());
|
||||
let all_cards = &*COLOR_CARDS;
|
||||
let mut cards = all_cards
|
||||
.iter()
|
||||
.filter(|card| card.tags.contains(&tag))
|
||||
.filter(|card| {
|
||||
if let Some(category) = &selected_category {
|
||||
let card_category = Category::from_oklch(&card.oklch);
|
||||
card_category == *category
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cards.sort_by(|a, b| {
|
||||
a.oklch[2]
|
||||
.partial_cmp(&b.oklch[2])
|
||||
.or_else(|| a.oklch[1].partial_cmp(&b.oklch[1]))
|
||||
.or_else(|| a.oklch[0].partial_cmp(&b.oklch[0]))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
serde_wasm_bindgen::to_value(&cards).map_err(|e| e.to_string())
|
||||
}
|
84
color-module/src/color_differ/cam16jch.rs
Normal file
84
color-module/src/color_differ/cam16jch.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use palette::cam16::Cam16Jch;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use super::{ColorDifference, Differ};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct HctDiffference {
|
||||
pub hue: Differ,
|
||||
pub chroma: Differ,
|
||||
pub lightness: Differ,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl HctDiffference {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(hue: Differ, chroma: Differ, lightness: Differ) -> Self {
|
||||
Self {
|
||||
hue,
|
||||
chroma,
|
||||
lightness,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorDifference for Cam16Jch<f32> {
|
||||
type Difference = HctDiffference;
|
||||
|
||||
fn difference(&self, other: &Self) -> Self::Difference {
|
||||
let hue = other.hue.into_positive_degrees() - self.hue.into_positive_degrees();
|
||||
let chroma = other.chroma - self.chroma;
|
||||
let lightness = other.lightness - self.lightness;
|
||||
|
||||
HctDiffference {
|
||||
hue: Differ {
|
||||
delta: hue,
|
||||
percent: hue / self.hue.into_positive_degrees(),
|
||||
},
|
||||
chroma: Differ {
|
||||
delta: chroma,
|
||||
percent: chroma / self.chroma,
|
||||
},
|
||||
lightness: Differ {
|
||||
delta: lightness,
|
||||
percent: lightness / self.lightness,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn difference_relative(&self, other: &Self) -> Self::Difference {
|
||||
let hue = if self.hue.into_positive_degrees() == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.hue.into_positive_degrees() - self.hue.into_positive_degrees())
|
||||
/ (360.0 - self.hue.into_positive_degrees())
|
||||
};
|
||||
let chroma = if self.chroma == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.chroma - self.chroma) / self.chroma
|
||||
};
|
||||
let lightness = if self.lightness == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.lightness - self.lightness) / (100.0 - self.lightness)
|
||||
};
|
||||
|
||||
HctDiffference {
|
||||
hue: Differ {
|
||||
delta: other.hue.into_positive_degrees() - self.hue.into_positive_degrees(),
|
||||
percent: hue,
|
||||
},
|
||||
chroma: Differ {
|
||||
delta: other.chroma - self.chroma,
|
||||
percent: chroma,
|
||||
},
|
||||
lightness: Differ {
|
||||
delta: other.lightness - self.lightness,
|
||||
percent: lightness,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
84
color-module/src/color_differ/hsl.rs
Normal file
84
color-module/src/color_differ/hsl.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use palette::Hsl;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use super::{ColorDifference, Differ};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct HSLDifference {
|
||||
pub hue: Differ,
|
||||
pub saturation: Differ,
|
||||
pub lightness: Differ,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl HSLDifference {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(hue: Differ, saturation: Differ, lightness: Differ) -> Self {
|
||||
Self {
|
||||
hue,
|
||||
saturation,
|
||||
lightness,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorDifference for Hsl {
|
||||
type Difference = HSLDifference;
|
||||
|
||||
fn difference(&self, other: &Self) -> Self::Difference {
|
||||
let hue = other.hue.into_positive_degrees() - self.hue.into_positive_degrees();
|
||||
let saturation = other.saturation - self.saturation;
|
||||
let lightness = other.lightness - self.lightness;
|
||||
|
||||
HSLDifference {
|
||||
hue: Differ {
|
||||
delta: hue,
|
||||
percent: hue / self.hue.into_positive_degrees(),
|
||||
},
|
||||
saturation: Differ {
|
||||
delta: saturation,
|
||||
percent: saturation / self.saturation,
|
||||
},
|
||||
lightness: Differ {
|
||||
delta: lightness,
|
||||
percent: lightness / self.lightness,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn difference_relative(&self, other: &Self) -> Self::Difference {
|
||||
let hue = if self.hue.into_positive_degrees() == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.hue.into_positive_degrees() - self.hue.into_positive_degrees())
|
||||
/ (1.0 - self.hue.into_positive_degrees())
|
||||
};
|
||||
let saturation = if self.saturation == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.saturation - self.saturation) / (1.0 - self.saturation)
|
||||
};
|
||||
let lightness = if self.lightness == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.lightness - self.lightness) / (1.0 - self.lightness)
|
||||
};
|
||||
|
||||
HSLDifference {
|
||||
hue: Differ {
|
||||
delta: other.hue.into_positive_degrees() - self.hue.into_positive_degrees(),
|
||||
percent: hue,
|
||||
},
|
||||
saturation: Differ {
|
||||
delta: other.saturation - self.saturation,
|
||||
percent: saturation,
|
||||
},
|
||||
lightness: Differ {
|
||||
delta: other.lightness - self.lightness,
|
||||
percent: lightness,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
29
color-module/src/color_differ/mod.rs
Normal file
29
color-module/src/color_differ/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
pub mod cam16jch;
|
||||
pub mod hsl;
|
||||
pub mod oklch;
|
||||
pub mod rgb;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct Differ {
|
||||
pub delta: f32,
|
||||
pub percent: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Differ {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(delta: f32, percent: f32) -> Self {
|
||||
Self { delta, percent }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ColorDifference {
|
||||
type Difference;
|
||||
|
||||
fn difference(&self, other: &Self) -> Self::Difference;
|
||||
fn difference_relative(&self, other: &Self) -> Self::Difference;
|
||||
}
|
84
color-module/src/color_differ/oklch.rs
Normal file
84
color-module/src/color_differ/oklch.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use palette::Oklch;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use super::{ColorDifference, Differ};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct OklchDifference {
|
||||
pub hue: Differ,
|
||||
pub chroma: Differ,
|
||||
pub lightness: Differ,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OklchDifference {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(hue: Differ, chroma: Differ, lightness: Differ) -> Self {
|
||||
Self {
|
||||
hue,
|
||||
chroma,
|
||||
lightness,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorDifference for Oklch {
|
||||
type Difference = OklchDifference;
|
||||
|
||||
fn difference(&self, other: &Self) -> Self::Difference {
|
||||
let hue = other.hue.into_positive_degrees() - self.hue.into_positive_degrees();
|
||||
let chroma = other.chroma - self.chroma;
|
||||
let lightness = other.l - self.l;
|
||||
|
||||
OklchDifference {
|
||||
hue: Differ {
|
||||
delta: hue,
|
||||
percent: hue / self.hue.into_positive_degrees(),
|
||||
},
|
||||
chroma: Differ {
|
||||
delta: chroma,
|
||||
percent: chroma / self.chroma,
|
||||
},
|
||||
lightness: Differ {
|
||||
delta: lightness,
|
||||
percent: lightness / self.l,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn difference_relative(&self, other: &Self) -> Self::Difference {
|
||||
let hue = if self.hue.into_positive_degrees() == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.hue.into_positive_degrees() - self.hue.into_positive_degrees())
|
||||
/ (1.0 - self.hue.into_positive_degrees())
|
||||
};
|
||||
let chroma = if self.chroma == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.chroma - self.chroma) / self.chroma
|
||||
};
|
||||
let lightness = if self.l == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.l - self.l) / (1.0 - self.l)
|
||||
};
|
||||
|
||||
OklchDifference {
|
||||
hue: Differ {
|
||||
delta: other.hue.into_positive_degrees() - self.hue.into_positive_degrees(),
|
||||
percent: hue,
|
||||
},
|
||||
chroma: Differ {
|
||||
delta: other.chroma - self.chroma,
|
||||
percent: chroma,
|
||||
},
|
||||
lightness: Differ {
|
||||
delta: other.l - self.l,
|
||||
percent: lightness,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
79
color-module/src/color_differ/rgb.rs
Normal file
79
color-module/src/color_differ/rgb.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use palette::Srgb;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use super::{ColorDifference, Differ};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct RGBDifference {
|
||||
pub r: Differ,
|
||||
pub g: Differ,
|
||||
pub b: Differ,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RGBDifference {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(r: Differ, g: Differ, b: Differ) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorDifference for Srgb {
|
||||
type Difference = RGBDifference;
|
||||
|
||||
fn difference(&self, other: &Self) -> Self::Difference {
|
||||
let r = other.red - self.red;
|
||||
let g = other.green - self.green;
|
||||
let b = other.blue - self.blue;
|
||||
|
||||
RGBDifference {
|
||||
r: Differ {
|
||||
delta: r,
|
||||
percent: r / self.red,
|
||||
},
|
||||
g: Differ {
|
||||
delta: g,
|
||||
percent: g / self.green,
|
||||
},
|
||||
b: Differ {
|
||||
delta: b,
|
||||
percent: b / self.blue,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn difference_relative(&self, other: &Self) -> Self::Difference {
|
||||
let r = if self.red == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.red - self.red) / (1.0 - self.red)
|
||||
};
|
||||
let g = if self.green == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.green - self.green) / (1.0 - self.green)
|
||||
};
|
||||
let b = if self.blue == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(other.blue - self.blue) / (1.0 - self.blue)
|
||||
};
|
||||
|
||||
RGBDifference {
|
||||
r: Differ {
|
||||
delta: other.red - self.red,
|
||||
percent: r,
|
||||
},
|
||||
g: Differ {
|
||||
delta: other.green - self.green,
|
||||
percent: g,
|
||||
},
|
||||
b: Differ {
|
||||
delta: other.blue - self.blue,
|
||||
percent: b,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
80
color-module/src/color_shifting.rs
Normal file
80
color-module/src/color_shifting.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use palette::{Darken, FromColor, Lighten, Mix, Oklch, Srgb};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::errors;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn lighten(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let lightened_color = oklch.lighten(percent);
|
||||
let srgb = Srgb::from_color(lightened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn lighten_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let lightened_color = oklch.lighten_fixed(value);
|
||||
let srgb = Srgb::from_color(lightened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn darken(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let darkened_color = oklch.darken(percent);
|
||||
let srgb = Srgb::from_color(darkened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn darken_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let darkened_color = oklch.darken_fixed(value);
|
||||
let srgb = Srgb::from_color(darkened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn mix(color1: &str, color2: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let srgb1 = Srgb::from_str(color1)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color1.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let srgb2 = Srgb::from_str(color2)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color2.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let mixed_color = srgb1.mix(srgb2, percent);
|
||||
Ok(format!("{:x}", mixed_color.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tint(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let tinted_color = origin_color.mix(Srgb::new(1.0, 1.0, 1.0), percent);
|
||||
Ok(format!("{:x}", tinted_color.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn shade(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let shaded_color = origin_color.mix(Srgb::new(0.0, 0.0, 0.0), percent);
|
||||
Ok(format!("{:x}", shaded_color.into_format::<u8>()))
|
||||
}
|
1
color-module/src/colorcards.json
Normal file
1
color-module/src/colorcards.json
Normal file
File diff suppressed because one or more lines are too long
127
color-module/src/convert/mod.rs
Normal file
127
color-module/src/convert/mod.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use palette::{
|
||||
cam16::{Cam16Jch, Parameters},
|
||||
convert::FromColorUnclamped,
|
||||
luma::Luma,
|
||||
Hsl, IntoColor, IsWithinBounds, Lch, Lchuv, Oklab, Oklch, Srgb,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
|
||||
let original_xyz = origin.into_xyz(Parameters::default_static_wp(40.0));
|
||||
let mut new_srgb = Srgb::from_color_unclamped(original_xyz);
|
||||
if new_srgb.is_within_bounds() {
|
||||
return new_srgb;
|
||||
}
|
||||
|
||||
let lchuv: Lchuv = Lchuv::from_color_unclamped(original_xyz);
|
||||
let mut c: f32 = lchuv.chroma;
|
||||
let original_c = c;
|
||||
let h = lchuv.hue;
|
||||
let l = lchuv.l;
|
||||
|
||||
loop {
|
||||
let new_lchuv = Lchuv::new(l, c, h);
|
||||
new_srgb = new_lchuv.into_color();
|
||||
c -= original_c / 1000.0;
|
||||
if c > 0.0 && (new_srgb.is_within_bounds()) {
|
||||
break new_srgb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch<f32>) -> String {
|
||||
format!("{:x}", map_cam16jch_to_srgb(origin).into_format::<u8>())
|
||||
}
|
||||
|
||||
pub fn map_lch_to_srgb(origin: &Lch) -> Srgb {
|
||||
let mut new_srgb: Srgb = (*origin).into_color();
|
||||
if new_srgb.is_within_bounds() {
|
||||
return new_srgb;
|
||||
}
|
||||
|
||||
let mut c: f32 = origin.chroma;
|
||||
let original_c = c;
|
||||
let h = origin.hue;
|
||||
let l = origin.l;
|
||||
|
||||
loop {
|
||||
let new_lch = Lch::new(l, c, h);
|
||||
new_srgb = new_lch.into_color();
|
||||
c -= original_c / 1000.0;
|
||||
if c > 0.0 && (new_srgb.is_within_bounds()) {
|
||||
break new_srgb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_lch_to_srgb_hex(origin: &Lch) -> String {
|
||||
format!("{:x}", map_lch_to_srgb(origin).into_format::<u8>())
|
||||
}
|
||||
|
||||
pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb {
|
||||
let mut new_original = Hsl::new(origin.hue, origin.saturation, origin.lightness);
|
||||
const FACTOR: f32 = 0.99;
|
||||
loop {
|
||||
let new_srgb = Srgb::from_color_unclamped(new_original);
|
||||
if new_srgb.is_within_bounds() {
|
||||
break new_srgb;
|
||||
}
|
||||
new_original = Hsl::new(
|
||||
new_original.hue,
|
||||
new_original.saturation * FACTOR,
|
||||
new_original.lightness,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_hsl_to_srgb_hex(origin: &Hsl) -> String {
|
||||
format!("{:x}", map_hsl_to_srgb(origin).into_format::<u8>())
|
||||
}
|
||||
|
||||
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>(),
|
||||
)
|
||||
};
|
||||
($origin: expr) => {
|
||||
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()?
|
||||
};
|
||||
}
|
@@ -7,6 +7,10 @@ pub enum ColorError {
|
||||
UnrecogniazedRGB(String),
|
||||
#[error("Some color component is out of bounds")]
|
||||
ComponentOutOfBounds,
|
||||
#[error("Unable to parse argument")]
|
||||
UnableToParseArgument,
|
||||
#[error("Unable to assemble output")]
|
||||
UnableToAssembleOutput,
|
||||
}
|
||||
|
||||
impl Into<JsValue> for ColorError {
|
||||
|
@@ -1,15 +1,24 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use std::str::FromStr;
|
||||
|
||||
use palette::{
|
||||
cam16::{Cam16Jch, Parameters},
|
||||
color_difference::Wcag21RelativeContrast,
|
||||
color_theory::*,
|
||||
convert::FromColorUnclamped,
|
||||
Darken, FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Lighten, Mix, Oklch, ShiftHue, Srgb,
|
||||
FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Oklch, Srgb,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod analysis;
|
||||
mod color_card;
|
||||
mod color_differ;
|
||||
mod color_shifting;
|
||||
mod convert;
|
||||
mod errors;
|
||||
mod palettes;
|
||||
mod reversing;
|
||||
mod schemes;
|
||||
mod series;
|
||||
mod theory;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn represent_rgb(color: &str) -> Result<Box<[u8]>, errors::ColorError> {
|
||||
@@ -116,91 +125,6 @@ pub fn hct_to_hex(hue: f32, chroma: f32, tone: f32) -> Result<String, errors::Co
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn shift_hue(color: &str, degree: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let shifted_color = oklch.shift_hue(degree);
|
||||
let srgb = Srgb::from_color(shifted_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn lighten(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let lightened_color = oklch.lighten(percent);
|
||||
let srgb = Srgb::from_color(lightened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn lighten_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let lightened_color = oklch.lighten_fixed(value);
|
||||
let srgb = Srgb::from_color(lightened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn darken(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let darkened_color = oklch.darken(percent);
|
||||
let srgb = Srgb::from_color(darkened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn darken_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let darkened_color = oklch.darken_fixed(value);
|
||||
let srgb = Srgb::from_color(darkened_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn mix(color1: &str, color2: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let srgb1 = Srgb::from_str(color1)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color1.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let srgb2 = Srgb::from_str(color2)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color2.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let mixed_color = srgb1.mix(srgb2, percent);
|
||||
Ok(format!("{:x}", mixed_color.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tint(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let tinted_color = origin_color.mix(Srgb::new(1.0, 1.0, 1.0), percent);
|
||||
Ok(format!("{:x}", tinted_color.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn shade(color: &str, percent: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let shaded_color = origin_color.mix(Srgb::new(0.0, 0.0, 0.0), percent);
|
||||
Ok(format!("{:x}", shaded_color.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, errors::ColorError> {
|
||||
let fg_srgb = Srgb::from_str(fg_color)
|
||||
@@ -211,169 +135,3 @@ pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, err
|
||||
.into_format::<f32>();
|
||||
Ok(fg_srgb.relative_contrast(bg_srgb))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn analogous_30(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_n30, color_p30) = oklch.analogous();
|
||||
let srgb_n30 = Srgb::from_color(color_n30);
|
||||
let srgb_p30 = Srgb::from_color(color_p30);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_n30.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p30.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn analogous_60(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_n60, color_p60) = oklch.analogous_secondary();
|
||||
let srgb_n60 = Srgb::from_color(color_n60);
|
||||
let srgb_p60 = Srgb::from_color(color_p60);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_n60.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p60.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn complementary(color: &str) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let complementary_color = oklch.complementary();
|
||||
let srgb = Srgb::from_color(complementary_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn split_complementary(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_p150, color_p210) = oklch.split_complementary();
|
||||
let srgb_p150 = Srgb::from_color(color_p150);
|
||||
let srgb_p210 = Srgb::from_color(color_p210);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_p150.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p210.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tetradic(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_p90, color_p180, color_p270) = oklch.tetradic();
|
||||
let srgb_p90 = Srgb::from_color(color_p90);
|
||||
let srgb_p180 = Srgb::from_color(color_p180);
|
||||
let srgb_p270 = Srgb::from_color(color_p270);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_p90.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p180.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p270.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn triadic(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_p120, color_p240) = oklch.triadic();
|
||||
let srgb_p120 = Srgb::from_color(color_p120);
|
||||
let srgb_p240 = Srgb::from_color(color_p240);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_p120.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p240.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn series(
|
||||
color: &str,
|
||||
expand_amount: i16,
|
||||
step: f32,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Arc::new(Oklch::from_color(origin_color.clone()));
|
||||
|
||||
let mut color_series = Vec::new();
|
||||
for s in (1..=expand_amount).rev() {
|
||||
let darkened_color = Arc::clone(&oklch).darken(s as f32 * step);
|
||||
let srgb = Srgb::from_color(darkened_color);
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
color_series.push(format!("{:x}", origin_color.into_format::<u8>()));
|
||||
for s in 1..=expand_amount {
|
||||
let lightened_color = Arc::clone(&oklch).lighten(s as f32 * step);
|
||||
let srgb = Srgb::from_color(lightened_color);
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
Ok(color_series)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tonal_lighten_series(
|
||||
color: &str,
|
||||
expand_amount: i16,
|
||||
step: f32,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let hct = Cam16Jch::from_xyz(
|
||||
origin_color.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
|
||||
let mut color_series = Vec::new();
|
||||
let mut lightness = hct.lightness;
|
||||
for _ in 1..=expand_amount {
|
||||
lightness += (100.0 - lightness) * step;
|
||||
let lightened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
|
||||
let srgb = Srgb::from_color(lightened_color.into_xyz(Parameters::default_static_wp(40.0)));
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
|
||||
Ok(color_series)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tonal_darken_series(
|
||||
color: &str,
|
||||
expand_amount: i16,
|
||||
step: f32,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let hct = Cam16Jch::from_xyz(
|
||||
origin_color.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
|
||||
let mut color_series = Vec::new();
|
||||
let mut lightness = hct.lightness;
|
||||
for _ in 1..=expand_amount {
|
||||
lightness *= 1.0 - step;
|
||||
let darkened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
|
||||
let srgb = Srgb::from_color(darkened_color.into_xyz(Parameters::default_static_wp(40.0)));
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
|
||||
Ok(color_series.into_iter().rev().collect())
|
||||
}
|
||||
|
94
color-module/src/palettes/auto_palette.rs
Normal file
94
color-module/src/palettes/auto_palette.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use palette::{FromColor, Oklch, Srgb};
|
||||
use std::str::FromStr;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::errors;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_palette_from_color(
|
||||
reference_color: &str,
|
||||
swatch_amount: i16,
|
||||
minimum_lightness: f32,
|
||||
maximum_lightness: f32,
|
||||
use_reference_color: Option<bool>,
|
||||
reference_color_bias: Option<i16>,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let reference_color_bias = reference_color_bias.unwrap_or(0);
|
||||
let original_color = Oklch::from_color(
|
||||
Srgb::from_str(reference_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
match use_reference_color {
|
||||
Some(true) => Ok(generate_incontinuous_palette(
|
||||
&original_color,
|
||||
swatch_amount,
|
||||
minimum_lightness,
|
||||
maximum_lightness,
|
||||
reference_color_bias,
|
||||
)),
|
||||
Some(false) | None => Ok(generate_continuous_palette(
|
||||
&original_color,
|
||||
swatch_amount,
|
||||
minimum_lightness,
|
||||
maximum_lightness,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_continuous_palette(
|
||||
original_color: &Oklch,
|
||||
swatch_amount: i16,
|
||||
minimum_lightness: f32,
|
||||
maximum_lightness: f32,
|
||||
) -> Vec<String> {
|
||||
let mut palette = Vec::new();
|
||||
|
||||
let step = (maximum_lightness - minimum_lightness) / (swatch_amount - 1) as f32;
|
||||
for i in 0..swatch_amount {
|
||||
let lightness = minimum_lightness + step * i as f32;
|
||||
let color = Oklch {
|
||||
l: lightness,
|
||||
..*original_color
|
||||
};
|
||||
palette.push(format!("{:x}", Srgb::from_color(color).into_format::<u8>()));
|
||||
}
|
||||
|
||||
palette
|
||||
}
|
||||
|
||||
fn generate_incontinuous_palette(
|
||||
original_color: &Oklch,
|
||||
swatch_amount: i16,
|
||||
minimum_lightness: f32,
|
||||
maximum_lightness: f32,
|
||||
original_place: i16,
|
||||
) -> Vec<String> {
|
||||
let mut palette = Vec::new();
|
||||
|
||||
let midpoint = swatch_amount / 2;
|
||||
let dark_side_amount = midpoint + original_place;
|
||||
|
||||
let step = (original_color.l - minimum_lightness) / dark_side_amount as f32;
|
||||
for i in 0..dark_side_amount {
|
||||
let lightness = minimum_lightness + step * i as f32;
|
||||
let color = Oklch {
|
||||
l: lightness,
|
||||
..*original_color
|
||||
};
|
||||
palette.push(format!("{:x}", Srgb::from_color(color).into_format::<u8>()));
|
||||
}
|
||||
|
||||
let light_side_amount = swatch_amount - dark_side_amount;
|
||||
let step = (maximum_lightness - original_color.l) / (light_side_amount - 1) as f32;
|
||||
for i in 0..light_side_amount {
|
||||
let lightness = original_color.l + step * i as f32;
|
||||
let color = Oklch {
|
||||
l: lightness,
|
||||
..*original_color
|
||||
};
|
||||
palette.push(format!("{:x}", Srgb::from_color(color).into_format::<u8>()));
|
||||
}
|
||||
|
||||
palette
|
||||
}
|
1
color-module/src/palettes/mod.rs
Normal file
1
color-module/src/palettes/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod auto_palette;
|
79
color-module/src/reversing.rs
Normal file
79
color-module/src/reversing.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use palette::rgb::Rgb;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct MixReversing {
|
||||
pub r_factor: f32,
|
||||
pub g_factor: f32,
|
||||
pub b_factor: f32,
|
||||
pub average: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MixReversing {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(r_factor: f32, g_factor: f32, b_factor: f32, average: f32) -> Self {
|
||||
Self {
|
||||
r_factor,
|
||||
g_factor,
|
||||
b_factor,
|
||||
average,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MixReversing {
|
||||
pub fn from_tint_rgb(basic_color: Rgb, mixed_result: Rgb) -> Self {
|
||||
let r_factor = if basic_color.red == 1.0 {
|
||||
0.0
|
||||
} else {
|
||||
(mixed_result.red - basic_color.red) / (1.0 - basic_color.red)
|
||||
};
|
||||
let g_factor = if basic_color.green == 1.0 {
|
||||
0.0
|
||||
} else {
|
||||
(mixed_result.green - basic_color.green) / (1.0 - basic_color.green)
|
||||
};
|
||||
let b_factor = if basic_color.blue == 1.0 {
|
||||
0.0
|
||||
} else {
|
||||
(mixed_result.blue - basic_color.blue) / (1.0 - basic_color.blue)
|
||||
};
|
||||
let average = (r_factor + g_factor + b_factor) / 3.0;
|
||||
|
||||
MixReversing {
|
||||
r_factor,
|
||||
g_factor,
|
||||
b_factor,
|
||||
average,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_shade_rgb(basic_color: Rgb, mixed_result: Rgb) -> Self {
|
||||
let r_factor = if basic_color.red == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(mixed_result.red - basic_color.red) / (0.0 - basic_color.red)
|
||||
};
|
||||
let g_factor = if basic_color.green == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(mixed_result.green - basic_color.green) / (0.0 - basic_color.green)
|
||||
};
|
||||
let b_factor = if basic_color.blue == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(mixed_result.blue - basic_color.blue) / (0.0 - basic_color.blue)
|
||||
};
|
||||
let average = (r_factor + g_factor + b_factor) / 3.0;
|
||||
|
||||
MixReversing {
|
||||
r_factor,
|
||||
g_factor,
|
||||
b_factor,
|
||||
average,
|
||||
}
|
||||
}
|
||||
}
|
136
color-module/src/schemes/material_design_2/baseline.rs
Normal file
136
color-module/src/schemes/material_design_2/baseline.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{ser::SerializeStruct, Serialize};
|
||||
|
||||
use crate::{convert::map_hsl_to_srgb_hex, errors, schemes::material_design_2::swatch::M2Swatch};
|
||||
|
||||
use super::{color_set::M2ColorSet, swatch::SwatchIndex};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct M2BaselineColors {
|
||||
pub primary: M2ColorSet,
|
||||
pub secondary: M2ColorSet,
|
||||
pub error: M2ColorSet,
|
||||
pub background: M2ColorSet,
|
||||
pub surface: M2ColorSet,
|
||||
pub shadow: String,
|
||||
pub custom_colors: HashMap<String, M2ColorSet>,
|
||||
neutral_swatch: M2Swatch,
|
||||
dark_set: bool,
|
||||
}
|
||||
|
||||
impl M2BaselineColors {
|
||||
pub fn new(
|
||||
primary_color: &str,
|
||||
secondary_color: &str,
|
||||
error_color: &str,
|
||||
dark_baseline: bool,
|
||||
) -> Result<Self, errors::ColorError> {
|
||||
let primary_swatch = M2Swatch::from_color(primary_color)?;
|
||||
let secondary_swatch = M2Swatch::from_color(secondary_color)?;
|
||||
let error_swatch = M2Swatch::from_color(error_color)?;
|
||||
let neutral_swatch = M2Swatch::default_neutral();
|
||||
|
||||
Ok(Self {
|
||||
primary: M2ColorSet::from_swatch(
|
||||
&primary_swatch,
|
||||
&neutral_swatch,
|
||||
dark_baseline,
|
||||
None,
|
||||
)?,
|
||||
secondary: M2ColorSet::from_swatch(
|
||||
&secondary_swatch,
|
||||
&neutral_swatch,
|
||||
dark_baseline,
|
||||
None,
|
||||
)?,
|
||||
error: M2ColorSet::from_swatch(&error_swatch, &neutral_swatch, dark_baseline, None)?,
|
||||
background: M2ColorSet::surface(&neutral_swatch, dark_baseline)?,
|
||||
surface: M2ColorSet::surface(&neutral_swatch, dark_baseline)?,
|
||||
shadow: map_hsl_to_srgb_hex(&neutral_swatch.tone(SwatchIndex::SI900)),
|
||||
custom_colors: HashMap::new(),
|
||||
neutral_swatch,
|
||||
dark_set: dark_baseline,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
|
||||
let swatch = M2Swatch::from_color(color)?;
|
||||
self.custom_colors.insert(
|
||||
name.to_string(),
|
||||
M2ColorSet::from_swatch(&swatch, &self.neutral_swatch, self.dark_set, None)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_css_variable(&self) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
let prefix = if self.dark_set { "dark" } else { "light" };
|
||||
|
||||
variable_lines.extend(self.primary.to_css_variable(&prefix, "primary"));
|
||||
variable_lines.extend(self.secondary.to_css_variable(&prefix, "secondary"));
|
||||
variable_lines.extend(self.error.to_css_variable(&prefix, "error"));
|
||||
variable_lines.extend(self.background.to_css_variable(&prefix, "background"));
|
||||
variable_lines.extend(self.surface.to_css_variable(&prefix, "surface"));
|
||||
variable_lines.push(format!("--color-{}-shadow: #{};", prefix, self.shadow));
|
||||
|
||||
for (name, color_set) in &self.custom_colors {
|
||||
variable_lines.extend(color_set.to_css_variable(&prefix, name));
|
||||
}
|
||||
|
||||
variable_lines
|
||||
}
|
||||
|
||||
pub fn to_scss_variable(&self) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
let prefix = if self.dark_set { "dark" } else { "light" };
|
||||
|
||||
variable_lines.extend(self.primary.to_scss_variable(&prefix, "primary"));
|
||||
variable_lines.extend(self.secondary.to_scss_variable(&prefix, "secondary"));
|
||||
variable_lines.extend(self.error.to_scss_variable(&prefix, "error"));
|
||||
variable_lines.extend(self.background.to_scss_variable(&prefix, "background"));
|
||||
variable_lines.extend(self.surface.to_scss_variable(&prefix, "surface"));
|
||||
variable_lines.push(format!("$color-{}-shadow: #{};", prefix, self.shadow));
|
||||
|
||||
for (name, color_set) in &self.custom_colors {
|
||||
variable_lines.extend(color_set.to_scss_variable(&prefix, name));
|
||||
}
|
||||
|
||||
variable_lines
|
||||
}
|
||||
|
||||
pub fn to_javascript_object(&self) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
let prefix = if self.dark_set { "dark" } else { "light" };
|
||||
|
||||
variable_lines.extend(self.primary.to_javascript_object(&prefix, "primary"));
|
||||
variable_lines.extend(self.secondary.to_javascript_object(&prefix, "secondary"));
|
||||
variable_lines.extend(self.error.to_javascript_object(&prefix, "error"));
|
||||
variable_lines.extend(self.background.to_javascript_object(&prefix, "background"));
|
||||
variable_lines.extend(self.surface.to_javascript_object(&prefix, "surface"));
|
||||
variable_lines.push(format!("{}Shadow: '#{}',", prefix, self.shadow));
|
||||
|
||||
for (name, color_set) in &self.custom_colors {
|
||||
variable_lines.extend(color_set.to_javascript_object(&prefix, name));
|
||||
}
|
||||
|
||||
variable_lines
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for M2BaselineColors {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_struct("baseline", 7)?;
|
||||
map.serialize_field("primary", &self.primary)?;
|
||||
map.serialize_field("secondary", &self.secondary)?;
|
||||
map.serialize_field("error", &self.error)?;
|
||||
map.serialize_field("background", &self.background)?;
|
||||
map.serialize_field("surface", &self.surface)?;
|
||||
map.serialize_field("shadow", &self.shadow)?;
|
||||
map.serialize_field("custom_colors", &self.custom_colors)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
115
color-module/src/schemes/material_design_2/color_set.rs
Normal file
115
color-module/src/schemes/material_design_2/color_set.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{convert::map_hsl_to_srgb_hex, errors};
|
||||
|
||||
use super::swatch::M2Swatch;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct M2ColorSet {
|
||||
pub root: String,
|
||||
pub variant: String,
|
||||
pub on: String,
|
||||
}
|
||||
|
||||
impl M2ColorSet {
|
||||
pub fn from_swatch(
|
||||
swatch: &M2Swatch,
|
||||
neutral: &M2Swatch,
|
||||
dark: bool,
|
||||
required_wacg_ratio: Option<f32>,
|
||||
) -> Result<Self, errors::ColorError> {
|
||||
let root_color = if dark {
|
||||
map_hsl_to_srgb_hex(&swatch.desaturated_key_tone())
|
||||
} else {
|
||||
map_hsl_to_srgb_hex(&swatch.key_tone())
|
||||
};
|
||||
if dark {
|
||||
Ok(Self {
|
||||
variant: map_hsl_to_srgb_hex(&swatch.desaturated_tone(&swatch.key_index - 2)),
|
||||
on: match required_wacg_ratio {
|
||||
Some(ratio) => map_hsl_to_srgb_hex(
|
||||
&neutral.desaturated_wacg_min_above(&root_color, ratio)?,
|
||||
),
|
||||
None => map_hsl_to_srgb_hex(&neutral.desaturated_wacg_max_tone(&root_color)?),
|
||||
},
|
||||
root: root_color,
|
||||
})
|
||||
} else {
|
||||
Ok(Self {
|
||||
variant: map_hsl_to_srgb_hex(&swatch.tone(&swatch.key_index + 2)),
|
||||
on: match required_wacg_ratio {
|
||||
Some(ratio) => {
|
||||
map_hsl_to_srgb_hex(&neutral.wacg_min_above(&root_color, ratio)?)
|
||||
}
|
||||
None => map_hsl_to_srgb_hex(&neutral.wacg_max_tone(&root_color)?),
|
||||
},
|
||||
root: root_color,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surface(neutral: &M2Swatch, dark: bool) -> Result<Self, errors::ColorError> {
|
||||
let root_color_index = if dark {
|
||||
super::swatch::SwatchIndex::SI900
|
||||
} else {
|
||||
super::swatch::SwatchIndex::SI50
|
||||
};
|
||||
if dark {
|
||||
Ok(Self {
|
||||
root: map_hsl_to_srgb_hex(&neutral.desaturated_tone(root_color_index)),
|
||||
variant: map_hsl_to_srgb_hex(&neutral.desaturated_tone(root_color_index + 2)),
|
||||
on: map_hsl_to_srgb_hex(&neutral.tone(super::swatch::SwatchIndex::SI50)),
|
||||
})
|
||||
} else {
|
||||
Ok(Self {
|
||||
root: map_hsl_to_srgb_hex(&neutral.tone(root_color_index)),
|
||||
variant: map_hsl_to_srgb_hex(&neutral.tone(root_color_index - 2)),
|
||||
on: map_hsl_to_srgb_hex(&neutral.tone(super::swatch::SwatchIndex::SI900)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_css_variable(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
|
||||
variable_lines.push(format!("--color-{}-{}: #{};", prefix, name, self.root));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-{}-variant: #{};",
|
||||
prefix, name, self.variant
|
||||
));
|
||||
variable_lines.push(format!("--color-{}-on-{}: #{};", prefix, name, self.on));
|
||||
|
||||
variable_lines
|
||||
}
|
||||
|
||||
pub fn to_scss_variable(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
|
||||
variable_lines.push(format!("$color-{}-{}: #{};", prefix, name, self.root));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-{}-variant: #{};",
|
||||
prefix, name, self.variant
|
||||
));
|
||||
variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on));
|
||||
|
||||
variable_lines
|
||||
}
|
||||
|
||||
pub fn to_javascript_object(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
let prefix = prefix.to_ascii_lowercase();
|
||||
let name = name
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_ascii_uppercase()
|
||||
.to_string()
|
||||
+ &name[1..];
|
||||
|
||||
variable_lines.push(format!("{}{}: '#{}',", prefix, name, self.root));
|
||||
variable_lines.push(format!("{}{}Variant: '#{}',", prefix, name, self.variant));
|
||||
variable_lines.push(format!("{}On{}: '#{}',", prefix, name, self.on));
|
||||
|
||||
variable_lines
|
||||
}
|
||||
}
|
95
color-module/src/schemes/material_design_2/mod.rs
Normal file
95
color-module/src/schemes/material_design_2/mod.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use baseline::M2BaselineColors;
|
||||
use palette::Hsl;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{convert::map_hsl_to_srgb_hex, errors};
|
||||
|
||||
use super::SchemeExport;
|
||||
|
||||
mod baseline;
|
||||
mod color_set;
|
||||
mod swatch;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MaterialDesign2Scheme {
|
||||
pub white: String,
|
||||
pub black: String,
|
||||
pub light: M2BaselineColors,
|
||||
pub dark: M2BaselineColors,
|
||||
}
|
||||
|
||||
impl MaterialDesign2Scheme {
|
||||
pub fn new(primary: &str, secondary: &str, error: &str) -> Result<Self, errors::ColorError> {
|
||||
Ok(Self {
|
||||
white: map_hsl_to_srgb_hex(&Hsl::new(0.0, 0.0, 1.0)),
|
||||
black: map_hsl_to_srgb_hex(&Hsl::new(0.0, 0.0, 0.0)),
|
||||
light: M2BaselineColors::new(primary, secondary, error, false)?,
|
||||
dark: M2BaselineColors::new(primary, secondary, error, true)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
|
||||
self.light.add_custom_color(name, color)?;
|
||||
self.dark.add_custom_color(name, color)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeExport for MaterialDesign2Scheme {
|
||||
fn output_css_variables(&self) -> String {
|
||||
let mut css_variables = Vec::new();
|
||||
|
||||
css_variables.push(format!("--color-white: #{};", self.white));
|
||||
css_variables.push(format!("--color-black: #{};", self.black));
|
||||
|
||||
css_variables.extend(self.light.to_css_variable());
|
||||
css_variables.extend(self.dark.to_css_variable());
|
||||
|
||||
css_variables.join("\n")
|
||||
}
|
||||
|
||||
fn output_scss_variables(&self) -> String {
|
||||
let mut scss_variables = Vec::new();
|
||||
|
||||
scss_variables.push(format!("$color-white: #{};", self.white));
|
||||
scss_variables.push(format!("$color-black: #{};", self.black));
|
||||
|
||||
scss_variables.extend(self.light.to_scss_variable());
|
||||
scss_variables.extend(self.dark.to_scss_variable());
|
||||
|
||||
scss_variables.join("\n")
|
||||
}
|
||||
|
||||
fn output_javascript_object(&self) -> String {
|
||||
let mut js_object = Vec::new();
|
||||
|
||||
js_object.push("{".to_string());
|
||||
|
||||
js_object.push(format!(" white: '#{}'", self.white));
|
||||
js_object.push(format!(" black: '#{}'", self.black));
|
||||
|
||||
js_object.push(" light: {".to_string());
|
||||
js_object.extend(
|
||||
self.light
|
||||
.to_javascript_object()
|
||||
.into_iter()
|
||||
.map(|s| format!(" {}", s))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
js_object.push(" }".to_string());
|
||||
|
||||
js_object.push(" dark: {".to_string());
|
||||
js_object.extend(
|
||||
self.dark
|
||||
.to_javascript_object()
|
||||
.into_iter()
|
||||
.map(|s| format!(" {}", s))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
js_object.push(" }".to_string());
|
||||
|
||||
js_object.push("}".to_string());
|
||||
|
||||
js_object.join("\n")
|
||||
}
|
||||
}
|
306
color-module/src/schemes/material_design_2/swatch.rs
Normal file
306
color-module/src/schemes/material_design_2/swatch.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
use std::{
|
||||
ops::{Add, Sub},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use palette::{color_difference::Wcag21RelativeContrast, FromColor, Hsl, Srgb};
|
||||
|
||||
use crate::{convert::map_hsl_to_srgb, errors};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct M2Swatch {
|
||||
_key_color: Hsl,
|
||||
swatch: Vec<Hsl>,
|
||||
desaturated_swatch: Vec<Hsl>,
|
||||
pub key_index: SwatchIndex,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SwatchIndex {
|
||||
SI900,
|
||||
SI800,
|
||||
SI700,
|
||||
SI600,
|
||||
SI500,
|
||||
SI400,
|
||||
SI300,
|
||||
SI200,
|
||||
SI100,
|
||||
SI50,
|
||||
}
|
||||
|
||||
impl SwatchIndex {
|
||||
pub fn to_usize(&self) -> usize {
|
||||
match self {
|
||||
SwatchIndex::SI900 => 0,
|
||||
SwatchIndex::SI800 => 1,
|
||||
SwatchIndex::SI700 => 2,
|
||||
SwatchIndex::SI600 => 3,
|
||||
SwatchIndex::SI500 => 4,
|
||||
SwatchIndex::SI400 => 5,
|
||||
SwatchIndex::SI300 => 6,
|
||||
SwatchIndex::SI200 => 7,
|
||||
SwatchIndex::SI100 => 8,
|
||||
SwatchIndex::SI50 => 9,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_usize(index: usize) -> Self {
|
||||
match index {
|
||||
0 => SwatchIndex::SI900,
|
||||
1 => SwatchIndex::SI800,
|
||||
2 => SwatchIndex::SI700,
|
||||
3 => SwatchIndex::SI600,
|
||||
4 => SwatchIndex::SI500,
|
||||
5 => SwatchIndex::SI400,
|
||||
6 => SwatchIndex::SI300,
|
||||
7 => SwatchIndex::SI200,
|
||||
8 => SwatchIndex::SI100,
|
||||
9 => SwatchIndex::SI50,
|
||||
_ => panic!("Invalid index"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<i16> for SwatchIndex {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: i16) -> Self::Output {
|
||||
let index = (self.to_usize() as i16 + rhs).clamp(0, 9);
|
||||
SwatchIndex::from_usize(index as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<i16> for &SwatchIndex {
|
||||
type Output = SwatchIndex;
|
||||
|
||||
fn add(self, rhs: i16) -> Self::Output {
|
||||
let index = (self.to_usize() as i16 + rhs).clamp(0, 9);
|
||||
SwatchIndex::from_usize(index as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<i16> for SwatchIndex {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: i16) -> Self::Output {
|
||||
let index = (self.to_usize() as i16 - rhs).clamp(0, 9);
|
||||
SwatchIndex::from_usize(index as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<i16> for &SwatchIndex {
|
||||
type Output = SwatchIndex;
|
||||
|
||||
fn sub(self, rhs: i16) -> Self::Output {
|
||||
let index = (self.to_usize() as i16 - rhs).clamp(0, 9);
|
||||
SwatchIndex::from_usize(index as usize)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_key_color_place(value: f32, min_value: f32, max_value: f32) -> usize {
|
||||
let interval = (max_value - min_value) / 9.0;
|
||||
let values = (0..=9)
|
||||
.map(|i| min_value + interval * i as f32)
|
||||
.collect::<Vec<f32>>();
|
||||
|
||||
let mut closest_index = 0;
|
||||
let mut min_diff = (value - values[closest_index]).abs();
|
||||
|
||||
for (i, v) in values.iter().enumerate() {
|
||||
let diff = (value - v).abs();
|
||||
if diff < min_diff {
|
||||
min_diff = diff;
|
||||
closest_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
closest_index
|
||||
}
|
||||
|
||||
impl M2Swatch {
|
||||
pub fn default_neutral() -> Self {
|
||||
let key_color = Hsl::new(0.0, 0.0, 0.0);
|
||||
let mut swatch = Vec::new();
|
||||
|
||||
let interval: f32 = 1.0 / 9.0;
|
||||
for i in 0..=9 {
|
||||
swatch.push(Hsl::new(0.0, 0.0, interval * i as f32));
|
||||
}
|
||||
|
||||
Self {
|
||||
_key_color: key_color,
|
||||
desaturated_swatch: swatch.clone(),
|
||||
swatch,
|
||||
key_index: SwatchIndex::SI900,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_color(color: &str) -> Result<Self, errors::ColorError> {
|
||||
let key_color = Hsl::from_color(
|
||||
Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>(),
|
||||
);
|
||||
let mut swatch = Vec::new();
|
||||
let mut desaturated_swatch = Vec::new();
|
||||
|
||||
let max_lightness = key_color.lightness.max(0.95);
|
||||
let min_lightness = key_color.lightness.min(0.2);
|
||||
let max_staturation = key_color.saturation.max(0.80);
|
||||
let min_staturation = key_color.saturation.min(0.45);
|
||||
|
||||
let key_place = find_key_color_place(key_color.lightness, min_lightness, max_lightness);
|
||||
|
||||
if key_place > 0 {
|
||||
let lightness_interval = (key_color.lightness - min_lightness) / key_place as f32;
|
||||
let saturation_interval = (max_staturation - key_color.saturation) / key_place as f32;
|
||||
for i in 0..key_place {
|
||||
swatch.push(Hsl::new(
|
||||
key_color.hue,
|
||||
max_staturation - saturation_interval * i as f32,
|
||||
min_lightness + lightness_interval * i as f32,
|
||||
));
|
||||
desaturated_swatch.push(Hsl::new(
|
||||
key_color.hue,
|
||||
(min_staturation + saturation_interval * i as f32) * 0.7_f32,
|
||||
min_lightness + lightness_interval * i as f32,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
swatch.push(key_color.clone());
|
||||
|
||||
if key_place < 9 {
|
||||
let lightness_interval = (max_lightness - key_color.lightness) / (9 - key_place) as f32;
|
||||
let saturation_interval =
|
||||
(key_color.saturation - min_staturation) / (9 - key_place) as f32;
|
||||
for i in 1..=9 - key_place {
|
||||
swatch.push(Hsl::new(
|
||||
key_color.hue,
|
||||
key_color.saturation - saturation_interval * i as f32,
|
||||
key_color.lightness + lightness_interval * i as f32,
|
||||
));
|
||||
desaturated_swatch.push(Hsl::new(
|
||||
key_color.hue,
|
||||
(min_staturation + saturation_interval * i as f32) * 0.7_f32,
|
||||
key_color.lightness + lightness_interval * i as f32,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
_key_color: key_color,
|
||||
swatch,
|
||||
desaturated_swatch,
|
||||
key_index: SwatchIndex::from_usize(key_place),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tone(&self, index: SwatchIndex) -> Hsl {
|
||||
self.swatch[index.to_usize()].clone()
|
||||
}
|
||||
|
||||
pub fn key_tone(&self) -> Hsl {
|
||||
self.swatch[self.key_index.to_usize()].clone()
|
||||
}
|
||||
|
||||
pub fn desaturated_tone(&self, index: SwatchIndex) -> Hsl {
|
||||
self.desaturated_swatch[index.to_usize()].clone()
|
||||
}
|
||||
|
||||
pub fn desaturated_key_tone(&self) -> Hsl {
|
||||
self.desaturated_swatch[self.key_index.to_usize()].clone()
|
||||
}
|
||||
|
||||
pub fn wacg_max_tone(&self, reference_color: &str) -> Result<Hsl, errors::ColorError> {
|
||||
let reference_color = Srgb::from_str(reference_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
|
||||
let mut max_wacg_index = 0;
|
||||
let mut max_wacg = 0.0;
|
||||
|
||||
for (i, tone) in self.swatch.iter().enumerate() {
|
||||
let swatch_color = map_hsl_to_srgb(tone);
|
||||
let wacg = swatch_color.relative_contrast(reference_color);
|
||||
if wacg > max_wacg {
|
||||
max_wacg = wacg;
|
||||
max_wacg_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.swatch[max_wacg_index].clone())
|
||||
}
|
||||
|
||||
pub fn desaturated_wacg_max_tone(
|
||||
&self,
|
||||
reference_color: &str,
|
||||
) -> Result<Hsl, errors::ColorError> {
|
||||
let reference_color = Srgb::from_str(reference_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
|
||||
let mut max_wacg_index = 0;
|
||||
let mut max_wacg = 0.0;
|
||||
|
||||
for (i, tone) in self.desaturated_swatch.iter().enumerate() {
|
||||
let swatch_color = map_hsl_to_srgb(tone);
|
||||
let wacg = swatch_color.relative_contrast(reference_color);
|
||||
if wacg > max_wacg {
|
||||
max_wacg = wacg;
|
||||
max_wacg_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.desaturated_swatch[max_wacg_index].clone())
|
||||
}
|
||||
|
||||
pub fn wacg_min_above(
|
||||
&self,
|
||||
reference_color: &str,
|
||||
ratio: f32,
|
||||
) -> Result<Hsl, errors::ColorError> {
|
||||
let reference_color = Srgb::from_str(reference_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
|
||||
let mut min_wacg_index = 0;
|
||||
let mut min_wacg = 21.0;
|
||||
|
||||
for (i, tone) in self.swatch.iter().enumerate() {
|
||||
let swatch_color = map_hsl_to_srgb(tone);
|
||||
let wacg = swatch_color.relative_contrast(reference_color);
|
||||
if wacg < min_wacg && wacg > ratio {
|
||||
min_wacg = wacg;
|
||||
min_wacg_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.swatch[min_wacg_index].clone())
|
||||
}
|
||||
|
||||
pub fn desaturated_wacg_min_above(
|
||||
&self,
|
||||
reference_color: &str,
|
||||
ratio: f32,
|
||||
) -> Result<Hsl, errors::ColorError> {
|
||||
let reference_color = Srgb::from_str(reference_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
|
||||
let mut min_wacg_index = 0;
|
||||
let mut min_wacg = 21.0;
|
||||
|
||||
for (i, tone) in self.desaturated_swatch.iter().enumerate() {
|
||||
let swatch_color = map_hsl_to_srgb(tone);
|
||||
let wacg = swatch_color.relative_contrast(reference_color);
|
||||
if wacg < min_wacg && wacg > ratio {
|
||||
min_wacg = wacg;
|
||||
min_wacg_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.desaturated_swatch[min_wacg_index].clone())
|
||||
}
|
||||
}
|
157
color-module/src/schemes/material_design_3/baseline.rs
Normal file
157
color-module/src/schemes/material_design_3/baseline.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::convert::map_lch_to_srgb_hex;
|
||||
|
||||
use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct M3BaselineColors {
|
||||
pub primary: M3ColorSet,
|
||||
pub secondary: M3ColorSet,
|
||||
pub tertiary: M3ColorSet,
|
||||
pub error: M3ColorSet,
|
||||
pub surface: M3SurfaceSet,
|
||||
pub outline: String,
|
||||
pub outline_variant: String,
|
||||
pub scrim: String,
|
||||
pub shadow: String,
|
||||
pub customs: HashMap<String, M3ColorSet>,
|
||||
dark_set: bool,
|
||||
}
|
||||
|
||||
impl M3BaselineColors {
|
||||
pub fn new(
|
||||
p: &TonalPalette,
|
||||
s: &TonalPalette,
|
||||
t: &TonalPalette,
|
||||
n: &TonalPalette,
|
||||
nv: &TonalPalette,
|
||||
e: &TonalPalette,
|
||||
dark_set: bool,
|
||||
) -> Self {
|
||||
let color_set_generator = if dark_set {
|
||||
M3ColorSet::new_dark_set
|
||||
} else {
|
||||
M3ColorSet::new_light_set
|
||||
};
|
||||
let surface_set_generator = if dark_set {
|
||||
M3SurfaceSet::new_dark_set
|
||||
} else {
|
||||
M3SurfaceSet::new_light_set
|
||||
};
|
||||
|
||||
let primary = color_set_generator(p);
|
||||
let secondary = color_set_generator(s);
|
||||
let tertiary = color_set_generator(t);
|
||||
let surface = surface_set_generator(n, nv);
|
||||
let error = color_set_generator(e);
|
||||
let outline = if dark_set { n.tone(60.0) } else { n.tone(50.0) };
|
||||
let outline_variant = if dark_set {
|
||||
nv.tone(30.0)
|
||||
} else {
|
||||
nv.tone(80.0)
|
||||
};
|
||||
let scrim = n.tone(0.0);
|
||||
let shadow = n.tone(0.0);
|
||||
|
||||
Self {
|
||||
primary,
|
||||
secondary,
|
||||
tertiary,
|
||||
error,
|
||||
surface,
|
||||
outline: map_lch_to_srgb_hex(&outline),
|
||||
outline_variant: map_lch_to_srgb_hex(&outline_variant),
|
||||
scrim: map_lch_to_srgb_hex(&scrim),
|
||||
shadow: map_lch_to_srgb_hex(&shadow),
|
||||
customs: HashMap::new(),
|
||||
dark_set,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_custom_set(&mut self, name: String, c: &TonalPalette) {
|
||||
let color_set_generator = if self.dark_set {
|
||||
M3ColorSet::new_dark_set
|
||||
} else {
|
||||
M3ColorSet::new_light_set
|
||||
};
|
||||
self.customs.insert(name, color_set_generator(c));
|
||||
}
|
||||
|
||||
pub fn to_css_variables(&self) -> Vec<String> {
|
||||
let mut css_variables = Vec::new();
|
||||
let prefix = if self.dark_set { "dark" } else { "light" };
|
||||
|
||||
css_variables.extend(self.primary.to_css_variables(prefix, "primary"));
|
||||
css_variables.extend(self.secondary.to_css_variables(prefix, "secondary"));
|
||||
css_variables.extend(self.tertiary.to_css_variables(prefix, "tertiary"));
|
||||
css_variables.extend(self.error.to_css_variables(prefix, "error"));
|
||||
css_variables.extend(self.surface.to_css_variables(prefix));
|
||||
css_variables.push(format!("--color-{}-outline: #{};", prefix, self.outline));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-outline-variant: #{};",
|
||||
prefix, self.outline_variant
|
||||
));
|
||||
css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim));
|
||||
css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadow));
|
||||
for (name, color_set) in &self.customs {
|
||||
css_variables.extend(color_set.to_css_variables(prefix, name));
|
||||
}
|
||||
|
||||
css_variables
|
||||
}
|
||||
|
||||
pub fn to_scss_variables(&self) -> Vec<String> {
|
||||
let mut scss_variables = Vec::new();
|
||||
let prefix = if self.dark_set { "dark" } else { "light" };
|
||||
|
||||
scss_variables.extend(self.primary.to_scss_variables(prefix, "primary"));
|
||||
scss_variables.extend(self.secondary.to_scss_variables(prefix, "secondary"));
|
||||
scss_variables.extend(self.tertiary.to_scss_variables(prefix, "tertiary"));
|
||||
scss_variables.extend(self.error.to_scss_variables(prefix, "error"));
|
||||
scss_variables.extend(self.surface.to_scss_variables(prefix));
|
||||
scss_variables.push(format!("$color-{}-outline: #{};", prefix, self.outline));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-outline-variant: #{};",
|
||||
prefix, self.outline_variant
|
||||
));
|
||||
scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim));
|
||||
scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadow));
|
||||
for (name, color_set) in &self.customs {
|
||||
scss_variables.extend(color_set.to_scss_variables(prefix, name));
|
||||
}
|
||||
|
||||
scss_variables
|
||||
}
|
||||
|
||||
pub fn to_javascript_object_fields(&self) -> Vec<String> {
|
||||
let mut js_object_fields = Vec::new();
|
||||
let prefix = if self.dark_set { "dark" } else { "light" };
|
||||
|
||||
js_object_fields.extend(self.primary.to_javascript_object_fields(prefix, "primary"));
|
||||
js_object_fields.extend(
|
||||
self.secondary
|
||||
.to_javascript_object_fields(prefix, "secondary"),
|
||||
);
|
||||
js_object_fields.extend(
|
||||
self.tertiary
|
||||
.to_javascript_object_fields(prefix, "tertiary"),
|
||||
);
|
||||
js_object_fields.extend(self.error.to_javascript_object_fields(prefix, "error"));
|
||||
js_object_fields.extend(self.surface.to_javascript_object_fields(prefix));
|
||||
js_object_fields.push(format!("{}Outline: '#{}',", prefix, self.outline));
|
||||
js_object_fields.push(format!(
|
||||
"{}OutlineVariant: '#{}',",
|
||||
prefix, self.outline_variant
|
||||
));
|
||||
js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim));
|
||||
js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadow));
|
||||
for (name, color_set) in &self.customs {
|
||||
js_object_fields.extend(color_set.to_javascript_object_fields(prefix, name));
|
||||
}
|
||||
|
||||
js_object_fields
|
||||
}
|
||||
}
|
181
color-module/src/schemes/material_design_3/color_set.rs
Normal file
181
color-module/src/schemes/material_design_3/color_set.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::convert::map_lch_to_srgb_hex;
|
||||
|
||||
use super::tonal_palette::TonalPalette;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct M3ColorSet {
|
||||
pub root: String,
|
||||
pub on_root: String,
|
||||
pub container: String,
|
||||
pub on_container: String,
|
||||
pub fixed: String,
|
||||
pub fixed_dim: String,
|
||||
pub on_fixed: String,
|
||||
pub fixed_variant: String,
|
||||
pub inverse: String,
|
||||
}
|
||||
|
||||
impl M3ColorSet {
|
||||
pub fn new_light_set(palette: &TonalPalette) -> Self {
|
||||
let root = palette.tone(40.0);
|
||||
let on_root = palette.tone(100.0);
|
||||
let container = palette.tone(90.0);
|
||||
let on_container = palette.tone(30.0);
|
||||
let fixed = palette.tone(90.0);
|
||||
let fixed_dim = palette.tone(80.0);
|
||||
let on_fixed = palette.tone(10.0);
|
||||
let fixed_variant = palette.tone(30.0);
|
||||
let inverse = palette.tone(80.0);
|
||||
|
||||
Self {
|
||||
root: map_lch_to_srgb_hex(&root),
|
||||
on_root: map_lch_to_srgb_hex(&on_root),
|
||||
container: map_lch_to_srgb_hex(&container),
|
||||
on_container: map_lch_to_srgb_hex(&on_container),
|
||||
fixed: map_lch_to_srgb_hex(&fixed),
|
||||
fixed_dim: map_lch_to_srgb_hex(&fixed_dim),
|
||||
on_fixed: map_lch_to_srgb_hex(&on_fixed),
|
||||
fixed_variant: map_lch_to_srgb_hex(&fixed_variant),
|
||||
inverse: map_lch_to_srgb_hex(&inverse),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_dark_set(palette: &TonalPalette) -> Self {
|
||||
let root = palette.tone(80.0);
|
||||
let on_root = palette.tone(20.0);
|
||||
let container = palette.tone(30.0);
|
||||
let on_container = palette.tone(90.0);
|
||||
let fixed = palette.tone(90.0);
|
||||
let fixed_dim = palette.tone(80.0);
|
||||
let on_fixed = palette.tone(10.0);
|
||||
let fixed_variant = palette.tone(30.0);
|
||||
let inverse = palette.tone(40.0);
|
||||
|
||||
Self {
|
||||
root: map_lch_to_srgb_hex(&root),
|
||||
on_root: map_lch_to_srgb_hex(&on_root),
|
||||
container: map_lch_to_srgb_hex(&container),
|
||||
on_container: map_lch_to_srgb_hex(&on_container),
|
||||
fixed: map_lch_to_srgb_hex(&fixed),
|
||||
fixed_dim: map_lch_to_srgb_hex(&fixed_dim),
|
||||
on_fixed: map_lch_to_srgb_hex(&on_fixed),
|
||||
fixed_variant: map_lch_to_srgb_hex(&fixed_variant),
|
||||
inverse: map_lch_to_srgb_hex(&inverse),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
|
||||
variable_lines.push(format!("--color-{}-{}: #{};", prefix, name, self.root));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-on-{}: #{};",
|
||||
prefix, name, self.on_root
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-{}-container: #{};",
|
||||
prefix, name, self.container
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-on-{}-container: #{};",
|
||||
prefix, name, self.on_container
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-{}-fixed: #{};",
|
||||
prefix, name, self.fixed
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-{}-fixed-dim: #{};",
|
||||
prefix, name, self.fixed_dim
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-on-{}-fixed: #{};",
|
||||
prefix, name, self.on_fixed
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-on-{}-fixed-variant: #{};",
|
||||
prefix, name, self.fixed_variant
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"--color-{}-inverse-{}: #{};",
|
||||
prefix, name, self.inverse
|
||||
));
|
||||
|
||||
variable_lines
|
||||
}
|
||||
|
||||
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
|
||||
variable_lines.push(format!("$color-{}-{}: #{};", prefix, name, self.root));
|
||||
variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on_root));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-{}-container: #{};",
|
||||
prefix, name, self.container
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-on-{}-container: #{};",
|
||||
prefix, name, self.on_container
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-{}-fixed: #{};",
|
||||
prefix, name, self.fixed
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-{}-fixed-dim: #{};",
|
||||
prefix, name, self.fixed_dim
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-on-{}-fixed: #{};",
|
||||
prefix, name, self.on_fixed
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-on-{}-fixed-variant: #{};",
|
||||
prefix, name, self.fixed_variant
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"$color-{}-inverse-{}: #{};",
|
||||
prefix, name, self.inverse
|
||||
));
|
||||
|
||||
variable_lines
|
||||
}
|
||||
|
||||
pub fn to_javascript_object_fields(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variable_lines = Vec::new();
|
||||
let prefix = prefix.to_ascii_lowercase();
|
||||
let name = name
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_ascii_uppercase()
|
||||
.to_string()
|
||||
+ &name[1..];
|
||||
|
||||
variable_lines.push(format!("{}{}: '#{}',", prefix, name, self.root));
|
||||
variable_lines.push(format!("{}on{}: '#{}',", prefix, name, self.on_root));
|
||||
variable_lines.push(format!(
|
||||
"{}{}Container: '#{}',",
|
||||
prefix, name, self.container
|
||||
));
|
||||
variable_lines.push(format!(
|
||||
"{}On{}Container: '#{}',",
|
||||
prefix, name, self.on_container
|
||||
));
|
||||
variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed));
|
||||
variable_lines.push(format!(
|
||||
"{}{}FixedDim: '#{}',",
|
||||
prefix, name, self.fixed_dim
|
||||
));
|
||||
variable_lines.push(format!("{}On{}Fixed: '#{}',", prefix, name, self.on_fixed));
|
||||
variable_lines.push(format!(
|
||||
"{}On{}FixedVariant: '#{}',",
|
||||
prefix, name, self.fixed_variant
|
||||
));
|
||||
variable_lines.push(format!("{}Inverse{}: '#{}',", prefix, name, self.inverse));
|
||||
|
||||
variable_lines
|
||||
}
|
||||
}
|
120
color-module/src/schemes/material_design_3/mod.rs
Normal file
120
color-module/src/schemes/material_design_3/mod.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use baseline::M3BaselineColors;
|
||||
use palette::{IntoColor, Lch, Srgb};
|
||||
use serde::Serialize;
|
||||
use tonal_palette::TonalPalette;
|
||||
|
||||
use crate::convert::map_lch_to_srgb_hex;
|
||||
use crate::errors;
|
||||
|
||||
use super::SchemeExport;
|
||||
|
||||
mod baseline;
|
||||
mod color_set;
|
||||
mod surface;
|
||||
mod tonal_palette;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MaterialDesign3Scheme {
|
||||
pub white: String,
|
||||
pub black: String,
|
||||
pub light_baseline: M3BaselineColors,
|
||||
pub dark_baseline: M3BaselineColors,
|
||||
}
|
||||
|
||||
impl MaterialDesign3Scheme {
|
||||
pub fn new(source_color: &str, error_color: &str) -> Result<Self, errors::ColorError> {
|
||||
let source: Lch = Srgb::from_str(source_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
|
||||
.into_format::<f32>()
|
||||
.into_color();
|
||||
let error: Lch = Srgb::from_str(error_color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))?
|
||||
.into_format::<f32>()
|
||||
.into_color();
|
||||
let source_hue = source.hue.into_positive_degrees();
|
||||
let p = TonalPalette::from_hue_and_chroma(source_hue, source.chroma);
|
||||
let s = TonalPalette::from_hue_and_chroma(source_hue, source.chroma / 3.0);
|
||||
let t = TonalPalette::from_hue_and_chroma(source_hue + 60.0, source.chroma / 2.0);
|
||||
let n = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 12.0).min(4.0));
|
||||
let nv = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 6.0).min(8.0));
|
||||
let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0);
|
||||
|
||||
Ok(Self {
|
||||
white: map_lch_to_srgb_hex(&Lch::new(100.0, 0.0, 0.0)),
|
||||
black: map_lch_to_srgb_hex(&Lch::new(0.0, 0.0, 0.0)),
|
||||
light_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, false),
|
||||
dark_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, true),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_custom_color(
|
||||
&mut self,
|
||||
name: String,
|
||||
color: String,
|
||||
) -> Result<(), errors::ColorError> {
|
||||
let custom_color: Lch = Srgb::from_str(&color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))?
|
||||
.into_format::<f32>()
|
||||
.into_color();
|
||||
let hue = custom_color.hue.into_positive_degrees();
|
||||
let palette = TonalPalette::from_hue_and_chroma(hue, custom_color.chroma);
|
||||
self.light_baseline.add_custom_set(name.clone(), &palette);
|
||||
self.dark_baseline.add_custom_set(name.clone(), &palette);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeExport for MaterialDesign3Scheme {
|
||||
fn output_css_variables(&self) -> String {
|
||||
let mut css_variables = Vec::new();
|
||||
|
||||
css_variables.push(format!("--color-white: #{};", self.white));
|
||||
css_variables.push(format!("--color-black: #{};", self.black));
|
||||
css_variables.extend(self.light_baseline.to_css_variables());
|
||||
css_variables.extend(self.dark_baseline.to_css_variables());
|
||||
|
||||
css_variables.join("\n")
|
||||
}
|
||||
|
||||
fn output_scss_variables(&self) -> String {
|
||||
let mut scss_variables = Vec::new();
|
||||
|
||||
scss_variables.push(format!("$color-white: #{};", self.white));
|
||||
scss_variables.push(format!("$color-black: #{};", self.black));
|
||||
scss_variables.extend(self.light_baseline.to_scss_variables());
|
||||
scss_variables.extend(self.dark_baseline.to_scss_variables());
|
||||
|
||||
scss_variables.join("\n")
|
||||
}
|
||||
|
||||
fn output_javascript_object(&self) -> String {
|
||||
let mut js_object = Vec::new();
|
||||
|
||||
js_object.push("const colorScheme = {".to_string());
|
||||
js_object.push(format!(" white: '#{}',", self.white));
|
||||
js_object.push(format!(" black: '#{}',", self.black));
|
||||
js_object.push(" light: {".to_string());
|
||||
js_object.extend(
|
||||
self.light_baseline
|
||||
.to_javascript_object_fields()
|
||||
.into_iter()
|
||||
.map(|s| format!(" {}", s))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
js_object.push(" },".to_string());
|
||||
js_object.push(" dark: {".to_string());
|
||||
js_object.extend(
|
||||
self.dark_baseline
|
||||
.to_javascript_object_fields()
|
||||
.into_iter()
|
||||
.map(|s| format!(" {}", s))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
js_object.push(" },".to_string());
|
||||
js_object.push("}".to_string());
|
||||
|
||||
js_object.join("\n")
|
||||
}
|
||||
}
|
215
color-module/src/schemes/material_design_3/surface.rs
Normal file
215
color-module/src/schemes/material_design_3/surface.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::convert::map_lch_to_srgb_hex;
|
||||
|
||||
use super::tonal_palette::TonalPalette;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct M3SurfaceSet {
|
||||
pub root: String,
|
||||
pub dim: String,
|
||||
pub bright: String,
|
||||
pub container: String,
|
||||
pub container_lowest: String,
|
||||
pub container_low: String,
|
||||
pub container_high: String,
|
||||
pub container_highest: String,
|
||||
pub on_root: String,
|
||||
pub on_root_variant: String,
|
||||
pub inverse: String,
|
||||
pub on_inverse: String,
|
||||
}
|
||||
|
||||
impl M3SurfaceSet {
|
||||
pub fn new_light_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self {
|
||||
let root = neutral.tone(98.0);
|
||||
let dim = neutral.tone(87.0);
|
||||
let bright = neutral.tone(98.0);
|
||||
let container = neutral.tone(94.0);
|
||||
let container_lowest = neutral.tone(100.0);
|
||||
let container_low = neutral.tone(96.0);
|
||||
let container_high = neutral.tone(92.0);
|
||||
let container_highest = neutral.tone(90.0);
|
||||
let on_root = neutral_variant.tone(10.0);
|
||||
let on_root_variant = neutral_variant.tone(30.0);
|
||||
let inverse = neutral.tone(20.0);
|
||||
let on_inverse = neutral_variant.tone(95.0);
|
||||
|
||||
Self {
|
||||
root: map_lch_to_srgb_hex(&root),
|
||||
dim: map_lch_to_srgb_hex(&dim),
|
||||
bright: map_lch_to_srgb_hex(&bright),
|
||||
container: map_lch_to_srgb_hex(&container),
|
||||
container_lowest: map_lch_to_srgb_hex(&container_lowest),
|
||||
container_low: map_lch_to_srgb_hex(&container_low),
|
||||
container_high: map_lch_to_srgb_hex(&container_high),
|
||||
container_highest: map_lch_to_srgb_hex(&container_highest),
|
||||
on_root: map_lch_to_srgb_hex(&on_root),
|
||||
on_root_variant: map_lch_to_srgb_hex(&on_root_variant),
|
||||
inverse: map_lch_to_srgb_hex(&inverse),
|
||||
on_inverse: map_lch_to_srgb_hex(&on_inverse),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_dark_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self {
|
||||
let root = neutral.tone(6.0);
|
||||
let dim = neutral.tone(6.0);
|
||||
let bright = neutral.tone(24.0);
|
||||
let container = neutral.tone(12.0);
|
||||
let container_lowest = neutral.tone(4.0);
|
||||
let container_low = neutral.tone(10.0);
|
||||
let container_high = neutral.tone(17.0);
|
||||
let container_highest = neutral.tone(22.0);
|
||||
let on_root = neutral_variant.tone(90.0);
|
||||
let on_root_variant = neutral_variant.tone(80.0);
|
||||
let inverse = neutral.tone(90.0);
|
||||
let on_inverse = neutral_variant.tone(20.0);
|
||||
|
||||
Self {
|
||||
root: map_lch_to_srgb_hex(&root),
|
||||
dim: map_lch_to_srgb_hex(&dim),
|
||||
bright: map_lch_to_srgb_hex(&bright),
|
||||
container: map_lch_to_srgb_hex(&container),
|
||||
container_lowest: map_lch_to_srgb_hex(&container_lowest),
|
||||
container_low: map_lch_to_srgb_hex(&container_low),
|
||||
container_high: map_lch_to_srgb_hex(&container_high),
|
||||
container_highest: map_lch_to_srgb_hex(&container_highest),
|
||||
on_root: map_lch_to_srgb_hex(&on_root),
|
||||
on_root_variant: map_lch_to_srgb_hex(&on_root_variant),
|
||||
inverse: map_lch_to_srgb_hex(&inverse),
|
||||
on_inverse: map_lch_to_srgb_hex(&on_inverse),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_css_variables(&self, prefix: &str) -> Vec<String> {
|
||||
let mut css_variables = Vec::new();
|
||||
|
||||
css_variables.push(format!("--color-{}-surface: ${};", prefix, self.root));
|
||||
css_variables.push(format!("--color-{}-surface-dim: ${};", prefix, self.dim));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-surface-bright: ${};",
|
||||
prefix, self.bright
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-surface-container: ${};",
|
||||
prefix, self.container
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-surface-container-lowest: ${};",
|
||||
prefix, self.container_lowest
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-surface-container-low: ${};",
|
||||
prefix, self.container_low
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-surface-container-high: ${};",
|
||||
prefix, self.container_high
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-surface-container-highest: ${};",
|
||||
prefix, self.container_highest
|
||||
));
|
||||
css_variables.push(format!("--color-{}-on-surface: ${};", prefix, self.on_root));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-on-surface-variant: ${};",
|
||||
prefix, self.on_root_variant
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-inverse-surface: ${};",
|
||||
prefix, self.inverse
|
||||
));
|
||||
css_variables.push(format!(
|
||||
"--color-{}-inverse-on-surface: ${};",
|
||||
prefix, self.on_inverse
|
||||
));
|
||||
|
||||
css_variables
|
||||
}
|
||||
|
||||
pub fn to_scss_variables(&self, prefix: &str) -> Vec<String> {
|
||||
let mut scss_variables = Vec::new();
|
||||
|
||||
scss_variables.push(format!("$color-{}-surface: ${};", prefix, self.root));
|
||||
scss_variables.push(format!("$color-{}-surface-dim: ${};", prefix, self.dim));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-surface-bright: ${};",
|
||||
prefix, self.bright
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-surface-container: ${};",
|
||||
prefix, self.container
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-surface-container-lowest: ${};",
|
||||
prefix, self.container_lowest
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-surface-container-low: ${};",
|
||||
prefix, self.container_low
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-surface-container-high: ${};",
|
||||
prefix, self.container_high
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-surface-container-highest: ${};",
|
||||
prefix, self.container_highest
|
||||
));
|
||||
scss_variables.push(format!("$color-{}-on-surface: ${};", prefix, self.on_root));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-on-surface-variant: ${};",
|
||||
prefix, self.on_root_variant
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-inverse-surface: ${};",
|
||||
prefix, self.inverse
|
||||
));
|
||||
scss_variables.push(format!(
|
||||
"$color-{}-inverse-on-surface: ${};",
|
||||
prefix, self.on_inverse
|
||||
));
|
||||
|
||||
scss_variables
|
||||
}
|
||||
|
||||
pub fn to_javascript_object_fields(&self, prefix: &str) -> Vec<String> {
|
||||
let mut js_object_fields = Vec::new();
|
||||
|
||||
js_object_fields.push(format!("{}Surface: '#{}',", prefix, self.root));
|
||||
js_object_fields.push(format!("{}SurfaceDim: '#{}',", prefix, self.dim));
|
||||
js_object_fields.push(format!("{}SurfaceBright: '#{}',", prefix, self.bright));
|
||||
js_object_fields.push(format!(
|
||||
"{}SurfaceContainer: '#{}',",
|
||||
prefix, self.container
|
||||
));
|
||||
js_object_fields.push(format!(
|
||||
"{}SurfaceContainerLowest: '#{}',",
|
||||
prefix, self.container_lowest
|
||||
));
|
||||
js_object_fields.push(format!(
|
||||
"{}SurfaceContainerLow: '#{}',",
|
||||
prefix, self.container_low
|
||||
));
|
||||
js_object_fields.push(format!(
|
||||
"{}SurfaceContainerHigh: '#{}',",
|
||||
prefix, self.container_high
|
||||
));
|
||||
js_object_fields.push(format!(
|
||||
"{}SurfaceContainerHighest: '#{}',",
|
||||
prefix, self.container_highest
|
||||
));
|
||||
js_object_fields.push(format!("{}OnSurface: '#{}',", prefix, self.on_root));
|
||||
js_object_fields.push(format!(
|
||||
"{}OnSurfaceVariant: '#{}',",
|
||||
prefix, self.on_root_variant
|
||||
));
|
||||
js_object_fields.push(format!("{}InverseSurface: '#{}',", prefix, self.inverse));
|
||||
js_object_fields.push(format!(
|
||||
"{}InverseOnSurface: '#{}',",
|
||||
prefix, self.on_inverse
|
||||
));
|
||||
|
||||
js_object_fields
|
||||
}
|
||||
}
|
89
color-module/src/schemes/material_design_3/tonal_palette.rs
Normal file
89
color-module/src/schemes/material_design_3/tonal_palette.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use palette::{cam16::Cam16Jch, IntoColor, Lch, Srgb};
|
||||
|
||||
use crate::errors;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TonalPalette {
|
||||
pub key_color: Lch,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn approximately_equal(a: f32, b: f32) -> bool {
|
||||
const EPSILON: f32 = 0.000001;
|
||||
(a - b).abs() < EPSILON
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn find_max_chroma(cache: &mut Vec<(f32, f32)>, hue: f32, tone: f32) -> f32 {
|
||||
for (k, v) in cache.iter() {
|
||||
if approximately_equal(*k, tone) {
|
||||
return *v;
|
||||
}
|
||||
}
|
||||
let chroma = Cam16Jch::new(tone, 200.0, hue).chroma;
|
||||
cache.push((tone, chroma));
|
||||
chroma
|
||||
}
|
||||
|
||||
fn from_hue_and_chroma(hue: f32, chroma: f32) -> Lch {
|
||||
let mut max_chroma_cache = Vec::new();
|
||||
let hue = if hue >= 360.0 { hue - 360.0 } else { hue };
|
||||
const PIVOT_TONE: f32 = 50.0;
|
||||
const TONE_STEP_SIZE: f32 = 1.0;
|
||||
const EPSILON: f32 = 0.01;
|
||||
|
||||
let mut lower_tone = 0.0_f32;
|
||||
let mut upper_tone = 100.0_f32;
|
||||
while lower_tone < upper_tone {
|
||||
let mid_tone = ((lower_tone + upper_tone) / 2.0).floor();
|
||||
let is_ascending = find_max_chroma(&mut max_chroma_cache, hue, mid_tone)
|
||||
< find_max_chroma(&mut max_chroma_cache, hue, mid_tone + TONE_STEP_SIZE);
|
||||
let sufficient_chroma =
|
||||
find_max_chroma(&mut max_chroma_cache, hue, mid_tone) >= chroma - EPSILON;
|
||||
|
||||
if sufficient_chroma {
|
||||
if (lower_tone - PIVOT_TONE).abs() < (upper_tone - PIVOT_TONE).abs() {
|
||||
upper_tone = mid_tone;
|
||||
} else {
|
||||
if approximately_equal(lower_tone, mid_tone) {
|
||||
return Lch::new(lower_tone, chroma, hue);
|
||||
}
|
||||
lower_tone = mid_tone;
|
||||
}
|
||||
} else {
|
||||
if is_ascending {
|
||||
lower_tone = mid_tone + TONE_STEP_SIZE;
|
||||
} else {
|
||||
upper_tone = mid_tone;
|
||||
}
|
||||
}
|
||||
}
|
||||
Lch::new(lower_tone, chroma, hue)
|
||||
}
|
||||
|
||||
impl TryFrom<String> for TonalPalette {
|
||||
type Error = errors::ColorError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
let key_color: Lch = Srgb::from_str(&value)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(value))?
|
||||
.into_format::<f32>()
|
||||
.into_color();
|
||||
Ok(TonalPalette { key_color })
|
||||
}
|
||||
}
|
||||
|
||||
impl TonalPalette {
|
||||
pub fn from_hue_and_chroma(hue: f32, chroma: f32) -> Self {
|
||||
let key_color = from_hue_and_chroma(hue, chroma);
|
||||
TonalPalette { key_color }
|
||||
}
|
||||
|
||||
pub fn tone(&self, tone: f32) -> Lch {
|
||||
let toned_color = Lch::new(tone, self.key_color.chroma, self.key_color.hue);
|
||||
|
||||
toned_color
|
||||
}
|
||||
}
|
144
color-module/src/schemes/mod.rs
Normal file
144
color-module/src/schemes/mod.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use material_design_2::MaterialDesign2Scheme;
|
||||
use material_design_3::MaterialDesign3Scheme;
|
||||
use q_style::{QScheme, SchemeSetting};
|
||||
use swatch_style::{SwatchEntry, SwatchSchemeSetting};
|
||||
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 mod swatch_style;
|
||||
|
||||
pub trait SchemeExport {
|
||||
fn output_css_variables(&self) -> String;
|
||||
fn output_scss_variables(&self) -> String;
|
||||
fn output_javascript_object(&self) -> String;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_material_design_3_scheme(
|
||||
source_color: &str,
|
||||
error_color: &str,
|
||||
custom_colors: JsValue,
|
||||
) -> Result<JsValue, errors::ColorError> {
|
||||
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
|
||||
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
|
||||
let mut scheme = MaterialDesign3Scheme::new(source_color, error_color)?;
|
||||
for (name, color) in custom_colors {
|
||||
scheme.add_custom_color(name, color)?;
|
||||
}
|
||||
Ok(serde_wasm_bindgen::to_value(&(
|
||||
scheme.clone(),
|
||||
scheme.output_css_variables(),
|
||||
scheme.output_scss_variables(),
|
||||
scheme.output_javascript_object(),
|
||||
))
|
||||
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_material_design_2_scheme(
|
||||
primary_color: &str,
|
||||
secondary_color: &str,
|
||||
error_color: &str,
|
||||
custom_colors: JsValue,
|
||||
) -> Result<JsValue, errors::ColorError> {
|
||||
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
|
||||
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
|
||||
let mut scheme = MaterialDesign2Scheme::new(primary_color, secondary_color, error_color)?;
|
||||
for (name, color) in custom_colors {
|
||||
scheme.add_custom_color(&name, &color)?;
|
||||
}
|
||||
Ok(serde_wasm_bindgen::to_value(&(
|
||||
scheme.clone(),
|
||||
scheme.output_css_variables(),
|
||||
scheme.output_scss_variables(),
|
||||
scheme.output_javascript_object(),
|
||||
))
|
||||
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
|
||||
}
|
||||
|
||||
#[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: SchemeSetting,
|
||||
) -> Result<JsValue, errors::ColorError> {
|
||||
let scheme = QScheme::new(
|
||||
primary_color,
|
||||
danger_color,
|
||||
success_color,
|
||||
warning_color,
|
||||
info_color,
|
||||
fg_color,
|
||||
bg_color,
|
||||
setting,
|
||||
)?;
|
||||
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: SchemeSetting,
|
||||
) -> Result<JsValue, errors::ColorError> {
|
||||
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,
|
||||
setting,
|
||||
)?;
|
||||
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_swatch_scheme(
|
||||
colors: Vec<SwatchEntry>,
|
||||
setting: SwatchSchemeSetting,
|
||||
) -> Result<JsValue, errors::ColorError> {
|
||||
let scheme = swatch_style::SwatchScheme::new(colors, setting)?;
|
||||
Ok(serde_wasm_bindgen::to_value(&(
|
||||
scheme.swatches(),
|
||||
scheme.output_css_variables(),
|
||||
scheme.output_scss_variables(),
|
||||
scheme.output_javascript_object(),
|
||||
))
|
||||
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
|
||||
}
|
244
color-module/src/schemes/q_style/baseline.rs
Normal file
244
color-module/src/schemes/q_style/baseline.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
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, foreground.l, &setting),
|
||||
secondary: secondary
|
||||
.map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
|
||||
tertiary: tertiary
|
||||
.map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
|
||||
accent: accent
|
||||
.map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
|
||||
neutral: ColorSet::new(&neutral_color, &neutral_swatch, foreground.l, &setting),
|
||||
danger: ColorSet::new(danger, &neutral_swatch, foreground.l, &setting),
|
||||
success: ColorSet::new(success, &neutral_swatch, foreground.l, &setting),
|
||||
warning: ColorSet::new(warning, &neutral_swatch, foreground.l, &setting),
|
||||
info: ColorSet::new(info, &neutral_swatch, foreground.l, &setting),
|
||||
outline: ColorSet::new(&outline_color, &neutral_swatch, foreground.l, &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()
|
||||
}
|
||||
}
|
343
color-module/src/schemes/q_style/color_set.rs
Normal file
343
color-module/src/schemes/q_style/color_set.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
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 match_wacg = |original: &Oklch<f32>, reference: &Luma| {
|
||||
let luma = map_oklch_to_luma(original);
|
||||
luma.relative_contrast(*reference)
|
||||
};
|
||||
|
||||
let mut fit_contrast = (f32::INFINITY, f32::NEG_INFINITY);
|
||||
let mut closest_contrast = (f32::INFINITY, f32::NEG_INFINITY);
|
||||
for scan_lightness in (0..=100).map(|x| x as f32 / 100.0) {
|
||||
let new_target = neutral_swatch.get(scan_lightness);
|
||||
let contrast_ratio = match_wacg(&new_target, &reference_luma);
|
||||
if (contrast_ratio - ratio).abs() < (closest_contrast.0 - ratio).abs()
|
||||
&& scan_lightness > closest_contrast.1
|
||||
{
|
||||
closest_contrast = (contrast_ratio, scan_lightness);
|
||||
}
|
||||
if contrast_ratio >= ratio
|
||||
&& (contrast_ratio - ratio).abs() < (closest_contrast.0 - ratio).abs()
|
||||
{
|
||||
fit_contrast = (contrast_ratio, scan_lightness);
|
||||
}
|
||||
}
|
||||
|
||||
neutral_swatch.get(if fit_contrast.0 == f32::INFINITY {
|
||||
closest_contrast.1
|
||||
} else {
|
||||
fit_contrast.1
|
||||
})
|
||||
}
|
||||
|
||||
impl ColorSet {
|
||||
pub fn new(
|
||||
color: &Oklch,
|
||||
neutral_swatch: &NeutralSwatch,
|
||||
foreground_lightness: f32,
|
||||
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(foreground_lightness),
|
||||
neutral_swatch.get(foreground_lightness),
|
||||
neutral_swatch.get(foreground_lightness),
|
||||
neutral_swatch.get(foreground_lightness),
|
||||
neutral_swatch.get(foreground_lightness),
|
||||
),
|
||||
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),
|
||||
),
|
||||
WACGSetting::HighContrast => (
|
||||
fit_to_wacg(&root, neutral_swatch, 21.0),
|
||||
fit_to_wacg(&hover, neutral_swatch, 21.0),
|
||||
fit_to_wacg(&active, neutral_swatch, 21.0),
|
||||
fit_to_wacg(&focus, neutral_swatch, 21.0),
|
||||
fit_to_wacg(&disabled, neutral_swatch, 21.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()
|
||||
}
|
||||
}
|
200
color-module/src/schemes/q_style/mod.rs
Normal file
200
color-module/src/schemes/q_style/mod.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use baseline::Baseline;
|
||||
use palette::FromColor;
|
||||
use scheme_setting::{ColorExpand, WACGSetting};
|
||||
use serde::Serialize;
|
||||
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::{ColorShifting, 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),
|
||||
&(&background * (setting.dark_convert / 2.0)),
|
||||
&(&foreground * 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 / 2.0)),
|
||||
&(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 = enum_iterator::all::<ColorExpand>()
|
||||
.map(|variant| {
|
||||
serde_json::json!({
|
||||
"label": variant.label(),
|
||||
"value": variant as u8,
|
||||
})
|
||||
})
|
||||
.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 = enum_iterator::all::<WACGSetting>()
|
||||
.map(|setting| {
|
||||
serde_json::json!({
|
||||
"label": setting.label(),
|
||||
"value": setting as u8,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn q_scheme_default_settings() -> SchemeSetting {
|
||||
SchemeSetting::default()
|
||||
}
|
58
color-module/src/schemes/q_style/neutral_swatch.rs
Normal file
58
color-module/src/schemes/q_style/neutral_swatch.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
212
color-module/src/schemes/q_style/scheme_setting.rs
Normal file
212
color-module/src/schemes/q_style/scheme_setting.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use std::ops::{Div, Mul};
|
||||
|
||||
use enum_iterator::Sequence;
|
||||
use palette::Oklch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use strum::Display;
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct ColorShifting {
|
||||
pub chroma: f32,
|
||||
pub lightness: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ColorShifting {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(chroma: f32, lightness: f32) -> Self {
|
||||
ColorShifting { chroma, lightness }
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJsValue)]
|
||||
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
|
||||
Ok(serde_wasm_bindgen::to_value(self)?)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
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
|
||||
},
|
||||
self.chroma
|
||||
+ if rhs.chroma > 0.0 {
|
||||
(100.0 - self.chroma) * rhs.chroma
|
||||
} else {
|
||||
self.chroma * rhs.chroma
|
||||
},
|
||||
self.hue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<f32> for ColorShifting {
|
||||
type Output = ColorShifting;
|
||||
|
||||
fn div(self, rhs: f32) -> Self::Output {
|
||||
ColorShifting {
|
||||
chroma: self.chroma / rhs,
|
||||
lightness: self.lightness / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<f32> for &ColorShifting {
|
||||
type Output = ColorShifting;
|
||||
|
||||
fn div(self, rhs: f32) -> Self::Output {
|
||||
ColorShifting {
|
||||
chroma: self.chroma / rhs,
|
||||
lightness: self.lightness / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
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, Sequence, Serialize_repr, Deserialize_repr)]
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
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, Sequence, Serialize_repr, Deserialize_repr)]
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
pub enum WACGSetting {
|
||||
Fixed,
|
||||
AutomaticAA,
|
||||
AutomaticAAA,
|
||||
HighContrast,
|
||||
}
|
||||
|
||||
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(),
|
||||
WACGSetting::HighContrast => "High Contrast".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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SchemeSetting {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
hover: ColorShifting,
|
||||
active: ColorShifting,
|
||||
focus: ColorShifting,
|
||||
disabled: ColorShifting,
|
||||
dark_convert: ColorShifting,
|
||||
expand_method: ColorExpand,
|
||||
wacg_follows: WACGSetting,
|
||||
) -> Self {
|
||||
SchemeSetting {
|
||||
hover,
|
||||
active,
|
||||
focus,
|
||||
disabled,
|
||||
dark_convert,
|
||||
expand_method,
|
||||
wacg_follows,
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJsValue)]
|
||||
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
|
||||
Ok(serde_wasm_bindgen::to_value(self)?)
|
||||
}
|
||||
}
|
144
color-module/src/schemes/swatch_style/mod.rs
Normal file
144
color-module/src/schemes/swatch_style/mod.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use palette::FromColor;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use setting::SwatchSchemeSetting;
|
||||
use swatch::Swatch;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{errors, parse_to_oklch};
|
||||
|
||||
use super::SchemeExport;
|
||||
|
||||
mod setting;
|
||||
mod swatch;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SwatchScheme {
|
||||
light: HashMap<String, Swatch>,
|
||||
dark: HashMap<String, Swatch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct SwatchEntry {
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SwatchEntry {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(name: &str, color: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
color: color.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJsValue)]
|
||||
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
|
||||
Ok(serde_wasm_bindgen::to_value(self)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl SwatchScheme {
|
||||
pub fn new(
|
||||
colors: Vec<SwatchEntry>,
|
||||
setting: SwatchSchemeSetting,
|
||||
) -> Result<Self, errors::ColorError> {
|
||||
let mut light = HashMap::new();
|
||||
let mut dark = HashMap::new();
|
||||
|
||||
for entry in colors {
|
||||
let color = parse_to_oklch!(&entry.color);
|
||||
let darken_color = color * setting.dark_convert;
|
||||
light.insert(entry.name.clone(), Swatch::new(&color, &setting));
|
||||
dark.insert(entry.name, Swatch::new(&darken_color, &setting));
|
||||
}
|
||||
|
||||
Ok(Self { light, dark })
|
||||
}
|
||||
|
||||
pub fn swatches(&self) -> HashMap<String, HashMap<String, Vec<String>>> {
|
||||
let mut light_swatches = HashMap::new();
|
||||
let mut dark_swatches = HashMap::new();
|
||||
|
||||
for (name, swatch) in &self.light {
|
||||
light_swatches.insert(name.clone(), swatch.swtch_hex());
|
||||
}
|
||||
for (name, swatch) in &self.dark {
|
||||
dark_swatches.insert(name.clone(), swatch.swtch_hex());
|
||||
}
|
||||
|
||||
HashMap::from([
|
||||
("light".to_string(), light_swatches),
|
||||
("dark".to_string(), dark_swatches),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeExport for SwatchScheme {
|
||||
fn output_css_variables(&self) -> String {
|
||||
let mut variables = Vec::new();
|
||||
|
||||
for (name, swatch) in &self.light {
|
||||
variables.extend(swatch.to_css_variables("light", name));
|
||||
}
|
||||
for (name, swatch) in &self.dark {
|
||||
variables.extend(swatch.to_css_variables("dark", name));
|
||||
}
|
||||
|
||||
variables.join("\n")
|
||||
}
|
||||
|
||||
fn output_scss_variables(&self) -> String {
|
||||
let mut variables = Vec::new();
|
||||
|
||||
for (name, swatch) in &self.light {
|
||||
variables.extend(swatch.to_scss_variables("light", name));
|
||||
}
|
||||
for (name, swatch) in &self.dark {
|
||||
variables.extend(swatch.to_scss_variables("dark", name));
|
||||
}
|
||||
|
||||
variables.join("\n")
|
||||
}
|
||||
|
||||
fn output_javascript_object(&self) -> String {
|
||||
let mut object = Vec::new();
|
||||
object.push("{".to_string());
|
||||
|
||||
object.push(" light: {".to_string());
|
||||
for (name, swatch) in &self.light {
|
||||
object.extend(
|
||||
swatch
|
||||
.to_javascript_fields("light", name)
|
||||
.iter()
|
||||
.map(|s| format!(" {}", s)),
|
||||
);
|
||||
}
|
||||
object.push(" },".to_string());
|
||||
|
||||
object.push(" dark: {".to_string());
|
||||
for (name, swatch) in &self.dark {
|
||||
object.extend(
|
||||
swatch
|
||||
.to_javascript_fields("dark", name)
|
||||
.iter()
|
||||
.map(|s| format!(" {}", s)),
|
||||
);
|
||||
}
|
||||
object.push(" },".to_string());
|
||||
|
||||
object.push("}".to_string());
|
||||
|
||||
object.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn swatch_scheme_default_settings() -> SwatchSchemeSetting {
|
||||
SwatchSchemeSetting::default()
|
||||
}
|
54
color-module/src/schemes/swatch_style/setting.rs
Normal file
54
color-module/src/schemes/swatch_style/setting.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
|
||||
|
||||
use crate::schemes::q_style::ColorShifting;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct SwatchSchemeSetting {
|
||||
pub amount: usize,
|
||||
pub min_lightness: f32,
|
||||
pub max_lightness: f32,
|
||||
pub include_primary: bool,
|
||||
pub dark_convert: ColorShifting,
|
||||
}
|
||||
|
||||
impl Default for SwatchSchemeSetting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
amount: 10,
|
||||
min_lightness: 0.1,
|
||||
max_lightness: 0.9,
|
||||
include_primary: false,
|
||||
dark_convert: ColorShifting {
|
||||
chroma: -0.3,
|
||||
lightness: -0.3,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SwatchSchemeSetting {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
amount: usize,
|
||||
min_lightness: f32,
|
||||
max_lightness: f32,
|
||||
include_primary: bool,
|
||||
dark_convert: ColorShifting,
|
||||
) -> Self {
|
||||
Self {
|
||||
amount,
|
||||
min_lightness,
|
||||
max_lightness,
|
||||
include_primary,
|
||||
dark_convert,
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJsValue)]
|
||||
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
|
||||
Ok(serde_wasm_bindgen::to_value(self)?)
|
||||
}
|
||||
}
|
131
color-module/src/schemes/swatch_style/swatch.rs
Normal file
131
color-module/src/schemes/swatch_style/swatch.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use palette::Oklch;
|
||||
|
||||
use crate::convert::map_oklch_to_srgb_hex;
|
||||
|
||||
use super::setting::SwatchSchemeSetting;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Swatch {
|
||||
min_key: f32,
|
||||
max_key: f32,
|
||||
primary_key: Oklch,
|
||||
include_primary: bool,
|
||||
color_amount: usize,
|
||||
}
|
||||
|
||||
impl Swatch {
|
||||
pub fn new(primary: &Oklch, setting: &SwatchSchemeSetting) -> Self {
|
||||
Self {
|
||||
min_key: primary.l.min(setting.min_lightness),
|
||||
max_key: primary.l.max(setting.max_lightness),
|
||||
primary_key: primary.clone(),
|
||||
include_primary: setting.include_primary,
|
||||
color_amount: setting.amount,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_interval(&self) -> usize {
|
||||
if !self.include_primary {
|
||||
return 0;
|
||||
}
|
||||
if self.primary_key.l == self.min_key {
|
||||
return 0;
|
||||
}
|
||||
if self.primary_key.l == self.max_key {
|
||||
return self.color_amount - 1;
|
||||
}
|
||||
let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32;
|
||||
((self.primary_key.l - self.min_key) / step).ceil() as usize
|
||||
}
|
||||
|
||||
pub fn swatch(&self) -> Vec<Oklch> {
|
||||
let mut swatch = Vec::new();
|
||||
if self.include_primary {
|
||||
let primary_index = self.find_interval();
|
||||
if primary_index > 0 {
|
||||
let step = (self.primary_key.l - self.min_key) / (primary_index - 1) as f32;
|
||||
for i in 0..primary_index {
|
||||
let lightness = self.min_key + step * i as f32;
|
||||
swatch.push(Oklch {
|
||||
l: lightness,
|
||||
..self.primary_key
|
||||
});
|
||||
}
|
||||
}
|
||||
if primary_index < self.color_amount - 1 {
|
||||
let step = (self.max_key - self.primary_key.l)
|
||||
/ (self.color_amount - primary_index - 1) as f32;
|
||||
for i in primary_index..self.color_amount {
|
||||
let lightness = self.min_key + step * i as f32;
|
||||
swatch.push(Oklch {
|
||||
l: lightness,
|
||||
..self.primary_key
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32;
|
||||
for i in 0..self.color_amount {
|
||||
let lightness = self.min_key + step * i as f32;
|
||||
swatch.push(Oklch {
|
||||
l: lightness,
|
||||
..self.primary_key
|
||||
});
|
||||
}
|
||||
}
|
||||
swatch
|
||||
}
|
||||
|
||||
pub fn swtch_hex(&self) -> Vec<String> {
|
||||
self.swatch().iter().map(map_oklch_to_srgb_hex).collect()
|
||||
}
|
||||
|
||||
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variables = Vec::new();
|
||||
for (i, color) in self.swatch().iter().enumerate() {
|
||||
variables.push(format!(
|
||||
"--color-{}-{}-{}: #{};",
|
||||
prefix,
|
||||
name,
|
||||
i * 100,
|
||||
map_oklch_to_srgb_hex(color)
|
||||
));
|
||||
}
|
||||
variables
|
||||
}
|
||||
|
||||
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variables = Vec::new();
|
||||
for (i, color) in self.swatch().iter().enumerate() {
|
||||
variables.push(format!(
|
||||
"${}-{}-{}: #{};",
|
||||
prefix,
|
||||
name,
|
||||
i * 100,
|
||||
map_oklch_to_srgb_hex(color)
|
||||
));
|
||||
}
|
||||
variables
|
||||
}
|
||||
|
||||
pub fn to_javascript_fields(&self, prefix: &str, name: &str) -> Vec<String> {
|
||||
let mut variables = Vec::new();
|
||||
let capitalized_name = name
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_ascii_uppercase()
|
||||
.to_string()
|
||||
+ &name[1..];
|
||||
for (i, color) in self.swatch().iter().enumerate() {
|
||||
variables.push(format!(
|
||||
"{}{}{}: '#{}',",
|
||||
prefix,
|
||||
capitalized_name,
|
||||
i * 100,
|
||||
map_oklch_to_srgb_hex(color)
|
||||
));
|
||||
}
|
||||
variables
|
||||
}
|
||||
}
|
87
color-module/src/series.rs
Normal file
87
color-module/src/series.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use palette::{
|
||||
cam16::{Cam16Jch, Parameters},
|
||||
Darken, FromColor, IntoColor, Lighten, Oklch, Srgb,
|
||||
};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::errors;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn series(
|
||||
color: &str,
|
||||
expand_amount: i16,
|
||||
step: f32,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Arc::new(Oklch::from_color(origin_color.clone()));
|
||||
|
||||
let mut color_series = Vec::new();
|
||||
for s in (1..=expand_amount).rev() {
|
||||
let darkened_color = Arc::clone(&oklch).darken(s as f32 * step);
|
||||
let srgb = Srgb::from_color(darkened_color);
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
color_series.push(format!("{:x}", origin_color.into_format::<u8>()));
|
||||
for s in 1..=expand_amount {
|
||||
let lightened_color = Arc::clone(&oklch).lighten(s as f32 * step);
|
||||
let srgb = Srgb::from_color(lightened_color);
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
Ok(color_series)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tonal_lighten_series(
|
||||
color: &str,
|
||||
expand_amount: i16,
|
||||
step: f32,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let hct = Cam16Jch::from_xyz(
|
||||
origin_color.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
|
||||
let mut color_series = Vec::new();
|
||||
let mut lightness = hct.lightness;
|
||||
for _ in 1..=expand_amount {
|
||||
lightness += (100.0 - lightness) * step;
|
||||
let lightened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
|
||||
let srgb = Srgb::from_color(lightened_color.into_xyz(Parameters::default_static_wp(40.0)));
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
|
||||
Ok(color_series)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tonal_darken_series(
|
||||
color: &str,
|
||||
expand_amount: i16,
|
||||
step: f32,
|
||||
) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let hct = Cam16Jch::from_xyz(
|
||||
origin_color.into_color(),
|
||||
Parameters::default_static_wp(40.0),
|
||||
);
|
||||
|
||||
let mut color_series = Vec::new();
|
||||
let mut lightness = hct.lightness;
|
||||
for _ in 1..=expand_amount {
|
||||
lightness *= 1.0 - step;
|
||||
let darkened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
|
||||
let srgb = Srgb::from_color(darkened_color.into_xyz(Parameters::default_static_wp(40.0)));
|
||||
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
|
||||
}
|
||||
|
||||
Ok(color_series.into_iter().rev().collect())
|
||||
}
|
108
color-module/src/theory.rs
Normal file
108
color-module/src/theory.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use palette::{
|
||||
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
|
||||
FromColor, Oklch, ShiftHue, Srgb,
|
||||
};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::errors;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn shift_hue(color: &str, degree: f32) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let shifted_color = oklch.shift_hue(degree);
|
||||
let srgb = Srgb::from_color(shifted_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn analogous_30(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_n30, color_p30) = oklch.analogous();
|
||||
let srgb_n30 = Srgb::from_color(color_n30);
|
||||
let srgb_p30 = Srgb::from_color(color_p30);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_n30.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p30.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn analogous_60(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_n60, color_p60) = oklch.analogous_secondary();
|
||||
let srgb_n60 = Srgb::from_color(color_n60);
|
||||
let srgb_p60 = Srgb::from_color(color_p60);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_n60.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p60.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn complementary(color: &str) -> Result<String, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let complementary_color = oklch.complementary();
|
||||
let srgb = Srgb::from_color(complementary_color);
|
||||
Ok(format!("{:x}", srgb.into_format::<u8>()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn split_complementary(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_p150, color_p210) = oklch.split_complementary();
|
||||
let srgb_p150 = Srgb::from_color(color_p150);
|
||||
let srgb_p210 = Srgb::from_color(color_p210);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_p150.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p210.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tetradic(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_p90, color_p180, color_p270) = oklch.tetradic();
|
||||
let srgb_p90 = Srgb::from_color(color_p90);
|
||||
let srgb_p180 = Srgb::from_color(color_p180);
|
||||
let srgb_p270 = Srgb::from_color(color_p270);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_p90.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p180.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p270.into_format::<u8>()),
|
||||
])
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn triadic(color: &str) -> Result<Vec<String>, errors::ColorError> {
|
||||
let origin_color = Srgb::from_str(color)
|
||||
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
|
||||
.into_format::<f32>();
|
||||
let oklch = Oklch::from_color(origin_color);
|
||||
let (color_p120, color_p240) = oklch.triadic();
|
||||
let srgb_p120 = Srgb::from_color(color_p120);
|
||||
let srgb_p240 = Srgb::from_color(color_p240);
|
||||
Ok(vec![
|
||||
format!("{:x}", srgb_p120.into_format::<u8>()),
|
||||
format!("{:x}", srgb_p240.into_format::<u8>()),
|
||||
])
|
||||
}
|
@@ -1,13 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<meta name="description"
|
||||
content="By transforming and selecting various color theories, freely design UI color combinations." />
|
||||
<title>Color Lab</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@iconify/react": "^5.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"color-module": "./color_functions",
|
||||
"dayjs": "^1.11.13",
|
||||
"jotai": "^2.11.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
17
src/App.tsx
17
src/App.tsx
@@ -1,17 +1,22 @@
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { ColorFunctionProvider } from './ColorFunctionContext';
|
||||
import { Notifications } from './components/Notifications';
|
||||
import { ColorCards } from './pages/Cards';
|
||||
import { CardsDetail } from './pages/CardsDetail';
|
||||
import { ColorCompare } from './pages/Compare';
|
||||
import { Harmonies } from './pages/Harmonies';
|
||||
import { Home } from './pages/Home';
|
||||
import { LightenDarken } from './pages/LightenDarken';
|
||||
import { MainLayout } from './pages/MainLayout';
|
||||
import { Mixer } from './pages/Mixer';
|
||||
import { NewScheme } from './pages/NewScheme';
|
||||
import { AutomaticPalette } from './pages/Palette';
|
||||
import { SchemeDetail } from './pages/SchemeDetail';
|
||||
import { SchemeNotFound } from './pages/SchemeNotFound';
|
||||
import { Schemes } from './pages/Schemes';
|
||||
import { TintsShades } from './pages/TintsShades';
|
||||
import { Tones } from './pages/Tones';
|
||||
import { WACGCheck } from './pages/WACG';
|
||||
import { Wheels } from './pages/Wheels';
|
||||
|
||||
const routes = createBrowserRouter([
|
||||
@@ -24,6 +29,7 @@ const routes = createBrowserRouter([
|
||||
path: 'schemes',
|
||||
element: <Schemes />,
|
||||
children: [
|
||||
{ index: true, element: <div /> },
|
||||
{ path: 'new', element: <NewScheme /> },
|
||||
{ path: 'not-found', element: <SchemeNotFound /> },
|
||||
{ path: ':id', element: <SchemeDetail /> },
|
||||
@@ -35,6 +41,17 @@ const routes = createBrowserRouter([
|
||||
{ path: 'tints-shades', element: <TintsShades /> },
|
||||
{ path: 'lighten-darken', element: <LightenDarken /> },
|
||||
{ path: 'mixer', element: <Mixer /> },
|
||||
{ path: 'palette', element: <AutomaticPalette /> },
|
||||
{ path: 'wacg', element: <WACGCheck /> },
|
||||
{ path: 'compare', element: <ColorCompare /> },
|
||||
{
|
||||
path: 'cards',
|
||||
element: <ColorCards />,
|
||||
children: [
|
||||
{ path: 'chinese', element: <CardsDetail mainTag="chinese" /> },
|
||||
{ path: 'japanese', element: <CardsDetail mainTag="japanese" /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import init, * as funcs from 'color-module';
|
||||
import { createContext, ReactNode, use, useEffect, useMemo, useState, useTransition } from 'react';
|
||||
import init, * as funcs from './color_functions/color_module';
|
||||
|
||||
export type ColorFunctionContextType = {
|
||||
colorFn: typeof funcs | null;
|
||||
@@ -23,7 +23,7 @@ export function useColorFunction(): ColorFunctionContextType {
|
||||
}
|
||||
|
||||
export function ColorFunctionProvider({ children }: WasmProviderProps) {
|
||||
const [wasmInstance, setWasmInstance] = useState<Wasm.InitOutput | null>(null);
|
||||
const [wasmInstance, setWasmInstance] = useState<typeof funcs | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
@@ -42,6 +42,7 @@ export function ColorFunctionProvider({ children }: WasmProviderProps) {
|
||||
try {
|
||||
await init();
|
||||
setWasmInstance(funcs);
|
||||
console.debug('[Load WASM]', 'Loaded');
|
||||
} catch (e) {
|
||||
console.error('[Load WASM]', e);
|
||||
setError(e);
|
||||
|
92
src/color_functions/color_module.d.ts
vendored
92
src/color_functions/color_module.d.ts
vendored
@@ -1,92 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function represent_rgb(color: string): Uint8Array;
|
||||
export function rgb_to_hex(r: number, g: number, b: number): string;
|
||||
export function represent_hsl(color: string): Float32Array;
|
||||
export function hsl_to_hex(h: number, s: number, l: number): string;
|
||||
export function represent_lab(color: string): Float32Array;
|
||||
export function lab_to_hex(l: number, a: number, b: number): string;
|
||||
export function represent_oklch(color: string): Float32Array;
|
||||
export function oklch_to_hex(l: number, c: number, h: number): string;
|
||||
export function represent_hct(color: string): Float32Array;
|
||||
export function hct_to_hex(hue: number, chroma: number, tone: number): string;
|
||||
export function shift_hue(color: string, degree: number): string;
|
||||
export function lighten(color: string, percent: number): string;
|
||||
export function lighten_absolute(color: string, value: number): string;
|
||||
export function darken(color: string, percent: number): string;
|
||||
export function darken_absolute(color: string, value: number): string;
|
||||
export function mix(color1: string, color2: string, percent: number): string;
|
||||
export function tint(color: string, percent: number): string;
|
||||
export function shade(color: string, percent: number): string;
|
||||
export function wacg_relative_contrast(fg_color: string, bg_color: string): number;
|
||||
export function analogous_30(color: string): (string)[];
|
||||
export function analogous_60(color: string): (string)[];
|
||||
export function complementary(color: string): string;
|
||||
export function split_complementary(color: string): (string)[];
|
||||
export function tetradic(color: string): (string)[];
|
||||
export function triadic(color: string): (string)[];
|
||||
export function series(color: string, expand_amount: number, step: number): (string)[];
|
||||
export function tonal_lighten_series(color: string, expand_amount: number, step: number): (string)[];
|
||||
export function tonal_darken_series(color: string, expand_amount: number, step: number): (string)[];
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly represent_rgb: (a: number, b: number) => [number, number, number, number];
|
||||
readonly rgb_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly represent_hsl: (a: number, b: number) => [number, number, number, number];
|
||||
readonly hsl_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly represent_lab: (a: number, b: number) => [number, number, number, number];
|
||||
readonly lab_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly represent_oklch: (a: number, b: number) => [number, number, number, number];
|
||||
readonly oklch_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly represent_hct: (a: number, b: number) => [number, number, number, number];
|
||||
readonly hct_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly shift_hue: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly lighten: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly lighten_absolute: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly darken: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly darken_absolute: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly mix: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||
readonly tint: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly shade: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
readonly analogous_30: (a: number, b: number) => [number, number, number, number];
|
||||
readonly analogous_60: (a: number, b: number) => [number, number, number, number];
|
||||
readonly complementary: (a: number, b: number) => [number, number, number, number];
|
||||
readonly split_complementary: (a: number, b: number) => [number, number, number, number];
|
||||
readonly tetradic: (a: number, b: number) => [number, number, number, number];
|
||||
readonly triadic: (a: number, b: number) => [number, number, number, number];
|
||||
readonly series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly __wbindgen_export_0: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
@@ -1,833 +0,0 @@
|
||||
let wasm;
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
? function (arg, view) {
|
||||
return cachedTextEncoder.encodeInto(arg, view);
|
||||
}
|
||||
: function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
});
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_export_0.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function represent_rgb(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.represent_rgb(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @returns {string}
|
||||
*/
|
||||
export function rgb_to_hex(r, g, b) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.rgb_to_hex(r, g, b);
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let cachedFloat32ArrayMemory0 = null;
|
||||
|
||||
function getFloat32ArrayMemory0() {
|
||||
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
|
||||
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedFloat32ArrayMemory0;
|
||||
}
|
||||
|
||||
function getArrayF32FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
|
||||
}
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {Float32Array}
|
||||
*/
|
||||
export function represent_hsl(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.represent_hsl(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} h
|
||||
* @param {number} s
|
||||
* @param {number} l
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hsl_to_hex(h, s, l) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.hsl_to_hex(h, s, l);
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {Float32Array}
|
||||
*/
|
||||
export function represent_lab(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.represent_lab(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} l
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @returns {string}
|
||||
*/
|
||||
export function lab_to_hex(l, a, b) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.lab_to_hex(l, a, b);
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {Float32Array}
|
||||
*/
|
||||
export function represent_oklch(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.represent_oklch(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} l
|
||||
* @param {number} c
|
||||
* @param {number} h
|
||||
* @returns {string}
|
||||
*/
|
||||
export function oklch_to_hex(l, c, h) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.oklch_to_hex(l, c, h);
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {Float32Array}
|
||||
*/
|
||||
export function represent_hct(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.represent_hct(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} hue
|
||||
* @param {number} chroma
|
||||
* @param {number} tone
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hct_to_hex(hue, chroma, tone) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.hct_to_hex(hue, chroma, tone);
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} degree
|
||||
* @returns {string}
|
||||
*/
|
||||
export function shift_hue(color, degree) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.shift_hue(ptr0, len0, degree);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} percent
|
||||
* @returns {string}
|
||||
*/
|
||||
export function lighten(color, percent) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.lighten(ptr0, len0, percent);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function lighten_absolute(color, value) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.lighten_absolute(ptr0, len0, value);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} percent
|
||||
* @returns {string}
|
||||
*/
|
||||
export function darken(color, percent) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.darken(ptr0, len0, percent);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function darken_absolute(color, value) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.darken_absolute(ptr0, len0, value);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color1
|
||||
* @param {string} color2
|
||||
* @param {number} percent
|
||||
* @returns {string}
|
||||
*/
|
||||
export function mix(color1, color2, percent) {
|
||||
let deferred4_0;
|
||||
let deferred4_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(color2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.mix(ptr0, len0, ptr1, len1, percent);
|
||||
var ptr3 = ret[0];
|
||||
var len3 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr3 = 0; len3 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred4_0 = ptr3;
|
||||
deferred4_1 = len3;
|
||||
return getStringFromWasm0(ptr3, len3);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} percent
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tint(color, percent) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.tint(ptr0, len0, percent);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} percent
|
||||
* @returns {string}
|
||||
*/
|
||||
export function shade(color, percent) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.shade(ptr0, len0, percent);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fg_color
|
||||
* @param {string} bg_color
|
||||
* @returns {number}
|
||||
*/
|
||||
export function wacg_relative_contrast(fg_color, bg_color) {
|
||||
const ptr0 = passStringToWasm0(fg_color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(bg_color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.wacg_relative_contrast(ptr0, len0, ptr1, len1);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return ret[0];
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function getArrayJsValueFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
const mem = getDataViewMemory0();
|
||||
const result = [];
|
||||
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
||||
result.push(wasm.__wbindgen_export_0.get(mem.getUint32(i, true)));
|
||||
}
|
||||
wasm.__externref_drop_slice(ptr, len);
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function analogous_30(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.analogous_30(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function analogous_60(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.analogous_60(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {string}
|
||||
*/
|
||||
export function complementary(color) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.complementary(ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function split_complementary(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.split_complementary(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function tetradic(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.tetradic(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function triadic(color) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.triadic(ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} expand_amount
|
||||
* @param {number} step
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function series(color, expand_amount, step) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.series(ptr0, len0, expand_amount, step);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} expand_amount
|
||||
* @param {number} step
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function tonal_lighten_series(color, expand_amount, step) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.tonal_lighten_series(ptr0, len0, expand_amount, step);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
* @param {number} expand_amount
|
||||
* @param {number} step
|
||||
* @returns {(string)[]}
|
||||
*/
|
||||
export function tonal_darken_series(color, expand_amount, step) {
|
||||
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.tonal_darken_series(ptr0, len0, expand_amount, step);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
|
||||
} catch (e) {
|
||||
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||
const table = wasm.__wbindgen_export_0;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
;
|
||||
};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
};
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
function __wbg_init_memory(imports, memory) {
|
||||
|
||||
}
|
||||
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasm = instance.exports;
|
||||
__wbg_init.__wbindgen_wasm_module = module;
|
||||
cachedDataViewMemory0 = null;
|
||||
cachedFloat32ArrayMemory0 = null;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
|
||||
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module_or_path !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module_or_path === 'undefined') {
|
||||
module_or_path = new URL('color_module_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync };
|
||||
export default __wbg_init;
|
Binary file not shown.
38
src/color_functions/color_module_bg.wasm.d.ts
vendored
38
src/color_functions/color_module_bg.wasm.d.ts
vendored
@@ -1,38 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const represent_rgb: (a: number, b: number) => [number, number, number, number];
|
||||
export const rgb_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const represent_hsl: (a: number, b: number) => [number, number, number, number];
|
||||
export const hsl_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const represent_lab: (a: number, b: number) => [number, number, number, number];
|
||||
export const lab_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const represent_oklch: (a: number, b: number) => [number, number, number, number];
|
||||
export const oklch_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const represent_hct: (a: number, b: number) => [number, number, number, number];
|
||||
export const hct_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const shift_hue: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const lighten: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const lighten_absolute: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const darken: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const darken_absolute: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const mix: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||
export const tint: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const shade: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
export const wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
export const analogous_30: (a: number, b: number) => [number, number, number, number];
|
||||
export const analogous_60: (a: number, b: number) => [number, number, number, number];
|
||||
export const complementary: (a: number, b: number) => [number, number, number, number];
|
||||
export const split_complementary: (a: number, b: number) => [number, number, number, number];
|
||||
export const tetradic: (a: number, b: number) => [number, number, number, number];
|
||||
export const triadic: (a: number, b: number) => [number, number, number, number];
|
||||
export const series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
export const tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
export const tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
export const __wbindgen_export_0: WebAssembly.Table;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __externref_drop_slice: (a: number, b: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "color-module",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"files": [
|
||||
"color_module_bg.wasm",
|
||||
"color_module.js",
|
||||
"color_module.d.ts"
|
||||
],
|
||||
"main": "color_module.js",
|
||||
"types": "color_module.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
@@ -170,7 +170,7 @@
|
||||
}
|
||||
|
||||
/* 输入框以及输入框组合体默认样式 */
|
||||
:where(input, textarea) {
|
||||
:where(input, textarea, select) {
|
||||
border: 1px solid oklch(from var(--color-bg) calc(l + (1 - l) * 0.1) c h);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
|
||||
@@ -193,6 +193,7 @@
|
||||
resize: none;
|
||||
}
|
||||
.input_wrapper {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -384,5 +385,68 @@
|
||||
background: transparent;
|
||||
border-radius: var(--border-radius-xxs);
|
||||
}
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
&::-webkit-slider-thumb {
|
||||
background: oklch(from var(--color-primary-disabled) l c h / 70%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&::-moz-range-thumb {
|
||||
background: oklch(from var(--color-primary-disabled) l c h / 70%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&::-webkit-slider-runnable-track {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&::-moz-range-track {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Badge */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) var(--spacing-m);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: bold;
|
||||
line-height: 1.2em;
|
||||
color: var(--color-fg);
|
||||
background-color: var(--color-neutral);
|
||||
&.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.primary {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
&.secondary {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
&.accent {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
&.danger {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
&.warn {
|
||||
background-color: var(--color-warn);
|
||||
}
|
||||
&.success {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
&.info {
|
||||
background-color: var(--color-info);
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.4em;
|
||||
}
|
||||
}
|
||||
|
10
src/components/ActionIcon.module.css
Normal file
10
src/components/ActionIcon.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@layer components {
|
||||
.action_icon {
|
||||
padding: var(--spacing-xs);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-xxs);
|
||||
line-height: 1em;
|
||||
.icon {
|
||||
}
|
||||
}
|
||||
}
|
25
src/components/ActionIcon.tsx
Normal file
25
src/components/ActionIcon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
|
||||
import cx from 'clsx';
|
||||
import { MouseEvent, MouseEventHandler, useCallback } from 'react';
|
||||
import styles from './ActionIcon.module.css';
|
||||
|
||||
type ActionIconProps = {
|
||||
icon: IconProps['icon'];
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
extendClassName?: HTMLButtonElement['className'];
|
||||
};
|
||||
|
||||
export function ActionIcon({ icon, onClick, extendClassName }: ActionIconProps) {
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>) => {
|
||||
onClick?.(event);
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<button type="button" onClick={handleClick} className={cx(styles.action_icon, extendClassName)}>
|
||||
<Icon icon={icon} className={styles.icon} />
|
||||
</button>
|
||||
);
|
||||
}
|
12
src/components/Badge.module.css
Normal file
12
src/components/Badge.module.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@layer components {
|
||||
.badge {
|
||||
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
12
src/components/Badge.tsx
Normal file
12
src/components/Badge.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import cx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
import styles from './Badge.module.css';
|
||||
|
||||
type BadgeProps = {
|
||||
extendClassName?: HTMLDivElement['className'];
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function Badge({ extendClassName, children }: BadgeProps) {
|
||||
return <div className={cx(styles.badge, extendClassName)}>{children}</div>;
|
||||
}
|
@@ -3,7 +3,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-xs);
|
||||
.extended_input_wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.rgb_input {
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
|
@@ -87,7 +87,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
|
||||
}
|
||||
};
|
||||
const updateH = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(evt.target.value, 10);
|
||||
let value = parseInt(evt.target.value, 10);
|
||||
if (value > 360) {
|
||||
value -= 360;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
|
||||
|
||||
return (
|
||||
<div className={styles.rgb_input}>
|
||||
<div className={cx('input_wrapper')}>
|
||||
<div className={cx('input_wrapper', styles.extended_input_wrapper)}>
|
||||
<Icon icon="tabler:hash" />
|
||||
<input type="text" value={hex} onChange={updateHex} className={styles.rgb_input} />
|
||||
</div>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
@layer components {
|
||||
.color_picker {
|
||||
min-width: calc(var(--spacing) * 105);
|
||||
max-width: calc(var(--spacing) * 125);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@@ -28,7 +28,7 @@ export function ColorRangePicker({
|
||||
}: ColorRangePickerProps) {
|
||||
const [pickerValue, setPickerValue] = useState(value);
|
||||
const handlePickerChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = evt.target.value as number;
|
||||
const value = Number(evt.target.value);
|
||||
setPickerValue(valueProcess(value));
|
||||
onChange?.(valueProcess(value));
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
|
||||
import styles from './EditableTitle.module.css';
|
||||
|
||||
type EditableTitleProps = {
|
||||
title: string;
|
||||
title?: string;
|
||||
onChange?: (newTitle: string) => void;
|
||||
};
|
||||
|
||||
|
44
src/components/FloatColorPicker.module.css
Normal file
44
src/components/FloatColorPicker.module.css
Normal file
@@ -0,0 +1,44 @@
|
||||
@layer components {
|
||||
.float_color_picker {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
.preview {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
z-index: 25;
|
||||
.preview_block {
|
||||
height: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: var(--border-radius-xxs);
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
.picker {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
padding: var(--spacing-s) var(--spacing-s);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
z-index: 260;
|
||||
.btns {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
src/components/FloatColorPicker.tsx
Normal file
56
src/components/FloatColorPicker.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ActionIcon } from './ActionIcon';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import styles from './FloatColorPicker.module.css';
|
||||
|
||||
type FloatColorPickerProps = {
|
||||
name?: string;
|
||||
color?: string | null;
|
||||
onPick?: (color: string | null | undefined) => void;
|
||||
};
|
||||
|
||||
export function FloatColorPicker({ name, color, onPick }: FloatColorPickerProps) {
|
||||
const [pickedColor, setPicked] = useState<string | null>(color ?? null);
|
||||
const [showPicker, setPickerShow] = useState(false);
|
||||
const handlePickAction = useCallback(
|
||||
(value: string) => {
|
||||
setPicked(value);
|
||||
onPick?.(value);
|
||||
},
|
||||
[onPick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(pickedColor, color)) {
|
||||
setPicked(color);
|
||||
}
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<div className={styles.float_color_picker}>
|
||||
<div className={styles.preview}>
|
||||
<div
|
||||
className={styles.preview_block}
|
||||
onClick={() => setPickerShow(true)}
|
||||
style={{
|
||||
backgroundColor: isNil(pickedColor) ? 'rgba(0, 0, 0, 0)' : `#${pickedColor}`,
|
||||
}}>
|
||||
{isNil(pickedColor) && <span>N/A</span>}
|
||||
</div>
|
||||
<ActionIcon icon="tabler:x" onClick={() => handlePickAction(null)} />
|
||||
</div>
|
||||
{showPicker && (
|
||||
<div className={styles.picker}>
|
||||
<ColorPicker color={pickedColor ?? null} onSelect={handlePickAction} />
|
||||
<div className={styles.btns}>
|
||||
<button type="button" className="primary" onClick={() => setPickerShow(false)}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isNil(name) && <input type="hidden" name={name} value={pickedColor ?? ''} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,17 +1,32 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { isEqual, isMap, isNil } from 'lodash-es';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { Option } from '../models';
|
||||
import styles from './HSegmentedControl.module.css';
|
||||
|
||||
type HSegmentedControlProps = {
|
||||
name?: string;
|
||||
defaultValue?: Option['value'];
|
||||
options?: Option[];
|
||||
value?: Option['value'];
|
||||
onChange?: (value: Option['value']) => void;
|
||||
extendClassName?: HTMLDivElement['className'];
|
||||
};
|
||||
|
||||
export function HSegmentedControl({ options = [], value, onChange }: HSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(value ?? options[0].value ?? null);
|
||||
export function HSegmentedControl({
|
||||
name,
|
||||
defaultValue,
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
extendClassName,
|
||||
}: HSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(
|
||||
value ??
|
||||
defaultValue ??
|
||||
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
|
||||
null,
|
||||
);
|
||||
const [sliderPosition, setSliderPosition] = useState(0);
|
||||
const [sliderWidth, setSliderWidth] = useState(0);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -28,17 +43,22 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.segmented_control}>
|
||||
<div className={cx(styles.segmented_control, extendClassName)}>
|
||||
<div className={styles.options}>
|
||||
{options.map((option, index) => (
|
||||
{options.map((option, index) => {
|
||||
const label = isMap(option) ? option.get('label') : option.label;
|
||||
const value = isMap(option) ? option.get('value') : option.value;
|
||||
return (
|
||||
<div
|
||||
key={`${index}_${option.value}`}
|
||||
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)}
|
||||
key={`${index}_${value}`}
|
||||
className={cx(styles.option, isEqual(selected, value) && styles.selected)}
|
||||
//@ts-expect-error TS2322
|
||||
ref={(el) => (optionsRef.current[index] = el!)}
|
||||
onClick={() => handleSelectAction(option.value, index)}>
|
||||
{option.label}
|
||||
onClick={() => handleSelectAction(value, index)}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{!isNil(selected) && (
|
||||
<div
|
||||
className={styles.slider}
|
||||
@@ -47,6 +67,7 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isNil(name) && <input type="hidden" name={name} value={selected} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -5,12 +5,19 @@
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xs);
|
||||
&.inline {
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
font-size: var(--font-size-xs);
|
||||
.inline & {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import cx from 'clsx';
|
||||
import styles from './Labeled.module.css';
|
||||
|
||||
type LabeledProps = {
|
||||
label: string;
|
||||
inline?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Labeled({ label, children }: LabeledProps) {
|
||||
export function Labeled({ label, inline = false, children }: LabeledProps) {
|
||||
return (
|
||||
<div className={styles.labeled}>
|
||||
<div className={cx(styles.labeled, inline && styles.inline)}>
|
||||
<label>{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
|
@@ -11,6 +11,7 @@ type LabeledPickerProps = {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function LabeledPicker({
|
||||
@@ -21,10 +22,11 @@ export function LabeledPicker({
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
disabled = false,
|
||||
}: LabeledPickerProps) {
|
||||
const [pickerValue, setPickerValue] = useState(value ?? min);
|
||||
const handlePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value as number;
|
||||
const value = Number(event.target.value);
|
||||
setPickerValue(value);
|
||||
onChange?.(value);
|
||||
};
|
||||
@@ -52,6 +54,7 @@ export function LabeledPicker({
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@@ -118,7 +118,7 @@ type ToastProps = {
|
||||
icon?: string;
|
||||
duration?: ToastDuration;
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
closeAction: () => void;
|
||||
closeAction: (tid?: string) => void;
|
||||
};
|
||||
const Toast = ({
|
||||
kind,
|
||||
@@ -157,7 +157,7 @@ export function useNotification() {
|
||||
type NotificationElement = {
|
||||
id: string;
|
||||
element: ReactNode;
|
||||
ref: RefObject<ReactNode>;
|
||||
ref: RefObject<ReactNode | HTMLDivElement>;
|
||||
};
|
||||
type NotificationsProps = {
|
||||
defaultDuration?: number;
|
||||
@@ -184,7 +184,7 @@ export function Notifications({
|
||||
duration?: number,
|
||||
) => {
|
||||
const id = v4();
|
||||
const ref = createRef(null);
|
||||
const ref = createRef<ReactNode | HTMLDivElement>();
|
||||
const newNotify = (
|
||||
<Notification
|
||||
kind={kind}
|
||||
@@ -194,6 +194,7 @@ export function Notifications({
|
||||
message={message}
|
||||
duration={duration ?? defaultDuration}
|
||||
closeAction={removeNotification}
|
||||
//@ts-expect-error TS2322
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
@@ -207,14 +208,9 @@ export function Notifications({
|
||||
setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id)));
|
||||
}, []);
|
||||
const showToast = useCallback(
|
||||
(
|
||||
kind: NotificationType,
|
||||
message?: string,
|
||||
icon?: IconifyIconProps['icon'],
|
||||
duration?: ToastDuration,
|
||||
) => {
|
||||
(kind: NotificationType, message?: string, icon?: string, duration?: ToastDuration) => {
|
||||
const id = v4();
|
||||
const ref = createRef(null);
|
||||
const ref = createRef<HTMLDivElement>();
|
||||
const newToast = (
|
||||
<Toast
|
||||
kind={kind}
|
||||
@@ -238,9 +234,6 @@ export function Notifications({
|
||||
value={{
|
||||
addNotification,
|
||||
removeNotification,
|
||||
showDialog: () => '',
|
||||
showModalDialog: () => '',
|
||||
closeDialog: () => {},
|
||||
showToast,
|
||||
}}>
|
||||
{children}
|
||||
@@ -250,6 +243,7 @@ export function Notifications({
|
||||
{notifications.slice(0, maxNotifications).map(({ id, element, ref }) => (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
//@ts-expect-error TS2322
|
||||
nodeRef={ref}
|
||||
unmountOnExit
|
||||
timeout={500}
|
||||
@@ -271,6 +265,7 @@ export function Notifications({
|
||||
{toasts.slice(0, 1).map(({ id, element, ref }) => (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
//@ts-expect-error TS2322
|
||||
nodeRef={ref}
|
||||
unmountOnExit
|
||||
timeout={500}
|
||||
|
21
src/components/SchemeSign.module.css
Normal file
21
src/components/SchemeSign.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
@layer components {
|
||||
.badge {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-yuebai);
|
||||
background-color: var(--color-mose);
|
||||
&.q {
|
||||
background-color: var(--color-mantianxingzi);
|
||||
}
|
||||
&.swatch {
|
||||
background-color: var(--color-pinlan);
|
||||
}
|
||||
&.m2 {
|
||||
background-color: #03dac6;
|
||||
color: var(--color-qihei);
|
||||
}
|
||||
&.m3 {
|
||||
background-color: #a78fff;
|
||||
color: var(--color-qihei);
|
||||
}
|
||||
}
|
||||
}
|
33
src/components/SchemeSign.tsx
Normal file
33
src/components/SchemeSign.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import cx from 'clsx';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { schemeType, SchemeType } from '../models';
|
||||
import { Badge } from './Badge';
|
||||
import styles from './SchemeSign.module.css';
|
||||
|
||||
type SchemeSignProps = {
|
||||
scheme?: SchemeType;
|
||||
short?: boolean;
|
||||
};
|
||||
|
||||
export function SchemeSign({ scheme, short = false }: SchemeSignProps) {
|
||||
const schemeName = schemeType(scheme, short);
|
||||
const signColorStyles = useMemo(() => {
|
||||
switch (scheme) {
|
||||
case 'q_scheme':
|
||||
return styles.q;
|
||||
case 'swatch_scheme':
|
||||
return styles.swatch;
|
||||
case 'material_2':
|
||||
return styles.m2;
|
||||
case 'material_3':
|
||||
return styles.m3;
|
||||
}
|
||||
}, [scheme]);
|
||||
|
||||
return (
|
||||
!isNil(scheme) && (
|
||||
<Badge extendClassName={cx(styles.badge, signColorStyles)}>{schemeName}</Badge>
|
||||
)
|
||||
);
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { clamp } from 'lodash-es';
|
||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { MouseEvent, RefObject, useEffect, useRef, useState, WheelEvent } from 'react';
|
||||
import styles from './ScrollArea.module.css';
|
||||
|
||||
type ScrollBarProps = {
|
||||
@@ -12,10 +12,12 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
const thumbRef = useRef<HTMLDivElement | null>(null);
|
||||
const handleMouseDown = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
const handleMouseMove = (evt: MouseEvent<HTMLDivElement>) => {
|
||||
evt.preventDefault();
|
||||
const container = containerRef?.current;
|
||||
const scrollbar = trackRef.current;
|
||||
@@ -34,7 +36,9 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
};
|
||||
const handleMouseUp = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
@@ -77,7 +81,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
const thumbRef = useRef<HTMLDivElement | null>(null);
|
||||
const handleMouseDown = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
@@ -99,7 +105,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
};
|
||||
const handleMouseUp = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
@@ -129,7 +137,7 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
className={styles.h_thumb}
|
||||
ref={thumbRef}
|
||||
style={{ left: thumbPos }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={(e) => handleMouseDown(e)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -148,11 +156,10 @@ export function ScrollArea({
|
||||
enableY = false,
|
||||
normalizedScroll = false,
|
||||
}: ScrollAreaProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [xScrollNeeded, setXScrollNeeded] = useState(false);
|
||||
const [yScrollNeeded, setYScrollNeeded] = useState(false);
|
||||
const handleWheel = (evt: WheelEvent) => {
|
||||
evt.preventDefault();
|
||||
const handleWheel = (evt: WheelEvent<HTMLDivElement>) => {
|
||||
const container = scrollContainerRef?.current;
|
||||
if (enableY && container) {
|
||||
const delta = evt.deltaY;
|
||||
@@ -178,7 +185,7 @@ export function ScrollArea({
|
||||
|
||||
return (
|
||||
<div className={styles.scroll_area}>
|
||||
<div className={styles.content} ref={scrollContainerRef} onWheel={handleWheel}>
|
||||
<div className={styles.content} ref={scrollContainerRef} onWheel={(e) => handleWheel(e)}>
|
||||
{children}
|
||||
</div>
|
||||
{enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />}
|
||||
|
@@ -1,22 +1,23 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styles from './Switch.module.css';
|
||||
|
||||
type SwitchProps = {
|
||||
name?: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
export function Switch({ checked = false, disabled = false, onChange }: SwitchProps) {
|
||||
export function Switch({ name, checked = false, disabled = false, onChange }: SwitchProps) {
|
||||
const [isChecked, setIsChecked] = useState(checked);
|
||||
const handleSwitch = useCallback(() => {
|
||||
if (!disabled) {
|
||||
setIsChecked((prev) => !prev);
|
||||
onChange?.(!isChecked);
|
||||
}
|
||||
}, [onChange, disabled]);
|
||||
}, [onChange, isChecked, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(checked, isChecked)) {
|
||||
@@ -25,10 +26,14 @@ export function Switch({ checked = false, disabled = false, onChange }: SwitchPr
|
||||
}, [checked]);
|
||||
|
||||
return (
|
||||
//@ts-expect-error TS2322
|
||||
<div className={styles.switch} disabled={disabled}>
|
||||
<div
|
||||
className={cx(styles.switch_handle, isChecked && styles.checked)}
|
||||
onClick={handleSwitch}></div>
|
||||
{!isNil(name) && (
|
||||
<input type="hidden" name={name} value={isChecked ? 'checked' : undefined} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,20 +1,30 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styles from './Tab.module.css';
|
||||
|
||||
type TabProps = {
|
||||
tabs: { title: string; id: unknown }[];
|
||||
activeTab?: unknown;
|
||||
onActive?: (id: unknown) => void;
|
||||
};
|
||||
|
||||
export function Tab({ tabs = [], onActive }: TabProps) {
|
||||
const [active, setActive] = useState(0);
|
||||
export function Tab({ tabs = [], activeTab, onActive }: TabProps) {
|
||||
const [active, setActive] = useState(() =>
|
||||
isNil(activeTab) ? 0 : tabs.findIndex((tab) => isEqual(tab.id, activeTab)),
|
||||
);
|
||||
const handleActivate = useCallback((index: number) => {
|
||||
setActive(index);
|
||||
onActive?.(tabs[index].id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const activeIndex = tabs.findIndex((tab) => isEqual(tab.id, activeTab));
|
||||
if (!isNil(activeIndex) && !isEqual(activeIndex, -1) && !isEqual(activeIndex, active)) {
|
||||
setActive(activeIndex);
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
<div className={styles.tabs_container}>
|
||||
{tabs.map((tab, index) => (
|
||||
|
@@ -18,7 +18,7 @@ const positionMap = {
|
||||
|
||||
export function Tooltip({ content, position = 'top', children }: TooltipProps) {
|
||||
const [show, setShow] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@@ -1,17 +1,32 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { isEqual, isMap, isNil } from 'lodash-es';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { Option } from '../models';
|
||||
import styles from './VSegmentedControl.module.css';
|
||||
|
||||
type VSegmentedControlProps = {
|
||||
name?: string;
|
||||
defaultValue?: Option['value'];
|
||||
options?: Option[];
|
||||
value?: Option['value'];
|
||||
onChange?: (value: Option['value']) => void;
|
||||
extendClassName?: HTMLDivElement['className'];
|
||||
};
|
||||
|
||||
export function VSegmentedControl({ options = [], value, onChange }: VSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(value ?? options[0].value ?? null);
|
||||
export function VSegmentedControl({
|
||||
name,
|
||||
defaultValue,
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
extendClassName,
|
||||
}: VSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(
|
||||
value ??
|
||||
defaultValue ??
|
||||
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
|
||||
null,
|
||||
);
|
||||
const [sliderPosition, setSliderPosition] = useState(0);
|
||||
const [sliderHeight, setSliderHeight] = useState(0);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -28,17 +43,22 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.segmented_control}>
|
||||
<div className={cx(styles.segmented_control, extendClassName)}>
|
||||
<div className={styles.options}>
|
||||
{options.map((option, index) => (
|
||||
{options.map((option, index) => {
|
||||
const label = isMap(option) ? option.get('label') : option.label;
|
||||
const value = isMap(option) ? option.get('value') : option.value;
|
||||
return (
|
||||
<div
|
||||
key={`${index}_${option.value}`}
|
||||
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)}
|
||||
key={`${index}_${value}`}
|
||||
className={cx(styles.option, isEqual(selected, value) && styles.selected)}
|
||||
//@ts-expect-error TS2322
|
||||
ref={(el) => (optionsRef.current[index] = el!)}
|
||||
onClick={() => handleSelectAction(option.value, index)}>
|
||||
{option.label}
|
||||
onClick={() => handleSelectAction(value, index)}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{!isNil(selected) && (
|
||||
<div
|
||||
className={styles.slider}
|
||||
@@ -47,6 +67,7 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isNil(name) && <input type="hidden" name={name} value={selected} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
28
src/hooks/useCopy.ts
Normal file
28
src/hooks/useCopy.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { isEmpty, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { NotificationType, useNotification } from '../components/Notifications';
|
||||
|
||||
export function useCopy() {
|
||||
const { showToast } = useNotification();
|
||||
const [cpState, copyToClipboard] = useCopyToClipboard();
|
||||
const copyAction = useCallback((content?: string | null) => {
|
||||
if (isNil(content) || isEmpty(content)) return;
|
||||
copyToClipboard(content);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNil(cpState.error)) {
|
||||
showToast(NotificationType.ERROR, 'Failed to copy to clipboard', 'tabler:alert-circle', 3000);
|
||||
} else if (!isNil(cpState.value)) {
|
||||
showToast(
|
||||
NotificationType.SUCCESS,
|
||||
`Content copied to clipboard.`,
|
||||
'tabler:circle-check',
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}, [cpState]);
|
||||
|
||||
return copyAction;
|
||||
}
|
37
src/material-2-scheme.ts
Normal file
37
src/material-2-scheme.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export type ColorSet = {
|
||||
root: string;
|
||||
variant: string;
|
||||
on: string;
|
||||
};
|
||||
|
||||
export type Baseline = {
|
||||
primary: ColorSet;
|
||||
secondary: ColorSet;
|
||||
error: ColorSet;
|
||||
background: ColorSet;
|
||||
surface: ColorSet;
|
||||
shadow: ColorSet;
|
||||
custom_colors: Record<string, ColorSet>;
|
||||
};
|
||||
|
||||
export type MaterialDesign2Scheme = {
|
||||
white: string;
|
||||
black: string;
|
||||
light: Baseline;
|
||||
dark: Baseline;
|
||||
};
|
||||
|
||||
export type MaterialDesign2SchemeSource = {
|
||||
primary: string | null;
|
||||
secondary: string | null;
|
||||
error: string | null;
|
||||
custom_colors?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type MaterialDesign2SchemeStorage = {
|
||||
source?: MaterialDesign2SchemeSource;
|
||||
scheme?: MaterialDesign2Scheme;
|
||||
cssVariables?: string;
|
||||
scssVariables?: string;
|
||||
jsVariables?: string;
|
||||
};
|
60
src/material-3-scheme.ts
Normal file
60
src/material-3-scheme.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export type ColorSet = {
|
||||
root: string;
|
||||
on_root: string;
|
||||
container: string;
|
||||
on_container: string;
|
||||
fixed: string;
|
||||
fixed_dim: string;
|
||||
on_fixed: string;
|
||||
fixed_variant: string;
|
||||
inverse: string;
|
||||
};
|
||||
|
||||
export type Surface = {
|
||||
root: string;
|
||||
dim: string;
|
||||
bright: string;
|
||||
container: string;
|
||||
container_lowest: string;
|
||||
container_low: string;
|
||||
container_high: string;
|
||||
container_highest: string;
|
||||
on_root: string;
|
||||
on_root_variant: string;
|
||||
inverse: string;
|
||||
on_inverse: string;
|
||||
};
|
||||
|
||||
export type Baseline = {
|
||||
primary: ColorSet;
|
||||
secondary: ColorSet;
|
||||
tertiary: ColorSet;
|
||||
error: ColorSet;
|
||||
surface: Surface;
|
||||
outline: string;
|
||||
outline_variant: string;
|
||||
scrim: string;
|
||||
shadow: string;
|
||||
customs: Record<string, ColorSet>;
|
||||
};
|
||||
|
||||
export type MaterialDesign3Scheme = {
|
||||
white: string;
|
||||
black: string;
|
||||
light_baseline: Baseline;
|
||||
dark_baseline: Baseline;
|
||||
};
|
||||
|
||||
export type MaterialDesign3SchemeSource = {
|
||||
source: string | null;
|
||||
error: string | null;
|
||||
custom_colors?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type MaterialDesign3SchemeStorage = {
|
||||
source?: MaterialDesign3SchemeSource;
|
||||
scheme?: MaterialDesign3Scheme;
|
||||
cssVariables?: string;
|
||||
scssVariables?: string;
|
||||
jsVariables?: string;
|
||||
};
|
@@ -1,9 +1,75 @@
|
||||
export type Option = {
|
||||
import { find, isNil } from 'lodash-es';
|
||||
import { MaterialDesign2SchemeStorage } from './material-2-scheme';
|
||||
import { MaterialDesign3SchemeStorage } from './material-3-scheme';
|
||||
import { QSchemeStorage } from './q-scheme';
|
||||
import { SwatchSchemeStorage } from './swatch_scheme';
|
||||
|
||||
export type Option<T = string | number | null> =
|
||||
| {
|
||||
label: string;
|
||||
value: string | number | null;
|
||||
};
|
||||
value: T;
|
||||
}
|
||||
| Record<'label' | 'value', T>;
|
||||
|
||||
export type HarmonyColor = {
|
||||
color: string;
|
||||
ratio: number;
|
||||
};
|
||||
|
||||
export type ColorDescription = {
|
||||
name: string;
|
||||
pinyin: string[];
|
||||
hue: number;
|
||||
lightness: number;
|
||||
category: string;
|
||||
tags: string[];
|
||||
rgb: [number, number, number];
|
||||
hsl: [number, number, number];
|
||||
lab: [number, number, number];
|
||||
oklch: [number, number, number];
|
||||
};
|
||||
|
||||
export type SchemeType = 'q_scheme' | 'swatch_scheme' | 'material_2' | 'material_3';
|
||||
export type SchemeTypeOption = {
|
||||
label: string;
|
||||
short: string;
|
||||
value: SchemeType;
|
||||
};
|
||||
export const SchemeTypeOptions: SchemeTypeOption[] = [
|
||||
{ label: 'Q Scheme', short: 'Q', value: 'q_scheme' },
|
||||
{ label: 'Swatch Scheme', short: 'Swatch', value: 'swatch_scheme' },
|
||||
{ label: 'Material Design 2 Scheme', short: 'M2', value: 'material_2' },
|
||||
{ label: 'Material Design 3 Scheme', short: 'M3', value: 'material_3' },
|
||||
];
|
||||
|
||||
export function schemeType(
|
||||
value?: SchemeTypeOption['value'] | null,
|
||||
short?: boolean,
|
||||
): string | null {
|
||||
const useShort = short ?? false;
|
||||
const foundType = find(SchemeTypeOptions, { value }) as SchemeTypeOption | undefined;
|
||||
if (isNil(foundType)) {
|
||||
return 'CORRUPTED';
|
||||
}
|
||||
return useShort ? foundType.short : foundType.label;
|
||||
}
|
||||
|
||||
export type SchemeContent<SchemeStorage> = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
description: string | null;
|
||||
type: SchemeType;
|
||||
schemeStorage: SchemeStorage;
|
||||
};
|
||||
|
||||
export type ColorShifting = {
|
||||
chroma: number;
|
||||
lightness: number;
|
||||
};
|
||||
|
||||
export type SchemeStorage =
|
||||
| QSchemeStorage
|
||||
| SwatchSchemeStorage
|
||||
| MaterialDesign2SchemeStorage
|
||||
| MaterialDesign3SchemeStorage;
|
||||
|
55
src/page-components/auto-palette/PaletteColors.tsx
Normal file
55
src/page-components/auto-palette/PaletteColors.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import { FlexColorStand } from '../../components/FlexColorStand';
|
||||
|
||||
type PaletteColorsProps = {
|
||||
color: string;
|
||||
swatchAmount: number;
|
||||
referenceBias: number;
|
||||
useReference: boolean;
|
||||
min: number;
|
||||
max: number;
|
||||
copyMode: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
|
||||
};
|
||||
|
||||
export function PaletteColors({
|
||||
color = '000000',
|
||||
swatchAmount,
|
||||
referenceBias,
|
||||
useReference,
|
||||
min,
|
||||
max,
|
||||
copyMode,
|
||||
}: PaletteColorsProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const colors = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return Array.from({ length: swatchAmount }, () => color);
|
||||
}
|
||||
try {
|
||||
if (!useReference) {
|
||||
return colorFn.generate_palette_from_color(color, swatchAmount, min, max);
|
||||
} else {
|
||||
return colorFn.generate_palette_from_color(
|
||||
color,
|
||||
swatchAmount,
|
||||
min,
|
||||
max,
|
||||
useReference,
|
||||
referenceBias,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Generate Auto Palette]', e);
|
||||
}
|
||||
return Array.from({ length: swatchAmount }, () => color);
|
||||
}, [color, swatchAmount, referenceBias, useReference, min, max]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{colors.map((c, index) => (
|
||||
<FlexColorStand key={`${c}-${index}`} color={c} valueMode={copyMode} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
42
src/page-components/cards-detail/ColorCard.module.css
Normal file
42
src/page-components/cards-detail/ColorCard.module.css
Normal file
@@ -0,0 +1,42 @@
|
||||
@layer pages {
|
||||
.card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-n);
|
||||
font-size: var(--font-size-xxs);
|
||||
line-height: var(--font-size-xxs);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-xs);
|
||||
cursor: pointer;
|
||||
}
|
||||
.color_block {
|
||||
width: 100%;
|
||||
height: 5em;
|
||||
}
|
||||
.description_line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xxs);
|
||||
flex: 1;
|
||||
.name {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--font-size-xs);
|
||||
font-weight: bold;
|
||||
}
|
||||
.en_name {
|
||||
font-style: italic;
|
||||
color: var(--color-neutral-focus);
|
||||
}
|
||||
}
|
||||
.color_value {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
76
src/page-components/cards-detail/ColorCard.tsx
Normal file
76
src/page-components/cards-detail/ColorCard.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { capitalize, isEmpty } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import { useCopyColor } from '../../hooks/useCopyColor';
|
||||
import { ColorDescription } from '../../models';
|
||||
import styles from './ColorCard.module.css';
|
||||
|
||||
type ColorCardProps = {
|
||||
color: ColorDescription;
|
||||
copyMode?: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
|
||||
};
|
||||
|
||||
export function ColorCard({ color, copyMode }: ColorCardProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const copytToClipboard = useCopyColor();
|
||||
const colorHex = useMemo(() => {
|
||||
const [r, g, b] = color.rgb;
|
||||
if (colorFn) {
|
||||
try {
|
||||
const hex = colorFn.rgb_to_hex(r, g, b);
|
||||
return hex;
|
||||
} catch (e) {
|
||||
console.error('[Convert RGB]', e);
|
||||
}
|
||||
}
|
||||
return `${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}, [colorFn, color]);
|
||||
const handleCopy = useCallback(() => {
|
||||
switch (copyMode) {
|
||||
case 'rgb':
|
||||
copytToClipboard(`rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})`);
|
||||
break;
|
||||
case 'hsl':
|
||||
copytToClipboard(
|
||||
`hsl(${color.hsl[0].toFixed(1)}, ${(color.hsl[1] * 100).toFixed(2)}%, ${(
|
||||
color.hsl[2] * 100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
break;
|
||||
case 'lab':
|
||||
copytToClipboard(
|
||||
`lab(${color.lab[0].toFixed(1)}, ${color.lab[1].toFixed(2)}, ${color.lab[2].toFixed(2)})`,
|
||||
);
|
||||
break;
|
||||
case 'oklch':
|
||||
copytToClipboard(
|
||||
`oklch(${(color.oklch[0] * 100).toFixed(2)}%, ${color.oklch[1].toFixed(
|
||||
4,
|
||||
)}, ${color.oklch[2].toFixed(1)})`,
|
||||
);
|
||||
break;
|
||||
case 'hex':
|
||||
default:
|
||||
copytToClipboard(`#${colorHex}`);
|
||||
break;
|
||||
}
|
||||
}, [copytToClipboard, color, copyMode, colorHex]);
|
||||
|
||||
return (
|
||||
<div className={styles.card} onClick={handleCopy}>
|
||||
<div
|
||||
className={styles.color_block}
|
||||
style={{ backgroundColor: `rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` }}
|
||||
/>
|
||||
<div className={styles.description_line}>
|
||||
<div className={styles.title}>
|
||||
<span className={styles.name}>{color.name}</span>
|
||||
{!isEmpty(color.pinyin) && (
|
||||
<span className={styles.en_name}>{color.pinyin.map(capitalize).join(' ')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.color_value}>#{colorHex}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
26
src/page-components/color-cards/CardNavigation.tsx
Normal file
26
src/page-components/color-cards/CardNavigation.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import cx from 'clsx';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styles from './CardsNavigation.module.css';
|
||||
|
||||
export function CardsNavigation() {
|
||||
return (
|
||||
<div className={styles.cards_list}>
|
||||
<menu className={styles.nav_menu}>
|
||||
<li>
|
||||
<NavLink
|
||||
to="chinese"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Chinese Traditional
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="japanese"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Japanese Traditional
|
||||
</NavLink>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
);
|
||||
}
|
33
src/page-components/color-cards/CardsNavigation.module.css
Normal file
33
src/page-components/color-cards/CardsNavigation.module.css
Normal file
@@ -0,0 +1,33 @@
|
||||
@layer pages {
|
||||
.cards_list {
|
||||
max-width: calc(var(--spacing) * 125);
|
||||
flex: 1 1 calc(var(--spacing) * 125);
|
||||
padding: calc(var(--spacing) * 4) 0;
|
||||
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
|
||||
z-index: 40;
|
||||
}
|
||||
.nav_menu {
|
||||
flex: 1 0;
|
||||
padding: var(--spacing-n);
|
||||
padding-block-start: var(--spacing-s);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xs);
|
||||
li {
|
||||
list-style: none;
|
||||
a.nav_link {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-inline: var(--spacing-l);
|
||||
padding-block: var(--spacing-s);
|
||||
&.active {
|
||||
background-color: var(--color-primary-active);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
src/page-components/color-compare/CompareLayout.module.css
Normal file
38
src/page-components/color-compare/CompareLayout.module.css
Normal file
@@ -0,0 +1,38 @@
|
||||
@layer pages {
|
||||
.elements {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
&:not(:first-of-type) {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
.element {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
.element_name {
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: bold;
|
||||
&.mean {
|
||||
border-top: 2px solid var(--color-fg);
|
||||
padding-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
.element_values {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--font-size-xs);
|
||||
}
|
||||
.element_value {
|
||||
align-self: flex-end;
|
||||
font-size: var(--font-size-s);
|
||||
line-height: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
src/page-components/color-compare/HCTCompare.tsx
Normal file
71
src/page-components/color-compare/HCTCompare.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Differ, HctDiffference } from 'color-module';
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import styles from './CompareLayout.module.css';
|
||||
import { CompareMethodProps } from './share-props';
|
||||
|
||||
export function HCTCompare({
|
||||
basic = '000000',
|
||||
compare = '000000',
|
||||
mode = 'absolute',
|
||||
}: CompareMethodProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const defaultCompareResult: HctDiffference = useMemo(
|
||||
() => new HctDiffference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
|
||||
[],
|
||||
);
|
||||
const differ = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return defaultCompareResult;
|
||||
}
|
||||
try {
|
||||
const diffFn = {
|
||||
absolute: colorFn.differ_in_hct,
|
||||
relative: colorFn.relative_differ_in_hct,
|
||||
};
|
||||
const diff = diffFn[mode](basic, compare);
|
||||
return diff;
|
||||
} catch (e) {
|
||||
console.error('[Compare in HCT]', e);
|
||||
}
|
||||
return defaultCompareResult;
|
||||
}, [basic, compare, mode]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h6>Compare in HCT Space</h6>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>H</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.hue.delta.toFixed(2)}</span>
|
||||
<span>
|
||||
{isNaN(differ.hue.percent) ? '0.00' : (differ.hue.percent * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>C</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.chroma.delta.toFixed(2)}</span>
|
||||
<span>
|
||||
{isNaN(differ.chroma.percent) ? '0.00' : (differ.chroma.percent * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>T</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.lightness.delta.toFixed(2)}</span>
|
||||
<span>
|
||||
{isNaN(differ.lightness.percent)
|
||||
? '0.00'
|
||||
: (differ.lightness.percent * 100).toFixed(2)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
74
src/page-components/color-compare/HSLCompare.tsx
Normal file
74
src/page-components/color-compare/HSLCompare.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Differ, HSLDifference } from 'color-module';
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import styles from './CompareLayout.module.css';
|
||||
import { CompareMethodProps } from './share-props';
|
||||
|
||||
export function HSLCompare({
|
||||
basic = '000000',
|
||||
compare = '000000',
|
||||
mode = 'absolute',
|
||||
}: CompareMethodProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const defaultCompareResult: HSLDifference = useMemo(
|
||||
() => new HSLDifference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
|
||||
[],
|
||||
);
|
||||
const differ = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return defaultCompareResult;
|
||||
}
|
||||
try {
|
||||
const diffFn = {
|
||||
absolute: colorFn.differ_in_hsl,
|
||||
relative: colorFn.relative_differ_in_hsl,
|
||||
};
|
||||
const diff = diffFn[mode](basic, compare);
|
||||
return diff;
|
||||
} catch (e) {
|
||||
console.error('[Compare in HSL]', e);
|
||||
}
|
||||
return defaultCompareResult;
|
||||
}, [basic, compare, mode]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h6>Compare in HSL Space</h6>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>H</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.hue.delta.toFixed(2)}</span>
|
||||
<span>
|
||||
{isNaN(differ.hue.percent) ? '0.00' : (differ.hue.percent * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>S</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{(differ.saturation.delta * 100).toFixed(2)}%</span>
|
||||
<span>
|
||||
{isNaN(differ.saturation.percent)
|
||||
? '0.00'
|
||||
: (differ.saturation.percent * 100).toFixed(2)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>L</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{(differ.lightness.delta * 100).toFixed(2)}%</span>
|
||||
<span>
|
||||
{isNaN(differ.lightness.percent)
|
||||
? '0.00'
|
||||
: (differ.lightness.percent * 100).toFixed(2)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
71
src/page-components/color-compare/OKLCHCompare.tsx
Normal file
71
src/page-components/color-compare/OKLCHCompare.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Differ, OklchDifference } from 'color-module';
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import styles from './CompareLayout.module.css';
|
||||
import { CompareMethodProps } from './share-props';
|
||||
|
||||
export function OklchCompare({
|
||||
basic = '000000',
|
||||
compare = '000000',
|
||||
mode = 'absolute',
|
||||
}: CompareMethodProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const defaultCompareResult: OklchDifference = useMemo(
|
||||
() => new OklchDifference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
|
||||
[],
|
||||
);
|
||||
const differ = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return defaultCompareResult;
|
||||
}
|
||||
try {
|
||||
const diffFn = {
|
||||
absolute: colorFn.differ_in_oklch,
|
||||
relative: colorFn.relative_differ_in_oklch,
|
||||
};
|
||||
const diff = diffFn[mode](basic, compare);
|
||||
return diff;
|
||||
} catch (e) {
|
||||
console.error('[Compare in Oklch]', e);
|
||||
}
|
||||
return defaultCompareResult;
|
||||
}, [basic, compare, mode]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h6>Compare in Oklch Space</h6>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>L</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{(differ.lightness.delta * 100).toFixed(2)}%</span>
|
||||
<span>
|
||||
{isNaN(differ.lightness.percent)
|
||||
? '0.0000'
|
||||
: (differ.lightness.percent * 100).toFixed(2)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>C</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.chroma.delta.toFixed(4)}</span>
|
||||
<span>
|
||||
{isNaN(differ.chroma.percent) ? '0.0000' : (differ.chroma.percent * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>H</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.hue.delta.toFixed(2)}</span>
|
||||
<span>
|
||||
{isNaN(differ.hue.percent) ? '0.00' : (differ.hue.percent * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
75
src/page-components/color-compare/RGBCompare.tsx
Normal file
75
src/page-components/color-compare/RGBCompare.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Differ, RGBDifference } from 'color-module';
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import styles from './CompareLayout.module.css';
|
||||
import { CompareMethodProps } from './share-props';
|
||||
|
||||
export function RGBCompare({
|
||||
basic = '000000',
|
||||
compare = '000000',
|
||||
mode = 'absolute',
|
||||
}: CompareMethodProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const defaultCompareResult: RGBDifference = useMemo(
|
||||
() => new RGBDifference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
|
||||
[],
|
||||
);
|
||||
const differ = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return defaultCompareResult;
|
||||
}
|
||||
try {
|
||||
const diffFn = {
|
||||
absolute: colorFn.differ_in_rgb,
|
||||
relative: colorFn.relative_differ_in_rgb,
|
||||
};
|
||||
const diff = diffFn[mode](basic, compare);
|
||||
return {
|
||||
r: {
|
||||
delta: Math.round(diff.r.delta * 255),
|
||||
percent: diff.r.percent,
|
||||
},
|
||||
g: {
|
||||
delta: Math.round(diff.g.delta * 255),
|
||||
percent: diff.g.percent,
|
||||
},
|
||||
b: {
|
||||
delta: Math.round(diff.b.delta * 255),
|
||||
percent: diff.b.percent,
|
||||
},
|
||||
} as RGBDifference;
|
||||
} catch (e) {
|
||||
console.error('[Compare in RGB]', e);
|
||||
}
|
||||
return defaultCompareResult;
|
||||
}, [basic, compare, mode]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h6>Compare in RGB Space</h6>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>R</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.r.delta}</span>
|
||||
<span>{isNaN(differ.r.percent) ? '0.00' : (differ.r.percent * 100).toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>G</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.g.delta}</span>
|
||||
<span>{isNaN(differ.g.percent) ? '0.00' : (differ.g.percent * 100).toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>B</div>
|
||||
<div className={styles.element_values}>
|
||||
<span>{differ.b.delta}</span>
|
||||
<span>{isNaN(differ.b.percent) ? '0.00' : (differ.b.percent * 100).toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
49
src/page-components/color-compare/ShadeScale.tsx
Normal file
49
src/page-components/color-compare/ShadeScale.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import cx from 'clsx';
|
||||
import { MixReversing } from 'color-module';
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import styles from './CompareLayout.module.css';
|
||||
import { CompareMethodProps } from './share-props';
|
||||
|
||||
export function ShadeScale({ basic = '000000', compare = '000000' }: CompareMethodProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const defaultMixResult: MixReversing = useMemo(() => new MixReversing(0, 0, 0, 0), []);
|
||||
const mixFactors = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return defaultMixResult;
|
||||
}
|
||||
try {
|
||||
const factor = colorFn.shade_scale(basic, compare);
|
||||
return factor;
|
||||
} catch (e) {
|
||||
console.error('[Reversing Tint]', e);
|
||||
}
|
||||
return defaultMixResult;
|
||||
}, [basic, compare]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h6>Compare in RGB Shade.</h6>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>R</div>
|
||||
<div className={styles.element_value}>{(mixFactors.r_factor * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>G</div>
|
||||
<div className={styles.element_value}>{(mixFactors.g_factor * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>B</div>
|
||||
<div className={styles.element_value}>{(mixFactors.b_factor * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={cx(styles.element_name, styles.mean)}>Factor</div>
|
||||
<div className={styles.element_value}>{(mixFactors.average * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
49
src/page-components/color-compare/TintScale.tsx
Normal file
49
src/page-components/color-compare/TintScale.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import cx from 'clsx';
|
||||
import { MixReversing } from 'color-module';
|
||||
import { useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import styles from './CompareLayout.module.css';
|
||||
import { CompareMethodProps } from './share-props';
|
||||
|
||||
export function TintScale({ basic = '000000', compare = '000000' }: CompareMethodProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const defaultMixResult: MixReversing = useMemo(() => new MixReversing(0, 0, 0, 0), []);
|
||||
const mixFactors = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return defaultMixResult;
|
||||
}
|
||||
try {
|
||||
const factor = colorFn.tint_scale(basic, compare);
|
||||
return factor;
|
||||
} catch (e) {
|
||||
console.error('[Reversing Tint]', e);
|
||||
}
|
||||
return defaultMixResult;
|
||||
}, [basic, compare]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h6>Compare in RGB Tint.</h6>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>R</div>
|
||||
<div className={styles.element_value}>{(mixFactors.r_factor * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>G</div>
|
||||
<div className={styles.element_value}>{(mixFactors.g_factor * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className={styles.element}>
|
||||
<div className={styles.element_name}>B</div>
|
||||
<div className={styles.element_value}>{(mixFactors.b_factor * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.elements}>
|
||||
<div className={styles.element}>
|
||||
<div className={cx(styles.element_name, styles.mean)}>Factor</div>
|
||||
<div className={styles.element_value}>{(mixFactors.average * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
5
src/page-components/color-compare/share-props.ts
Normal file
5
src/page-components/color-compare/share-props.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type CompareMethodProps = {
|
||||
basic?: string;
|
||||
compare?: string;
|
||||
mode?: 'absolute' | 'relative';
|
||||
};
|
@@ -23,18 +23,18 @@ export function Darkens({ color, darkens, mix, step, maximum, copyMode }: Darken
|
||||
switch (mix) {
|
||||
case 'progressive':
|
||||
for (let i = 1; i <= darkens; i++) {
|
||||
const darkenColor = colorFn.darken(last(darkenColors), step);
|
||||
const darkenColor = colorFn.darken(last(darkenColors) ?? '', step ?? 0);
|
||||
darkenColors.push(darkenColor);
|
||||
}
|
||||
break;
|
||||
case 'linear':
|
||||
for (let i = 1; i <= darkens; i++) {
|
||||
const darkenColor = colorFn.darken(color, step * i);
|
||||
const darkenColor = colorFn.darken(color, (step ?? 0) * i);
|
||||
darkenColors.push(darkenColor);
|
||||
}
|
||||
break;
|
||||
case 'average': {
|
||||
const interval = maximum / darkens / 100;
|
||||
const interval = (maximum ?? 0) / darkens / 100;
|
||||
for (let i = 1; i <= darkens; i++) {
|
||||
const darkenColor = colorFn.darken(color, interval * i);
|
||||
darkenColors.push(darkenColor);
|
||||
|
@@ -23,18 +23,18 @@ export function Lightens({ color, lightens, mix, step, maximum, copyMode }: Ligh
|
||||
switch (mix) {
|
||||
case 'progressive':
|
||||
for (let i = 1; i <= lightens; i++) {
|
||||
const lightenColor = colorFn.lighten(last(lightenColors), step);
|
||||
const lightenColor = colorFn.lighten(last(lightenColors) ?? '', step ?? 0);
|
||||
lightenColors.push(lightenColor);
|
||||
}
|
||||
break;
|
||||
case 'linear':
|
||||
for (let i = 1; i <= lightens; i++) {
|
||||
const lightenColor = colorFn.lighten(color, step * i);
|
||||
const lightenColor = colorFn.lighten(color, (step ?? 0) * i);
|
||||
lightenColors.push(lightenColor);
|
||||
}
|
||||
break;
|
||||
case 'average': {
|
||||
const interval = maximum / lightens / 100;
|
||||
const interval = (maximum ?? 0) / lightens / 100;
|
||||
for (let i = 1; i <= lightens; i++) {
|
||||
const lightenColor = colorFn.lighten(color, interval * i);
|
||||
lightenColors.push(lightenColor);
|
||||
|
@@ -58,9 +58,9 @@ export function NavigationMenu() {
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/chroma"
|
||||
to="/palette"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Chromatography
|
||||
Auto Palette
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
@@ -70,6 +70,13 @@ export function NavigationMenu() {
|
||||
WACG Check
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/compare"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Compare
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/cards"
|
||||
|
13
src/page-components/scheme/ColorEntry.module.css
Normal file
13
src/page-components/scheme/ColorEntry.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@layer components {
|
||||
.delete_btn {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-yuebai);
|
||||
background-color: oklch(from var(--color-danger) l c h / 0.25);
|
||||
&:hover {
|
||||
background-color: oklch(from var(--color-danger-hover) l c h / 0.65);
|
||||
}
|
||||
&:active {
|
||||
background-color: oklch(from var(--color-danger-active) l c h / 0.65);
|
||||
}
|
||||
}
|
||||
}
|
38
src/page-components/scheme/ColorEntry.tsx
Normal file
38
src/page-components/scheme/ColorEntry.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ActionIcon } from '../../components/ActionIcon';
|
||||
import { FloatColorPicker } from '../../components/FloatColorPicker';
|
||||
import styles from './colorEntry.module.css';
|
||||
|
||||
export type IdenticalColorEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type ColorEntryProps = {
|
||||
entry: IdenticalColorEntry;
|
||||
onDelete?: (index: string) => void;
|
||||
};
|
||||
|
||||
export function ColorEntry({ entry, onDelete }: ColorEntryProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="input_wrapper">
|
||||
<input type="text" name={`name_${entry.id}`} defaultValue={entry.name} />
|
||||
</div>
|
||||
<div>
|
||||
<FloatColorPicker
|
||||
name={`color_${entry.id}`}
|
||||
color={isEmpty(entry.color) ? undefined : entry.color}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ActionIcon
|
||||
icon="tabler:trash"
|
||||
extendClassName={styles.delete_btn}
|
||||
onClick={() => onDelete?.(entry.id)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
6
src/page-components/scheme/CorruptedScheme.module.css
Normal file
6
src/page-components/scheme/CorruptedScheme.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@layer pages {
|
||||
.corrupted {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
8
src/page-components/scheme/CorruptedScheme.tsx
Normal file
8
src/page-components/scheme/CorruptedScheme.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import styles from './CorruptedScheme.module.css';
|
||||
export function CorruptedScheme() {
|
||||
return (
|
||||
<div className="center">
|
||||
<div className={styles.corrupted}>Unrecognizable or corrupted scheme</div>
|
||||
</div>
|
||||
);
|
||||
}
|
16
src/page-components/scheme/Export.module.css
Normal file
16
src/page-components/scheme/Export.module.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@layer pages {
|
||||
.export_layout {
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
.tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
}
|
50
src/page-components/scheme/Export.tsx
Normal file
50
src/page-components/scheme/Export.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { HSegmentedControl } from '../../components/HSegmentedControl';
|
||||
import { Labeled } from '../../components/Labeled';
|
||||
import { ScrollArea } from '../../components/ScrollArea';
|
||||
import { useCopy } from '../../hooks/useCopy';
|
||||
import { Option, SchemeContent, SchemeStorage } from '../../models';
|
||||
import styles from './Export.module.css';
|
||||
|
||||
const exportOptions: Option[] = [
|
||||
{ label: 'CSS', value: 'css' },
|
||||
{ label: 'SCSS', value: 'scss' },
|
||||
{ label: 'Javascript Object', value: 'js_object' },
|
||||
];
|
||||
|
||||
type SchemeExportProps = {
|
||||
scheme: SchemeContent<SchemeStorage>;
|
||||
};
|
||||
|
||||
export function SchemeExport({ scheme }: SchemeExportProps) {
|
||||
const [activeExport, setActiveExport] = useState<Option['value']>(exportOptions[0].value);
|
||||
const exportContent = useMemo(() => {
|
||||
switch (activeExport) {
|
||||
case 'css':
|
||||
return scheme.schemeStorage.cssVariables;
|
||||
case 'scss':
|
||||
return scheme.schemeStorage.scssVariables;
|
||||
case 'js_object':
|
||||
return scheme.schemeStorage.jsVariables;
|
||||
}
|
||||
}, [scheme, activeExport]);
|
||||
const copyToClipboard = useCopy();
|
||||
|
||||
return (
|
||||
<ScrollArea enableY>
|
||||
<div className={styles.export_layout}>
|
||||
<div className={styles.tools}>
|
||||
<Labeled label="Export Options" inline>
|
||||
<HSegmentedControl
|
||||
options={exportOptions}
|
||||
value={activeExport}
|
||||
onChange={setActiveExport}
|
||||
/>
|
||||
</Labeled>
|
||||
<button onClick={() => copyToClipboard(exportContent)}>Copy</button>
|
||||
</div>
|
||||
<pre>{exportContent}</pre>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/M2Scheme.tsx
Normal file
35
src/page-components/scheme/M2Scheme.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { Tab } from '../../components/Tab';
|
||||
import { MaterialDesign2SchemeStorage } from '../../material-2-scheme';
|
||||
import { SchemeContent } from '../../models';
|
||||
import { SchemeExport } from './Export';
|
||||
import { M2SchemeBuilder } from './m2-scheme/Builder';
|
||||
import { M2SchemePreview } from './m2-scheme/Preview';
|
||||
|
||||
const tabOptions = [
|
||||
{ title: 'Overview', id: 'overview' },
|
||||
{ title: 'Builder', id: 'builder' },
|
||||
{ title: 'Exports', id: 'export' },
|
||||
];
|
||||
|
||||
type M2SchemeProps = {
|
||||
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
|
||||
};
|
||||
|
||||
export function M2Scheme({ scheme }: M2SchemeProps) {
|
||||
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
|
||||
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
|
||||
{isEqual(activeTab, 'overview') && <M2SchemePreview scheme={scheme} />}
|
||||
{isEqual(activeTab, 'builder') && (
|
||||
<M2SchemeBuilder scheme={scheme} onBuildComplete={() => setActiveTab('overview')} />
|
||||
)}
|
||||
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
|
||||
</>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/M3Scheme.tsx
Normal file
35
src/page-components/scheme/M3Scheme.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { Tab } from '../../components/Tab';
|
||||
import { MaterialDesign3SchemeStorage } from '../../material-3-scheme';
|
||||
import { SchemeContent } from '../../models';
|
||||
import { SchemeExport } from './Export';
|
||||
import { M3SchemeBuilder } from './m3-scheme/Builder';
|
||||
import { M3SchemePreview } from './m3-scheme/Preview';
|
||||
|
||||
const tabOptions = [
|
||||
{ title: 'Overview', id: 'overview' },
|
||||
{ title: 'Builder', id: 'builder' },
|
||||
{ title: 'Exports', id: 'export' },
|
||||
];
|
||||
|
||||
type M3SchemeProps = {
|
||||
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
|
||||
};
|
||||
|
||||
export function M3Scheme({ scheme }: M3SchemeProps) {
|
||||
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
|
||||
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
|
||||
{isEqual(activeTab, 'overview') && <M3SchemePreview scheme={scheme} />}
|
||||
{isEqual(activeTab, 'builder') && (
|
||||
<M3SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
|
||||
)}
|
||||
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
|
||||
</>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user