Compare commits

..

90 Commits

Author SHA1 Message Date
徐涛
2ddffe1b12 调整涉及WASM内结构体的实例化位置。 2025-02-10 16:43:30 +08:00
徐涛
3eae8116e7 更改WASM的引入方式。 2025-02-10 16:17:59 +08:00
徐涛
12e76b658e 调整首页描述。 2025-02-10 14:47:06 +08:00
徐涛
2ec3578e1c 修复或屏蔽编译错误。 2025-02-10 14:42:17 +08:00
徐涛
f944d48e1b 增加部分结构的构造函数。 2025-02-10 14:31:32 +08:00
徐涛
88e3d1f928 修正大部分的编译错误。 2025-02-10 14:28:34 +08:00
徐涛
2144cd548a 修正Vite设置。 2025-02-10 12:22:43 +08:00
徐涛
2c47369772 基本完成M2 Scheme的预览。 2025-02-10 09:51:15 +08:00
徐涛
2f51a80c91 调整M2 Scheme中Surface颜色的生成。 2025-02-10 09:39:39 +08:00
徐涛
546ca97b10 基本完成M2 Scheme的构建功能。 2025-02-10 08:38:40 +08:00
徐涛
6b262f536d 调整M3 Scheme部分内容的预览展示。 2025-02-08 17:17:56 +08:00
徐涛
6aa3875919 修正一处书写错误。 2025-02-08 16:43:32 +08:00
徐涛
0369f238f2 将M3 Scheme的颜色生成从Cam16Jch重构为Lch。 2025-02-08 16:38:51 +08:00
徐涛
c60aefaaff 微调M3 Scheme生成参数。 2025-02-08 15:28:03 +08:00
徐涛
7bfe9a7620 基本完成M3 Scheme的生成功能。 2025-02-08 15:17:04 +08:00
徐涛
1b044c66d7 调整M3 Scheme预览中色块的布局。 2025-02-08 15:16:18 +08:00
徐涛
e74ffc9721 基本完成M3 Scheme的预览页面。 2025-02-08 15:10:56 +08:00
徐涛
1553c51621 完成M3 Scheme构建页面的功能。 2025-02-08 13:54:12 +08:00
徐涛
14d775e956 修正M3 Scheme的生成算法。 2025-02-08 13:49:58 +08:00
徐涛
e2806a0cc5 修正Cam16Jch到sRGB的转换算法。 2025-02-08 13:34:55 +08:00
徐涛
5d3fc2903b 提取用来录入自定义颜色的公共ColorEntry组件。 2025-02-08 10:11:02 +08:00
徐涛
71feeb4efc 放置默认的M3 Scheme和M2 Scheme的主要切换页面。 2025-02-07 22:59:35 +08:00
徐涛
2b9547a7c2 完成SchemeDetail接受Scheme内容功能。 2025-02-07 22:53:03 +08:00
徐涛
131c43c5cf 修正Swatch Scheme在生成包含原始颜色时的定位错乱问题。 2025-02-07 22:52:01 +08:00
徐涛
505af1c67e 修正是否包含Primary颜色的表单控制。 2025-02-07 22:27:06 +08:00
徐涛
320b750834 完成Swatch Scheme的构建基本功能。 2025-02-07 22:22:33 +08:00
徐涛
6728ca1be2 提升导出页面组件为公共组件。 2025-02-07 22:22:03 +08:00
徐涛
83dcb3f80f 调整Swatch Scheme的部分类型定义。 2025-02-07 17:29:41 +08:00
徐涛
b8018e323d Swatch Scheme的默认亮度参数改为0-1之间的值。 2025-02-07 17:25:21 +08:00
徐涛
d68ac6a3df 增加将Map转换成Object的工具函数。 2025-02-07 17:22:31 +08:00
徐涛
0f5805bb7f 增加Scheme存储对于无效Scheme记录的过滤。 2025-02-07 16:18:22 +08:00
徐涛
a3fb9b656b 增加过滤记录中的空白值。 2025-02-07 16:11:15 +08:00
徐涛
d817024bf3 向Switch组件增加表单可用功能。 2025-02-07 15:07:30 +08:00
徐涛
d98e3a69d9 增加导出Swatch Scheme默认设置的功能。 2025-02-07 14:44:24 +08:00
徐涛
8b0e9699c7 调整Q Scheme Builder中Grid布局中内容的对齐方式。 2025-02-07 13:25:29 +08:00
徐涛
8e71d3c555 增加Swatch Scheme中必要结构体的导出函数。 2025-02-07 13:06:50 +08:00
徐涛
e9c2d4cb16 完成Q Scheme的导出复制功能。 2025-02-07 09:56:40 +08:00
徐涛
2acb69da20 增加pre元素的默认样式。 2025-02-07 09:56:28 +08:00
徐涛
9664983b5c 增加复制文字内容到剪贴板的功能。 2025-02-07 09:56:17 +08:00
徐涛
fc340f3f74 调整WACG适配算法。 2025-02-07 09:30:39 +08:00
徐涛
f9f855e818 调整Q Scheme构造组件对于已记录值的获取和处理。 2025-02-07 09:01:26 +08:00
徐涛
853b9b6b75 调整Q Scheme设置参数的保存类型。 2025-02-07 09:00:53 +08:00
徐涛
41788c4944 改进枚举内容的序列化输出。 2025-02-07 09:00:06 +08:00
徐涛
2bc250fc3d 去掉调试信息,改进结构体序列化方法。 2025-02-07 08:28:50 +08:00
徐涛
7468e28928 调整Q Scheme颜色单元格预览内容格式。 2025-02-07 08:18:21 +08:00
徐涛
ca83ce082b 修改默认导出函数的名称。 2025-02-07 08:14:38 +08:00
徐涛
89b2a2f9d9 增加将Q Scheme配置转换为普通JS对象的方法。 2025-02-07 08:12:45 +08:00
徐涛
74dd9e7354 完成Q Scheme预览功能。 2025-02-07 06:23:43 +08:00
徐涛
592244911f 综合调整Q Scheme的生成算法。 2025-02-07 06:23:16 +08:00
徐涛
08fabb53a2 调整Q Scheme暗色模式前景色和背景色的生成,以及颜色调整系数的使用。 2025-02-06 17:26:45 +08:00
徐涛
0350380df6 Tab增加被动切换标签页的功能。 2025-02-06 16:41:12 +08:00
徐涛
32d8457802 Q Scheme Builder增加用于通知Scheme构建完成的事件。 2025-02-06 14:31:22 +08:00
徐涛
b124bb4eda 基本完成Q Scheme的Builder功能。 2025-02-06 11:21:51 +08:00
徐涛
92229b0de4 增加对于生成的导出变量的记录。 2025-02-06 11:21:27 +08:00
徐涛
e3642cad97 修正wacg自适应推断算法为双向修正策略,以避免指定条件无法满足时死循环的发生。 2025-02-06 10:45:47 +08:00
徐涛
59519e1408 增加web_sys库的支持。 2025-02-06 10:44:34 +08:00
徐涛
56a4786675 增加构建QScheme设置的构造函数。 2025-02-06 06:19:15 +08:00
徐涛
838f0c0fa0 增加对于默认表单值的处理。 2025-02-05 21:40:53 +08:00
徐涛
9fa05824d2 调整浮动ColorPicker的z-index。 2025-01-27 09:42:22 +08:00
徐涛
14851c8284 增加尚未选择颜色的提示。 2025-01-27 08:08:43 +08:00
徐涛
50646ffccf 调整Picker中颜色预览块的大小。 2025-01-27 07:49:40 +08:00
徐涛
a9ad4dea5d 增加一个悬浮Color Picker组件 2025-01-27 07:48:21 +08:00
徐涛
7b26c95a9a 修复ActionIcon可能会被当作表单提交按钮的问题。 2025-01-27 07:34:50 +08:00
徐涛
8efb3ec318 将枚举换成整型值的输出。 2025-01-25 10:57:43 +08:00
徐涛
c4f703906e 增加获取Q Scheme默认配置功能。 2025-01-25 09:32:13 +08:00
徐涛
6dba92a2c5 修正SegmentControl解析Map格式的默认值。 2025-01-25 09:22:17 +08:00
徐涛
4b4428fd3b 增强SegmentControl系列组件对于Map的支持。 2025-01-25 08:56:57 +08:00
徐涛
1b41fb4d22 改进输入框包装器的样式。 2025-01-24 17:18:32 +08:00
徐涛
a3de0f961a 调整存储的Scheme内容。 2025-01-24 16:14:37 +08:00
徐涛
79794ed0f7 调整Scheme详细页面的布局。 2025-01-24 15:18:45 +08:00
徐涛
20757a789a 完善不可识别Scheme的警告页面。 2025-01-24 15:17:07 +08:00
徐涛
f9f984a1b4 重构Scheme编辑器结构。 2025-01-24 15:09:25 +08:00
徐涛
9606106c45 增加自定义Scheme的激活和删除功能。 2025-01-24 14:53:43 +08:00
徐涛
3882ae764f 增加ActionIcon组件。 2025-01-24 11:26:25 +08:00
徐涛
2b17a5de0f 重构对于Scheme类型的展示。 2025-01-24 10:48:36 +08:00
徐涛
b2357811b6 缩小Badge组件的留白尺寸。 2025-01-24 10:46:10 +08:00
徐涛
32273256c0 调整Badge圆角大小。 2025-01-24 10:30:04 +08:00
徐涛
7e2132662f 增加Badge组件。 2025-01-24 10:09:14 +08:00
徐涛
cff2ad0439 重构创建Scheme功能支持多种Scheme类型选择。 2025-01-24 09:11:00 +08:00
徐涛
dc411987bf 增加选择器对于额外配置样式的支持。 2025-01-24 09:00:23 +08:00
徐涛
26ebc3c7e3 更新前端Scheme类型定义。 2025-01-23 16:54:08 +08:00
徐涛
ab4e0b440c 修正一处拼写错误。 2025-01-23 16:02:46 +08:00
徐涛
e0d35d279f 与WASM统一Scheme定义。 2025-01-23 15:37:17 +08:00
徐涛
2a4fb7f043 更新WASM包。 2025-01-23 09:42:57 +08:00
徐涛
2d91f45809 增加Swatch Scheme主题的生成和导出。 2025-01-23 09:34:34 +08:00
徐涛
3ad637e1fa 改变QScheme设置内容的导出方式。 2025-01-22 16:15:29 +08:00
徐涛
c78a2f0183 更新重新优化的WASM包。 2025-01-22 15:32:11 +08:00
徐涛
4a9dfbb664 分拆根文件中的功能函数。 2025-01-22 15:29:27 +08:00
徐涛
86ecb5a258 增加QScheme主题样式的生成与导出。 2025-01-22 14:59:40 +08:00
徐涛
826e526ba2 调整子模块的可访问性。 2025-01-20 13:23:37 +08:00
130 changed files with 5319 additions and 2951 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -8,14 +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"

View File

@@ -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

View 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,
))
}

View File

@@ -1,7 +1,8 @@
use std::sync::LazyLock;
use std::{str::FromStr, sync::LazyLock};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, EnumString};
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorDescription {
@@ -87,3 +88,45 @@ impl Category {
}
}
}
#[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())
}

View File

@@ -12,6 +12,18 @@ pub struct HctDiffference {
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;

View File

@@ -12,6 +12,18 @@ pub struct HSLDifference {
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;

View File

@@ -13,6 +13,14 @@ pub struct Differ {
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;

View File

@@ -12,6 +12,18 @@ pub struct OklchDifference {
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;

View File

@@ -12,6 +12,14 @@ pub struct RGBDifference {
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;

View 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>()))
}

View File

@@ -1,30 +1,64 @@
use palette::{
cam16::{Cam16Jch, Parameters},
convert::FromColorUnclamped,
Hsl, IsWithinBounds, Srgb,
luma::Luma,
Hsl, IntoColor, IsWithinBounds, Lch, Lchuv, Oklab, Oklch, Srgb,
};
#[allow(dead_code)]
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
let mut new_original = Cam16Jch::new(origin.lightness, origin.chroma, origin.hue);
const FACTOR: f32 = 0.99;
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_srgb =
Srgb::from_color_unclamped(new_original.into_xyz(Parameters::default_static_wp(40.0)));
if new_srgb.is_within_bounds() {
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;
}
new_original = Cam16Jch::new(
new_original.lightness,
new_original.chroma * FACTOR,
new_original.hue,
);
}
}
#[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;
@@ -44,3 +78,50 @@ pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb {
pub fn map_hsl_to_srgb_hex(origin: &Hsl) -> String {
format!("{:x}", map_hsl_to_srgb(origin).into_format::<u8>())
}
pub fn map_oklch_to_luma(origin: &Oklch) -> Luma {
let lab_color: Oklab = (*origin).into_color();
let linear_rgb = Srgb::from_linear(lab_color.into_color());
let luma_color: Luma = linear_rgb.into_color();
luma_color
}
pub fn map_oklch_to_srgb(origin: &Oklch) -> Srgb {
Srgb::from_linear::<f32>((*origin).into_color())
}
pub fn map_oklch_to_srgb_hex(origin: &Oklch) -> String {
format!("{:x}", map_oklch_to_srgb(origin).into_format::<u8>())
}
#[macro_export]
macro_rules! parse_to_oklch {
($origin: ident) => {
palette::Oklch::from_color(
palette::Srgb::from_str($origin)
.map_err(|_| crate::errors::ColorError::UnrecogniazedRGB($origin.to_string()))?
.into_format::<f32>(),
)
};
($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()?
};
}

View File

@@ -1,24 +1,24 @@
use std::{str::FromStr, sync::Arc};
use std::str::FromStr;
use color_card::Category;
use color_differ::ColorDifference;
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 strum::IntoEnumIterator;
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> {
@@ -125,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)
@@ -220,389 +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())
}
#[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_card::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())
}
#[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,
))
}

View File

@@ -11,6 +11,19 @@ pub struct MixReversing {
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 {

View File

@@ -45,18 +45,8 @@ impl M2BaselineColors {
None,
)?,
error: M2ColorSet::from_swatch(&error_swatch, &neutral_swatch, dark_baseline, None)?,
background: M2ColorSet::from_swatch(
&neutral_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
surface: M2ColorSet::from_swatch(
&neutral_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
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,

View File

@@ -48,6 +48,27 @@ impl M2ColorSet {
}
}
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();

View File

@@ -6,9 +6,9 @@ use crate::{convert::map_hsl_to_srgb_hex, errors};
use super::SchemeExport;
pub mod baseline;
pub mod color_set;
pub mod swatch;
mod baseline;
mod color_set;
mod swatch;
#[derive(Debug, Clone, Serialize)]
pub struct MaterialDesign2Scheme {

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex;
use crate::convert::map_lch_to_srgb_hex;
use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette};
@@ -16,7 +16,7 @@ pub struct M3BaselineColors {
pub outline: String,
pub outline_variant: String,
pub scrim: String,
pub shadown: String,
pub shadow: String,
pub customs: HashMap<String, M3ColorSet>,
dark_set: bool,
}
@@ -62,10 +62,10 @@ impl M3BaselineColors {
tertiary,
error,
surface,
outline: map_cam16jch_to_srgb_hex(&outline),
outline_variant: map_cam16jch_to_srgb_hex(&outline_variant),
scrim: map_cam16jch_to_srgb_hex(&scrim),
shadown: map_cam16jch_to_srgb_hex(&shadow),
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,
}
@@ -95,7 +95,7 @@ impl M3BaselineColors {
prefix, self.outline_variant
));
css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim));
css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadown));
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));
}
@@ -118,7 +118,7 @@ impl M3BaselineColors {
prefix, self.outline_variant
));
scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim));
scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadown));
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));
}
@@ -147,7 +147,7 @@ impl M3BaselineColors {
prefix, self.outline_variant
));
js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim));
js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadown));
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));
}

View File

@@ -1,6 +1,6 @@
use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex;
use crate::convert::map_lch_to_srgb_hex;
use super::tonal_palette::TonalPalette;
@@ -9,7 +9,7 @@ pub struct M3ColorSet {
pub root: String,
pub on_root: String,
pub container: String,
pub on_conatiner: String,
pub on_container: String,
pub fixed: String,
pub fixed_dim: String,
pub on_fixed: String,
@@ -30,15 +30,15 @@ impl M3ColorSet {
let inverse = palette.tone(80.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
on_root: map_cam16jch_to_srgb_hex(&on_root),
container: map_cam16jch_to_srgb_hex(&container),
on_conatiner: map_cam16jch_to_srgb_hex(&on_container),
fixed: map_cam16jch_to_srgb_hex(&fixed),
fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim),
on_fixed: map_cam16jch_to_srgb_hex(&on_fixed),
fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
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),
}
}
@@ -54,15 +54,15 @@ impl M3ColorSet {
let inverse = palette.tone(40.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
on_root: map_cam16jch_to_srgb_hex(&on_root),
container: map_cam16jch_to_srgb_hex(&container),
on_conatiner: map_cam16jch_to_srgb_hex(&on_container),
fixed: map_cam16jch_to_srgb_hex(&fixed),
fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim),
on_fixed: map_cam16jch_to_srgb_hex(&on_fixed),
fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
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),
}
}
@@ -80,7 +80,7 @@ impl M3ColorSet {
));
variable_lines.push(format!(
"--color-{}-on-{}-container: #{};",
prefix, name, self.on_conatiner
prefix, name, self.on_container
));
variable_lines.push(format!(
"--color-{}-{}-fixed: #{};",
@@ -117,7 +117,7 @@ impl M3ColorSet {
));
variable_lines.push(format!(
"$color-{}-on-{}-container: #{};",
prefix, name, self.on_conatiner
prefix, name, self.on_container
));
variable_lines.push(format!(
"$color-{}-{}-fixed: #{};",
@@ -162,7 +162,7 @@ impl M3ColorSet {
));
variable_lines.push(format!(
"{}On{}Container: '#{}',",
prefix, name, self.on_conatiner
prefix, name, self.on_container
));
variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed));
variable_lines.push(format!(

View File

@@ -1,12 +1,11 @@
use std::str::FromStr;
use baseline::M3BaselineColors;
use palette::cam16::{Cam16Jch, Parameters};
use palette::{IntoColor, Srgb};
use palette::{IntoColor, Lch, Srgb};
use serde::Serialize;
use tonal_palette::TonalPalette;
use crate::convert::map_cam16jch_to_srgb_hex;
use crate::convert::map_lch_to_srgb_hex;
use crate::errors;
use super::SchemeExport;
@@ -26,20 +25,14 @@ pub struct MaterialDesign3Scheme {
impl MaterialDesign3Scheme {
pub fn new(source_color: &str, error_color: &str) -> Result<Self, errors::ColorError> {
let source = Cam16Jch::from_xyz(
Srgb::from_str(source_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let error = Cam16Jch::from_xyz(
Srgb::from_str(error_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
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);
@@ -49,8 +42,8 @@ impl MaterialDesign3Scheme {
let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0);
Ok(Self {
white: map_cam16jch_to_srgb_hex(&Cam16Jch::new(100.0, 0.0, 0.0)),
black: map_cam16jch_to_srgb_hex(&Cam16Jch::new(0.0, 0.0, 0.0)),
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),
})
@@ -61,13 +54,10 @@ impl MaterialDesign3Scheme {
name: String,
color: String,
) -> Result<(), errors::ColorError> {
let custom_color = Cam16Jch::from_xyz(
Srgb::from_str(&color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
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);
@@ -103,8 +93,8 @@ impl SchemeExport for MaterialDesign3Scheme {
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(format!(" white: '#{}',", self.white));
js_object.push(format!(" black: '#{}',", self.black));
js_object.push(" light: {".to_string());
js_object.extend(
self.light_baseline
@@ -125,6 +115,6 @@ impl SchemeExport for MaterialDesign3Scheme {
js_object.push(" },".to_string());
js_object.push("}".to_string());
js_object.join(",\n")
js_object.join("\n")
}
}

View File

@@ -1,6 +1,6 @@
use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex;
use crate::convert::map_lch_to_srgb_hex;
use super::tonal_palette::TonalPalette;
@@ -36,18 +36,18 @@ impl M3SurfaceSet {
let on_inverse = neutral_variant.tone(95.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
dim: map_cam16jch_to_srgb_hex(&dim),
bright: map_cam16jch_to_srgb_hex(&bright),
container: map_cam16jch_to_srgb_hex(&container),
container_lowest: map_cam16jch_to_srgb_hex(&container_lowest),
container_low: map_cam16jch_to_srgb_hex(&container_low),
container_high: map_cam16jch_to_srgb_hex(&container_high),
container_highest: map_cam16jch_to_srgb_hex(&container_highest),
on_root: map_cam16jch_to_srgb_hex(&on_root),
on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
on_inverse: map_cam16jch_to_srgb_hex(&on_inverse),
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),
}
}
@@ -66,18 +66,18 @@ impl M3SurfaceSet {
let on_inverse = neutral_variant.tone(20.0);
Self {
root: map_cam16jch_to_srgb_hex(&root),
dim: map_cam16jch_to_srgb_hex(&dim),
bright: map_cam16jch_to_srgb_hex(&bright),
container: map_cam16jch_to_srgb_hex(&container),
container_lowest: map_cam16jch_to_srgb_hex(&container_lowest),
container_low: map_cam16jch_to_srgb_hex(&container_low),
container_high: map_cam16jch_to_srgb_hex(&container_high),
container_highest: map_cam16jch_to_srgb_hex(&container_highest),
on_root: map_cam16jch_to_srgb_hex(&on_root),
on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse),
on_inverse: map_cam16jch_to_srgb_hex(&on_inverse),
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),
}
}

View File

@@ -1,15 +1,12 @@
use std::str::FromStr;
use palette::{
cam16::{Cam16Jch, Parameters},
IntoColor, Srgb,
};
use palette::{cam16::Cam16Jch, IntoColor, Lch, Srgb};
use crate::errors;
#[derive(Debug, Clone)]
pub struct TonalPalette {
pub key_color: Cam16Jch<f32>,
pub key_color: Lch,
}
#[inline]
@@ -30,7 +27,7 @@ fn find_max_chroma(cache: &mut Vec<(f32, f32)>, hue: f32, tone: f32) -> f32 {
chroma
}
fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> {
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;
@@ -51,7 +48,7 @@ fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> {
upper_tone = mid_tone;
} else {
if approximately_equal(lower_tone, mid_tone) {
return Cam16Jch::new(lower_tone, chroma, hue);
return Lch::new(lower_tone, chroma, hue);
}
lower_tone = mid_tone;
}
@@ -63,20 +60,17 @@ fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> {
}
}
}
Cam16Jch::new(lower_tone, chroma, hue)
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 = Cam16Jch::from_xyz(
Srgb::from_str(&value)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(value))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let key_color: Lch = Srgb::from_str(&value)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(value))?
.into_format::<f32>()
.into_color();
Ok(TonalPalette { key_color })
}
}
@@ -87,8 +81,8 @@ impl TonalPalette {
TonalPalette { key_color }
}
pub fn tone(&self, tone: f32) -> Cam16Jch<f32> {
let toned_color = Cam16Jch::new(tone, self.key_color.chroma, self.key_color.hue);
pub fn tone(&self, tone: f32) -> Lch {
let toned_color = Lch::new(tone, self.key_color.chroma, self.key_color.hue);
toned_color
}

View File

@@ -2,12 +2,16 @@ 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;
@@ -57,3 +61,84 @@ pub fn generate_material_design_2_scheme(
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_automatically(
primary_color: &str,
danger_color: &str,
success_color: &str,
warning_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
setting: 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)?)
}

View 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()
}
}

View 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()
}
}

View 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()
}

View File

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

View File

@@ -0,0 +1,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)?)
}
}

View 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()
}

View 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)?)
}
}

View 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
}
}

View 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
View 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>()),
])
}

View File

@@ -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>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<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" />
<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>

View File

@@ -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",

View File

@@ -29,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 /> },

View File

@@ -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);

View File

@@ -1,46 +0,0 @@
import { SchemeColor } from './models';
export type ColorSetVariant = {
chroma: number;
lightness: number;
};
export type Settings = {
hover: ColorSetVariant;
active: ColorSetVariant;
focus: ColorSetVariant;
disabled: ColorSetVariant;
foregroundRange: [SchemeColor, SchemeColor];
foregroundGeneration: 'fixed' | 'wacg_atuo';
};
export type SchemeSet = {
primary: SchemeColor;
onPrimary: SchemeColor;
secondary: SchemeColor;
onSecondary: SchemeColor;
accent: SchemeColor;
onAccent: SchemeColor;
neutral: SchemeColor;
onNeutral: SchemeColor;
danger: SchemeColor;
onDanger: SchemeColor;
warning: SchemeColor;
onWarning: SchemeColor;
success: SchemeColor;
onSuccess: SchemeColor;
info: SchemeColor;
onInfo: SchemeColor;
border: SchemeColor;
lightenBorder: SchemeColor;
elevation: SchemeColor;
background: SchemeColor;
onBackground: SchemeColor;
inverseBackground: SchemeColor;
onInverseBackground: SchemeColor;
};
export type ColorQScheme = {
scheme: SchemeSet;
setting: Settings;
};

View File

@@ -1,202 +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 function color_categories(): any;
export function search_color_cards(tag: string, category?: string): any;
export function differ_in_rgb(color: string, other: string): RGBDifference;
export function relative_differ_in_rgb(color: string, other: string): RGBDifference;
export function differ_in_hsl(color: string, other: string): HSLDifference;
export function relative_differ_in_hsl(color: string, other: string): HSLDifference;
export function differ_in_hct(color: string, other: string): HctDiffference;
export function relative_differ_in_hct(color: string, other: string): HctDiffference;
export function differ_in_oklch(color: string, other: string): OklchDifference;
export function relative_differ_in_oklch(color: string, other: string): OklchDifference;
export function tint_scale(basic_color: string, mixed_color: string): MixReversing;
export function shade_scale(basic_color: string, mixed_color: string): MixReversing;
export function generate_palette_from_color(reference_color: string, swatch_amount: number, minimum_lightness: number, maximum_lightness: number, use_reference_color?: boolean, reference_color_bias?: number): (string)[];
export class Differ {
private constructor();
free(): void;
delta: number;
percent: number;
}
export class HSLDifference {
private constructor();
free(): void;
hue: Differ;
saturation: Differ;
lightness: Differ;
}
export class HctDiffference {
private constructor();
free(): void;
hue: Differ;
chroma: Differ;
lightness: Differ;
}
export class MixReversing {
private constructor();
free(): void;
r_factor: number;
g_factor: number;
b_factor: number;
average: number;
}
export class OklchDifference {
private constructor();
free(): void;
hue: Differ;
chroma: Differ;
lightness: Differ;
}
export class RGBDifference {
private constructor();
free(): void;
r: Differ;
g: Differ;
b: Differ;
}
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 color_categories: () => [number, number, number];
readonly search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly tint_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly shade_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly __wbg_differ_free: (a: number, b: number) => void;
readonly __wbg_get_differ_delta: (a: number) => number;
readonly __wbg_set_differ_delta: (a: number, b: number) => void;
readonly __wbg_get_differ_percent: (a: number) => number;
readonly __wbg_set_differ_percent: (a: number, b: number) => void;
readonly __wbg_hsldifference_free: (a: number, b: number) => void;
readonly __wbg_get_hsldifference_hue: (a: number) => number;
readonly __wbg_set_hsldifference_hue: (a: number, b: number) => void;
readonly __wbg_get_hsldifference_saturation: (a: number) => number;
readonly __wbg_set_hsldifference_saturation: (a: number, b: number) => void;
readonly __wbg_get_hsldifference_lightness: (a: number) => number;
readonly __wbg_set_hsldifference_lightness: (a: number, b: number) => void;
readonly __wbg_oklchdifference_free: (a: number, b: number) => void;
readonly __wbg_get_oklchdifference_hue: (a: number) => number;
readonly __wbg_set_oklchdifference_hue: (a: number, b: number) => void;
readonly __wbg_get_oklchdifference_chroma: (a: number) => number;
readonly __wbg_set_oklchdifference_chroma: (a: number, b: number) => void;
readonly __wbg_get_oklchdifference_lightness: (a: number) => number;
readonly __wbg_set_oklchdifference_lightness: (a: number, b: number) => void;
readonly __wbg_mixreversing_free: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_r_factor: (a: number) => number;
readonly __wbg_set_mixreversing_r_factor: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_g_factor: (a: number) => number;
readonly __wbg_set_mixreversing_g_factor: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_b_factor: (a: number) => number;
readonly __wbg_set_mixreversing_b_factor: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_average: (a: number) => number;
readonly __wbg_set_mixreversing_average: (a: number, b: number) => void;
readonly __wbg_hctdiffference_free: (a: number, b: number) => void;
readonly __wbg_get_hctdiffference_hue: (a: number) => number;
readonly __wbg_set_hctdiffference_hue: (a: number, b: number) => void;
readonly __wbg_get_hctdiffference_chroma: (a: number) => number;
readonly __wbg_set_hctdiffference_chroma: (a: number, b: number) => void;
readonly __wbg_get_hctdiffference_lightness: (a: number) => number;
readonly __wbg_set_hctdiffference_lightness: (a: number, b: number) => void;
readonly __wbg_rgbdifference_free: (a: number, b: number) => void;
readonly __wbg_get_rgbdifference_r: (a: number) => number;
readonly __wbg_set_rgbdifference_r: (a: number, b: number) => void;
readonly __wbg_get_rgbdifference_g: (a: number) => number;
readonly __wbg_set_rgbdifference_g: (a: number, b: number) => void;
readonly __wbg_get_rgbdifference_b: (a: number) => number;
readonly __wbg_set_rgbdifference_b: (a: number, b: number) => void;
readonly generate_palette_from_color: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table;
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>;

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +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 color_categories: () => [number, number, number];
export const search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
export const tint_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
export const shade_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
export const __wbg_differ_free: (a: number, b: number) => void;
export const __wbg_get_differ_delta: (a: number) => number;
export const __wbg_set_differ_delta: (a: number, b: number) => void;
export const __wbg_get_differ_percent: (a: number) => number;
export const __wbg_set_differ_percent: (a: number, b: number) => void;
export const __wbg_hsldifference_free: (a: number, b: number) => void;
export const __wbg_get_hsldifference_hue: (a: number) => number;
export const __wbg_set_hsldifference_hue: (a: number, b: number) => void;
export const __wbg_get_hsldifference_saturation: (a: number) => number;
export const __wbg_set_hsldifference_saturation: (a: number, b: number) => void;
export const __wbg_get_hsldifference_lightness: (a: number) => number;
export const __wbg_set_hsldifference_lightness: (a: number, b: number) => void;
export const __wbg_oklchdifference_free: (a: number, b: number) => void;
export const __wbg_get_oklchdifference_hue: (a: number) => number;
export const __wbg_set_oklchdifference_hue: (a: number, b: number) => void;
export const __wbg_get_oklchdifference_chroma: (a: number) => number;
export const __wbg_set_oklchdifference_chroma: (a: number, b: number) => void;
export const __wbg_get_oklchdifference_lightness: (a: number) => number;
export const __wbg_set_oklchdifference_lightness: (a: number, b: number) => void;
export const __wbg_mixreversing_free: (a: number, b: number) => void;
export const __wbg_get_mixreversing_r_factor: (a: number) => number;
export const __wbg_set_mixreversing_r_factor: (a: number, b: number) => void;
export const __wbg_get_mixreversing_g_factor: (a: number) => number;
export const __wbg_set_mixreversing_g_factor: (a: number, b: number) => void;
export const __wbg_get_mixreversing_b_factor: (a: number) => number;
export const __wbg_set_mixreversing_b_factor: (a: number, b: number) => void;
export const __wbg_get_mixreversing_average: (a: number) => number;
export const __wbg_set_mixreversing_average: (a: number, b: number) => void;
export const __wbg_hctdiffference_free: (a: number, b: number) => void;
export const __wbg_get_hctdiffference_hue: (a: number) => number;
export const __wbg_set_hctdiffference_hue: (a: number, b: number) => void;
export const __wbg_get_hctdiffference_chroma: (a: number) => number;
export const __wbg_set_hctdiffference_chroma: (a: number, b: number) => void;
export const __wbg_get_hctdiffference_lightness: (a: number) => number;
export const __wbg_set_hctdiffference_lightness: (a: number, b: number) => void;
export const __wbg_rgbdifference_free: (a: number, b: number) => void;
export const __wbg_get_rgbdifference_r: (a: number) => number;
export const __wbg_set_rgbdifference_r: (a: number, b: number) => void;
export const __wbg_get_rgbdifference_g: (a: number) => number;
export const __wbg_set_rgbdifference_g: (a: number, b: number) => void;
export const __wbg_get_rgbdifference_b: (a: number) => number;
export const __wbg_set_rgbdifference_b: (a: number, b: number) => void;
export const generate_palette_from_color: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export_2: WebAssembly.Table;
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;

View File

@@ -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/*"
]
}

View File

@@ -193,6 +193,7 @@
resize: none;
}
.input_wrapper {
width: fit-content;
display: flex;
flex-direction: row;
align-items: center;
@@ -440,4 +441,12 @@
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;
}
}

View 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 {
}
}
}

View 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>
);
}

View 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
View 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>;
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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));
};

View File

@@ -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;
};

View 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);
}
}
}
}

View 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>
);
}

View File

@@ -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) => (
<div
key={`${index}_${option.value}`}
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)}
ref={(el) => (optionsRef.current[index] = el!)}
onClick={() => handleSelectAction(option.value, index)}>
{option.label}
</div>
))}
{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}_${value}`}
className={cx(styles.option, isEqual(selected, value) && styles.selected)}
//@ts-expect-error TS2322
ref={(el) => (optionsRef.current[index] = el!)}
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>
);
}

View File

@@ -26,7 +26,7 @@ export function LabeledPicker({
}: 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);
};

View File

@@ -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}

View 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);
}
}
}

View 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>
)
);
}

View File

@@ -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,10 +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) => {
const handleWheel = (evt: WheelEvent<HTMLDivElement>) => {
const container = scrollContainerRef?.current;
if (enableY && container) {
const delta = evt.deltaY;
@@ -177,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} />}

View File

@@ -1,15 +1,16 @@
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) {
@@ -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>
);
}

View File

@@ -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) => (

View File

@@ -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

View File

@@ -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) => (
<div
key={`${index}_${option.value}`}
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)}
ref={(el) => (optionsRef.current[index] = el!)}
onClick={() => handleSelectAction(option.value, index)}>
{option.label}
</div>
))}
{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}_${value}`}
className={cx(styles.option, isEqual(selected, value) && styles.selected)}
//@ts-expect-error TS2322
ref={(el) => (optionsRef.current[index] = el!)}
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
View 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;
}

View File

@@ -1,16 +1,37 @@
import { SchemeColor } from './models';
export type SchemeSet = {
primary: SchemeColor;
primaryVariant: SchemeColor;
onPrimary: SchemeColor;
secondary: SchemeColor;
secondaryVariant: SchemeColor;
onSecondary: SchemeColor;
background: SchemeColor;
onBackground: SchemeColor;
surface: SchemeColor;
onSurface: SchemeColor;
error: SchemeColor;
onError: SchemeColor;
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;
};

View File

@@ -1,49 +1,60 @@
import { SchemeColor } from './models';
export type SchemeSet = {
primary: SchemeColor;
onPrimary: SchemeColor;
primaryContainer: SchemeColor;
onPrimaryContainer: SchemeColor;
primaryFixed: SchemeColor;
onPrimaryFixed: SchemeColor;
primaryFixedDim: SchemeColor;
onPrimaryFixedVaraint: SchemeColor;
secondary: SchemeColor;
onSecondary: SchemeColor;
secondaryContainer: SchemeColor;
onSecondaryContainer: SchemeColor;
secondaryFixed: SchemeColor;
onSecondaryFixed: SchemeColor;
secondaryFixedDim: SchemeColor;
onSecondaryFixedVaraint: SchemeColor;
tertiary: SchemeColor;
onTertiary: SchemeColor;
tertiaryContainer: SchemeColor;
onTertiaryContainer: SchemeColor;
tertiaryFixed: SchemeColor;
onTertiaryFixed: SchemeColor;
tertiaryFixedDim: SchemeColor;
onTertiaryFixedVaraint: SchemeColor;
error: SchemeColor;
onError: SchemeColor;
errorContainer: SchemeColor;
onErrorContainer: SchemeColor;
surface: SchemeColor;
surfaceDim: SchemeColor;
surfaceBright: SchemeColor;
onSurface: SchemeColor;
onSurfaceVariant: SchemeColor;
surfaceContainerLowest: SchemeColor;
surfaceContainerLow: SchemeColor;
surfaceContainer: SchemeColor;
surfaceContainerHigh: SchemeColor;
surfaceContainerHighest: SchemeColor;
inverseSurface: SchemeColor;
onInverseSurface: SchemeColor;
inversePrimary: SchemeColor;
outline: SchemeColor;
outlineVariant: SchemeColor;
scrim: SchemeColor;
shadow: SchemeColor;
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;
};

View File

@@ -1,7 +1,15 @@
export type Option = {
label: string;
value: string | number | null;
};
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: T;
}
| Record<'label' | 'value', T>;
export type HarmonyColor = {
color: string;
@@ -21,14 +29,47 @@ export type ColorDescription = {
oklch: [number, number, number];
};
export type SchemeType = 'color_q' | 'material_2' | 'material_3';
export type SchemeColor = string | null;
export type SchemeContent<Scheme> = {
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;
lightScheme: Scheme;
darkScheme: Scheme;
schemeStorage: SchemeStorage;
};
export type ColorShifting = {
chroma: number;
lightness: number;
};
export type SchemeStorage =
| QSchemeStorage
| SwatchSchemeStorage
| MaterialDesign2SchemeStorage
| MaterialDesign3SchemeStorage;

View File

@@ -1,30 +1,19 @@
import { Differ, HctDiffference } from 'color-module';
import { useMemo } from 'react';
import { HctDiffference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props';
const defaultCompareResult: HctDiffference = {
hue: {
delta: 0,
percent: 0,
},
chroma: {
delta: 0,
percent: 0,
},
lightness: {
delta: 0,
percent: 0,
},
};
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;

View File

@@ -1,30 +1,19 @@
import { Differ, HSLDifference } from 'color-module';
import { useMemo } from 'react';
import { HSLDifference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props';
const defaultCompareResult: HSLDifference = {
hue: {
delta: 0,
percent: 0,
},
saturation: {
delta: 0,
percent: 0,
},
lightness: {
delta: 0,
percent: 0,
},
};
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;

View File

@@ -1,30 +1,19 @@
import { Differ, OklchDifference } from 'color-module';
import { useMemo } from 'react';
import { OklchDifference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props';
const defaultCompareResult: OklchDifference = {
hue: {
delta: 0,
percent: 0,
},
chroma: {
delta: 0,
percent: 0,
},
lightness: {
delta: 0,
percent: 0,
},
};
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;

View File

@@ -1,30 +1,19 @@
import { Differ, RGBDifference } from 'color-module';
import { useMemo } from 'react';
import { RGBDifference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props';
const defaultCompareResult: RGBDifference = {
r: {
delta: 0,
percent: 0,
},
g: {
delta: 0,
percent: 0,
},
b: {
delta: 0,
percent: 0,
},
};
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;

View File

@@ -1,19 +1,13 @@
import cx from 'clsx';
import { MixReversing } from 'color-module';
import { useMemo } from 'react';
import { MixReversing } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props';
const defaultMixResult: MixReversing = {
r_factor: 0,
g_factor: 0,
b_factor: 0,
average: 0,
};
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;

View File

@@ -1,19 +1,13 @@
import cx from 'clsx';
import { MixReversing } from 'color-module';
import { useMemo } from 'react';
import { MixReversing } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props';
const defaultMixResult: MixReversing = {
r_factor: 0,
g_factor: 0,
b_factor: 0,
average: 0,
};
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;

View File

@@ -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);

View File

@@ -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);

View 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);
}
}
}

View 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>
</>
);
}

View File

@@ -0,0 +1,6 @@
@layer pages {
.corrupted {
font-size: var(--font-size-xl);
color: var(--color-danger);
}
}

View 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>
);
}

View 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);
}
}
}

View 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>
);
}

View 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} />}
</>
);
}

View 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} />}
</>
);
}

View File

@@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { SchemeContent } from '../../models';
import { QSchemeStorage } from '../../q-scheme';
import { SchemeExport } from './Export';
import { QSchemeBuilder } from './q-scheme/Builder';
import { QSchemePreview } from './q-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type QSchemeProps = {
scheme: SchemeContent<QSchemeStorage>;
};
export function QScheme({ scheme }: QSchemeProps) {
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') && <QSchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<QSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
}

View File

@@ -1,25 +0,0 @@
@layer pages {
.scheme_content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.series_row {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
h4 {
padding-block: var(--spacing-xs);
font-size: var(--font-size-l);
}
ul {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
gap: var(--spacing-l);
}
}
}

View File

@@ -1,61 +0,0 @@
import { isArray } from 'lodash-es';
import { ColorStand } from '../../components/ColorStand';
import { SchemeSet } from '../../stores/schemes';
import styles from './SchemeContent.module.css';
type ColorSeriesProps = {
title: string;
series: SchemeSet['lightScheme' | 'darkScheme']['primary'];
simpleSeries?: boolean;
};
function ColorSeries({ title, series, simpleSeries = false }: ColorSeriesProps) {
return (
<div className={styles.series_row}>
<h4>{title}</h4>
<ul>
<ColorStand title="Normal" color={series?.normal} />
{simpleSeries ? (
<>
<ColorStand title="Lightness" color={series?.lighten} />
<ColorStand title="Darkness" color={series?.darken} />
</>
) : (
<>
<ColorStand title="Hover" color={series?.hover} />
<ColorStand title="Active" color={series?.active} />
<ColorStand title="Focus" color={series?.focus} />
<ColorStand title="Disabled" color={series?.disabled} />
</>
)}
</ul>
</div>
);
}
type SchemeContentProps = {
scheme: SchemeSet['lightScheme' | 'darkScheme'];
};
export function SchemeContent({ scheme }: SchemeContentProps) {
return (
<div className={styles.scheme_content}>
<ColorSeries title="Foreground Series" series={scheme.foreground} simpleSeries />
<ColorSeries title="Background Series" series={scheme.background} simpleSeries />
<ColorSeries title="Primary Series" series={scheme.primary} />
{isArray(scheme.secondary) ? (
scheme.secondary.map((cSet, index) => (
<ColorSeries title={`Secondary Series ${index + 1}`} series={cSet} />
))
) : (
<ColorSeries title="Secondary Series" series={scheme.secondary} />
)}
<ColorSeries title="Accent Series" series={scheme.accent} />
<ColorSeries title="Neutral Serias" series={scheme.neutral} />
<ColorSeries title="Success Series" series={scheme.success} />
<ColorSeries title="Danger Series" series={scheme.danger} />
<ColorSeries title="Warn Series" series={scheme.warning} />
<ColorSeries title="Info Series" series={scheme.info} />
</div>
);
}

View File

@@ -1,17 +0,0 @@
@layer pages {
.scheme_view_layout {
flex: 1 0;
width: 100%;
padding: calc(var(--spacing) * 4) calc(var(--spacing) * 8);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
overflow: hidden;
}
.preview_switch_container {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-m);
}
}

View File

@@ -1,23 +0,0 @@
import { useState } from 'react';
import { Switch } from '../../components/Switch';
import { SchemeSet } from '../../stores/schemes';
import { SchemeContent } from './SchemeContent';
import styles from './SchemeView.module.css';
type SchemeViewProps = {
scheme: SchemeSet['lightScheme' | 'darkScheme'];
};
export function SchemeView({ scheme }: SchemeViewProps) {
const [enablePreview, setEnablePreview] = useState(false);
return (
<div className={styles.scheme_view_layout}>
<div className={styles.preview_switch_container}>
<span>Preview scheme</span>
<Switch onChange={(checked) => setEnablePreview(checked)} />
</div>
{enablePreview ? <div>SVG Preview</div> : <SchemeContent scheme={scheme} />}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { SchemeContent } from '../../models';
import { SwatchSchemeStorage } from '../../swatch_scheme';
import { SchemeExport } from './Export';
import { SwatchSchemeBuilder } from './swatch-scheme/Builder';
import { SwatchSchemePreview } from './swatch-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type SwatchSchemeProps = {
scheme: SchemeContent<SwatchSchemeStorage>;
};
export function SwatchScheme({ scheme }: SwatchSchemeProps) {
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') && <SwatchSchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<SwatchSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
}

View File

@@ -0,0 +1,46 @@
@layer pages {
.builder_layout {
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.3em;
display: grid;
grid-template-columns: repeat(3, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
.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);
}
}
}
}

View File

@@ -0,0 +1,178 @@
import { includes, isEmpty, isNil, merge } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { MaterialDesign2SchemeStorage } from '../../../material-2-scheme';
import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type M2SchemeBuilderProps = {
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
onBuildComplete?: () => void;
};
export function M2SchemeBuilder({ scheme, onBuildComplete }: M2SchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const originalColors = useMemo(() => {
return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map(
([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry),
);
}, [scheme.schemeStorage.source]);
const [newColors, setNewColors] = useState<IdenticalColorEntry[]>([]);
const [deleted, setDeleted] = useState<string[]>([]);
const addEntryAction = useCallback(() => {
setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]);
}, []);
const colorKeys = useMemo(
() =>
[...originalColors, ...newColors]
.map((color) => color.id)
.filter((c) => !includes(deleted, c)),
[originalColors, newColors, deleted],
);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
try {
const primaryColor = formData.get('primary') as string;
if (isNil(primaryColor) || isEmpty(primaryColor)) {
errMsg.set('primary', 'Primary color is required');
}
const secondaryColor = formData.get('secondary') as string;
if (isNil(secondaryColor) || isEmpty(secondaryColor)) {
errMsg.set('secondary', 'Secondary color is required');
}
const errorColor = formData.get('error') as string;
if (isNil(errorColor) || isEmpty(errorColor)) {
errMsg.set('error', 'Error color is required');
}
if (!isEmpty(errMsg)) return errMsg;
const customColors: Record<string, string> = {};
for (const key of colorKeys) {
const name = formData.get(`name_${key}`) as string;
const color = formData.get(`color_${key}`) as string;
if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue;
customColors[name] = color;
}
const generatedScheme = colorFn?.generate_material_design_2_scheme(
primaryColor,
secondaryColor,
errorColor,
customColors,
);
updateScheme((prev) => {
prev.schemeStorage.source = {
primary: primaryColor,
secondary: secondaryColor,
error: errorColor,
custom_colors: customColors,
};
prev.schemeStorage.scheme = merge(generatedScheme[0], {
light: { custom_colors: mapToObject(generatedScheme[0].light.custom_colors) },
dark: { custom_colors: mapToObject(generatedScheme[0].dark.custom_colors) },
});
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildComplete?.();
} catch (e) {
console.error('[generate m2 scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Required Colors</h5>
<label className={styles.label}>Primary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="primary"
color={
isNil(scheme.schemeStorage.source?.primary) ||
isEmpty(scheme.schemeStorage.source?.primary)
? undefined
: scheme.schemeStorage.source.primary
}
/>
{errMsg.get('primary') && (
<span className={styles.error_msg}>{errMsg.get('primary')}</span>
)}
</div>
<label className={styles.label}>Secondary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="secondary"
color={
isNil(scheme.schemeStorage.source?.secondary) ||
isEmpty(scheme.schemeStorage.source.secondary)
? undefined
: scheme.schemeStorage.source.secondary
}
/>
{errMsg.get('secondary') && (
<span className={styles.error_msg}>{errMsg.get('secondary')}</span>
)}
</div>
<label className={styles.label}>Error Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="error"
color={
isNil(scheme.schemeStorage.source?.error) ||
isEmpty(scheme.schemeStorage.source.error)
? undefined
: scheme.schemeStorage.source.error
}
/>
{errMsg.get('error') && <span className={styles.error_msg}>{errMsg.get('error')}</span>}
</div>
<h5 className={styles.segment_title}>Custom Colors</h5>
<label style={{ gridColumn: 1 }}>Name</label>
<label>Color</label>
<div>
<button type="button" className="small" onClick={addEntryAction}>
Add Color
</button>
</div>
{originalColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{newColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@@ -0,0 +1,41 @@
@layer pages {
.preview_layout {
padding: var(--spacing-s) var(--spacing-m);
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-m);
}
.preview_block {
width: inherit;
padding: var(--spacing-m) var(--spacing-m);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.1em;
h4 {
font-size: var(--font-size-m);
font-weight: bold;
line-height: 1.7em;
}
}
.horizontal_set {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: 0;
}
.color_block {
padding: var(--spacing-s) var(--spacing-xs);
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
.wacg {
font-size: var(--font-size-xxs);
}
}
}

View File

@@ -0,0 +1,94 @@
import { useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { ScrollArea } from '../../../components/ScrollArea';
import { Baseline, ColorSet, MaterialDesign2SchemeStorage } from '../../../material-2-scheme';
import { SchemeContent } from '../../../models';
import styles from './Preview.module.css';
type M2SchemeFunctionColorProps = {
title: string;
color: ColorSet;
};
function M2SchemeFunctionColor({ title, color }: M2SchemeFunctionColorProps) {
const { colorFn } = useColorFunction();
const rootWacgRatio = useMemo(() => {
try {
return colorFn?.wacg_relative_contrast(color.on, color.root) ?? null;
} catch (e) {
console.error('[calc root wacg]', e);
}
return null;
}, [colorFn, color.on, color.root]);
const variantWacgRatio = useMemo(() => {
try {
return colorFn?.wacg_relative_contrast(color.on, color.variant) ?? null;
} catch (e) {
console.error('[calc variant wacg]', e);
}
return null;
}, [colorFn, color.on, color.variant]);
return (
<div className={styles.horizontal_set}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${color.root}`, color: `#${color.on}` }}>
<span>{title}</span>
<span className={styles.wacg}>WACG: {rootWacgRatio?.toFixed(2)}</span>
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${color.variant}`, color: `#${color.on}` }}>
<span>{title} Variant</span>
<span className={styles.wacg}>WACG: {variantWacgRatio?.toFixed(2)}</span>
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${color.on}`, color: `#${color.root}` }}>
On {title}
</div>
</div>
);
}
type PreviewBlockProps = {
title: string;
baseline: Baseline;
};
function PreviewBlock({ title, baseline }: PreviewBlockProps) {
return (
<div
className={styles.preview_block}
style={{
backgroundColor: `#${baseline.background.root}`,
color: `#${baseline.background.on}`,
}}>
<h4>{title} Scheme</h4>
<M2SchemeFunctionColor title="Primary" color={baseline.primary} />
<M2SchemeFunctionColor title="Secondary" color={baseline.secondary} />
<M2SchemeFunctionColor title="Error" color={baseline.error} />
<M2SchemeFunctionColor title="Background" color={baseline.background} />
<M2SchemeFunctionColor title="Surface" color={baseline.surface} />
{Object.entries(baseline.custom_colors).map(([name, set], index) => (
<M2SchemeFunctionColor key={index} title={name} color={set} />
))}
</div>
);
}
type M2SchemePreviewProps = {
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
};
export function M2SchemePreview({ scheme }: M2SchemePreviewProps) {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<PreviewBlock title="Light" baseline={scheme.schemeStorage.scheme!.light} />
<PreviewBlock title="Dark" baseline={scheme.schemeStorage.scheme!.dark} />
</div>
</ScrollArea>
);
}

View File

@@ -0,0 +1,46 @@
@layer pages {
.builder_layout {
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.3em;
display: grid;
grid-template-columns: repeat(3, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
.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);
}
}
}
}

View File

@@ -0,0 +1,165 @@
import { includes, isEmpty, isNil } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { MaterialDesign3Scheme, MaterialDesign3SchemeStorage } from '../../../material-3-scheme';
import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type M3SchemeBuilderProps = {
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
onBuildCompleted?: () => void;
};
export function M3SchemeBuilder({ scheme, onBuildCompleted }: M3SchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const originalColors = useMemo(() => {
return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map(
([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry),
);
}, [scheme.schemeStorage.source]);
const [newColors, setNewColors] = useState<IdenticalColorEntry[]>([]);
const [deleted, setDeleted] = useState<string[]>([]);
const addEntryAction = useCallback(() => {
setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]);
}, []);
const colorKeys = useMemo(
() =>
[...originalColors, ...newColors]
.map((color) => color.id)
.filter((c) => !includes(deleted, c)),
[originalColors, newColors, deleted],
);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
try {
const sourceColor = formData.get('source') as string;
if (isNil(sourceColor) || isEmpty(sourceColor)) {
errMsg.set('source', 'Source color is required');
}
const errorColor = formData.get('error') as string;
if (isNil(errorColor) || isEmpty(errorColor)) {
errMsg.set('error', 'Error color is required');
}
if (!isEmpty(errMsg)) return errMsg;
const customColors: Record<string, string> = {};
for (const key of colorKeys) {
const name = formData.get(`name_${key}`) as string;
const color = formData.get(`color_${key}`) as string;
if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue;
customColors[name] = color;
}
const generatedScheme = colorFn?.generate_material_design_3_scheme(
sourceColor,
errorColor,
customColors,
);
updateScheme((prev) => {
prev.schemeStorage.source = {
source: sourceColor as string,
error: errorColor as string,
custom_colors: customColors,
};
prev.schemeStorage.scheme = {
white: generatedScheme[0].white,
black: generatedScheme[0].black,
light_baseline: {
...generatedScheme[0].light_baseline,
customs: mapToObject(generatedScheme[0].light_baseline.customs),
},
dark_baseline: {
...generatedScheme[0].dark_baseline,
customs: mapToObject(generatedScheme[0].dark_baseline.customs),
},
} as MaterialDesign3Scheme;
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildCompleted?.();
} catch (e) {
console.error('[generate m3 scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Required Colors</h5>
<label className={styles.label}>Source Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="source"
color={
isNil(scheme.schemeStorage.source?.source) ||
isEmpty(scheme.schemeStorage.source?.source)
? undefined
: scheme.schemeStorage.source?.source
}
/>
{errMsg.has('source') && <span className={styles.error_msg}>{errMsg.get('source')}</span>}
</div>
<label className={styles.label}>Error Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="error"
color={
isNil(scheme.schemeStorage.source?.error) ||
isEmpty(scheme.schemeStorage.source.error)
? undefined
: scheme.schemeStorage.source.error
}
/>
{errMsg.has('error') && <span className={styles.error_msg}>{errMsg.get('error')}</span>}
</div>
<h5 className={styles.segment_title}>Custom Colors</h5>
<label style={{ gridColumn: 1 }}>Name</label>
<label>Color</label>
<div>
<button type="button" className="small" onClick={addEntryAction}>
Add Color
</button>
</div>
{originalColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{newColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@@ -0,0 +1,89 @@
@layer pages {
.preview_layout {
padding: var(--spacing-s) var(--spacing-m);
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-m);
}
.preview_block {
width: inherit;
padding: var(--spacing-m) var(--spacing-m);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
font-size: var(--font-size-xs);
line-height: 1.1em;
.main_grid {
display: grid;
grid-template-columns: 3fr 1fr;
gap: var(--spacing-m);
.main_color_sets {
grid-column: 1;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: var(--spacing-xs);
}
.surface_sets {
grid-column: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-xs);
.surface_basics {
flex-grow: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
}
.surface_levels {
flex-grow: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0;
}
.surface_variants {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
}
}
.additional_sets {
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-xs);
.constants {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-s);
}
}
}
h4 {
font-size: var(--font-size-m);
font-weight: bold;
line-height: 1.7em;
}
}
.vertical_set {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
}
.horizontal_set {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: 0;
}
.color_block {
padding: var(--spacing-s) var(--spacing-xs);
flex: 1;
}
}

View File

@@ -0,0 +1,272 @@
import { ScrollArea } from '../../../components/ScrollArea';
import {
Baseline,
ColorSet,
MaterialDesign3SchemeStorage,
Surface,
} from '../../../material-3-scheme';
import { SchemeContent } from '../../../models';
import styles from './Preview.module.css';
type ColorSetProps = {
name: string;
set: ColorSet;
};
function VerticalColorSet({ name, set }: ColorSetProps) {
return (
<div className={styles.vertical_set}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.root}`, color: `#${set.on_root}` }}>
{name}
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.on_root}`, color: `#${set.root}` }}>
On {name}
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.container}`,
color: `#${set.on_container}`,
}}>
{name} Container
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.on_container}`,
color: `#${set.container}`,
}}>
On {name} Container
</div>
</div>
);
}
function HorizontalColorSet({ name, set }: ColorSetProps) {
return (
<div className={styles.horizontal_set}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.root}`, color: `#${set.on_root}` }}>
{name}
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.on_root}`, color: `#${set.root}` }}>
On {name}
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.container}`,
color: `#${set.on_container}`,
}}>
{name} Container
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.on_container}`,
color: `#${set.container}`,
}}>
On {name} Container
</div>
</div>
);
}
type SurfaceColorSetProps = {
surface: Surface;
outline: string;
outlineVariant: string;
};
function SurfaceColorSet({ surface, outline, outlineVariant }: SurfaceColorSetProps) {
return (
<div className={styles.surface_sets}>
<div className={styles.surface_basics}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.dim}`, color: `#${surface.on_root}` }}>
Surface Dim
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.root}`, color: `#${surface.on_root}` }}>
Surface
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.bright}`, color: `#${surface.on_root}` }}>
Surface Bright
</div>
</div>
<div className={styles.surface_levels}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container_lowest}`, color: `#${surface.on_root}` }}>
Surf. Container Lowest
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container_low}`, color: `#${surface.on_root}` }}>
Surf. Container Low
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container}`, color: `#${surface.on_root}` }}>
Surf. Container
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container_high}`, color: `#${surface.on_root}` }}>
Surf. Container High
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${surface.container_highest}`,
color: `#${surface.on_root}`,
}}>
Surf. Container Highest
</div>
</div>
<div className={styles.surface_variants}>
<div
className={styles.color_block}
style={{
backgroundColor: `#${surface.on_root}`,
color: `#${surface.root}`,
}}>
On Surface
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${surface.on_root_variant}`,
color: `#${surface.root}`,
}}>
On Surface Var.
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${outline}`,
color: `#${surface.root}`,
}}>
Outline
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${outlineVariant}`,
color: `#${surface.on_root}`,
}}>
Outline Variant
</div>
</div>
</div>
);
}
type AdditionalColorSetsProps = {
baseline: Baseline;
};
function AdditionalColorSets({ baseline }: AdditionalColorSetsProps) {
return (
<div className={styles.additional_sets}>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.surface.inverse}`,
color: `#${baseline.surface.on_inverse}`,
}}>
Inverse Surface
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.surface.on_inverse}`,
color: `#${baseline.surface.inverse}`,
}}>
Inverse On Surface
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.primary.inverse}`,
color: `#${baseline.surface.on_root}`,
}}>
Inverse Primary
</div>
<div className={styles.constants}>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.scrim}`,
color: `#ffffff`,
}}>
Scrim
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.shadow}`,
color: `#ffffff`,
}}>
Shadow
</div>
</div>
</div>
);
}
type PreviewBlockProps = {
title: string;
baseline: Baseline;
};
function PreviewBlock({ title, baseline }: PreviewBlockProps) {
return (
<div className={styles.preview_block} style={{ backgroundColor: `#${baseline.surface.root}` }}>
<h4 style={{ color: `#${baseline.surface.on_root}` }}>{title}</h4>
<div className={styles.main_grid}>
<div className={styles.main_color_sets}>
<VerticalColorSet name="Primary" set={baseline.primary} />
<VerticalColorSet name="Secondary" set={baseline.secondary} />
<VerticalColorSet name="Tertiary" set={baseline.tertiary} />
</div>
<VerticalColorSet name="Error" set={baseline.error} />
<SurfaceColorSet
surface={baseline.surface}
outline={baseline.outline}
outlineVariant={baseline.outline_variant}
/>
<AdditionalColorSets baseline={baseline} />
</div>
{Object.entries(baseline.customs).map(([name, set], index) => (
<HorizontalColorSet key={index} name={name} set={set} />
))}
</div>
);
}
type M3SchemePreviewProps = {
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
};
export function M3SchemePreview({ scheme }: M3SchemePreviewProps) {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<PreviewBlock title="Light Scheme" baseline={scheme.schemeStorage.scheme!.light_baseline} />
<PreviewBlock title="Dark Scheme" baseline={scheme.schemeStorage.scheme!.dark_baseline} />
</div>
</ScrollArea>
);
}

View File

@@ -0,0 +1,38 @@
@layer pages {
.builder_layout {
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.3em;
display: grid;
grid-template-columns: repeat(4, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.parameter_input {
max-width: 8em;
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
}
}

View File

@@ -0,0 +1,362 @@
import { ColorExpand, ColorShifting, SchemeSetting, WACGSetting } from 'color-module';
import { every, isEmpty, isNil } from 'lodash-es';
import { useActionState, useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { VSegmentedControl } from '../../../components/VSegmentedControl';
import { SchemeContent } from '../../../models';
import { QSchemeSetting, QSchemeSource, QSchemeStorage } from '../../../q-scheme';
import { useUpdateScheme } from '../../../stores/schemes';
import { defaultEmptyFormData } from '../../../utls';
import styles from './Builder.module.css';
type QSchemeBuilderProps = {
scheme: SchemeContent<QSchemeStorage>;
onBuildCompleted?: () => void;
};
export function QSchemeBuilder({ scheme, onBuildCompleted }: QSchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const defaultSetting = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
const defaultValues = colorFn.q_scheme_default_settings();
if (scheme.schemeStorage.source?.setting)
return new SchemeSetting(
new ColorShifting(
scheme.schemeStorage.source?.setting?.hover.chroma ?? defaultValues.hover.chroma,
scheme.schemeStorage.source?.setting?.hover.lightness ?? defaultValues.hover.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.active.chroma ?? defaultValues.active.chroma,
scheme.schemeStorage.source?.setting?.active.lightness ??
defaultValues.active.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.focus.chroma ?? defaultValues.focus.chroma,
scheme.schemeStorage.source?.setting?.focus.lightness ?? defaultValues.focus.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.disabled.chroma ?? defaultValues.disabled.chroma,
scheme.schemeStorage.source?.setting?.disabled.lightness ??
defaultValues.disabled.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.dark_convert.chroma ??
defaultValues.dark_convert.chroma,
scheme.schemeStorage.source?.setting?.dark_convert.lightness ??
defaultValues.dark_convert.lightness,
),
scheme.schemeStorage.source?.setting?.expand_method ?? defaultValues.expand_method,
scheme.schemeStorage.source?.setting?.wacg_follows ?? defaultValues.wacg_follows,
);
return defaultValues;
} catch (e) {
console.error('[Q scheme builder]', e);
}
return null;
}, [scheme]);
const expandingMethods = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
return colorFn.q_scheme_color_expanding_methods();
} catch (e) {
console.error('[Q scheme builder]', e);
}
return [];
}, []);
const wacgFollowStrategies = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
return colorFn.q_scheme_wacg_settings();
} catch (e) {
console.error('[Q scheme builder]', e);
}
return [];
}, []);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
const requiredFields = [
'primary',
'danger',
'success',
'warn',
'info',
'foreground',
'background',
];
for (const field of requiredFields) {
if (!formData.get(field)) {
errMsg.set(field, 'This color is required for scheme generating.');
}
}
if (!isEmpty(errMsg)) return errMsg;
try {
const schemeSetting = new SchemeSetting(
new ColorShifting(
Number(formData.get('hover_chroma')) / 100,
Number(formData.get('hover_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('active_chroma')) / 100,
Number(formData.get('active_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('focus_chroma')) / 100,
Number(formData.get('focus_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('disabled_chroma')) / 100,
Number(formData.get('disabled_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('dark_chroma')) / 100,
Number(formData.get('dark_lightness')) / 100,
),
Number(formData.get('expanding')) as ColorExpand,
Number(formData.get('wacg')) as WACGSetting,
);
const dumpedSetting = schemeSetting.toJsValue() as QSchemeSetting;
const source: QSchemeSource = {
primary: defaultEmptyFormData(formData, 'primary', null),
secondary: defaultEmptyFormData(formData, 'secondary', null),
tertiary: defaultEmptyFormData(formData, 'tertiary', null),
accent: defaultEmptyFormData(formData, 'accent', null),
danger: defaultEmptyFormData(formData, 'danger', null),
success: defaultEmptyFormData(formData, 'success', null),
warning: defaultEmptyFormData(formData, 'warn', null),
info: defaultEmptyFormData(formData, 'info', null),
foreground: defaultEmptyFormData(formData, 'foreground', null),
background: defaultEmptyFormData(formData, 'background', null),
setting: dumpedSetting,
};
const generatedScheme = every([source.secondary, source.tertiary, source.accent], isNil)
? colorFn?.generate_q_scheme_automatically(
source.primary ?? '',
source.danger ?? '',
source.success ?? '',
source.warning ?? '',
source.info ?? '',
source.foreground ?? '',
source.background ?? '',
schemeSetting,
)
: colorFn?.generate_q_scheme_manually(
source.primary ?? '',
source.secondary ?? undefined,
source.tertiary ?? undefined,
source.accent ?? undefined,
source.danger ?? '',
source.success ?? '',
source.warning ?? '',
source.info ?? '',
source.foreground ?? '',
source.background ?? '',
schemeSetting,
);
updateScheme((prev) => {
prev.schemeStorage.source = source;
prev.schemeStorage.scheme = generatedScheme[0];
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildCompleted?.();
} catch (e) {
console.error('[build q scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Original Colors</h5>
<label className={styles.label}>Primary Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="primary" color={scheme.schemeStorage.source?.primary} />
{errMsg.has('primary') && (
<span className={styles.error_msg}>{errMsg.get('primary')}</span>
)}
</div>
<label className={styles.label}>Secondary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="secondary" color={scheme.schemeStorage.source?.secondary} />
</div>
<label className={styles.label}>Tertiary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="tertiary" color={scheme.schemeStorage.source?.tertiary} />
</div>
<label className={styles.label}>Accent Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="accent" color={scheme.schemeStorage.source?.accent} />
</div>
<label className={styles.label}>Danger Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="danger" color={scheme.schemeStorage.source?.danger} />
{errMsg.has('danger') && <span className={styles.error_msg}>{errMsg.get('danger')}</span>}
</div>
<label className={styles.label}>Success Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="success" color={scheme.schemeStorage.source?.success} />
{errMsg.has('success') && (
<span className={styles.error_msg}>{errMsg.get('success')}</span>
)}
</div>
<label className={styles.label}>Warning Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="warn" color={scheme.schemeStorage.source?.warning} />
{errMsg.has('warn') && <span className={styles.error_msg}>{errMsg.get('warn')}</span>}
</div>
<label className={styles.label}>Info Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="info" color={scheme.schemeStorage.source?.info} />
{errMsg.has('info') && <span className={styles.error_msg}>{errMsg.get('info')}</span>}
</div>
<label className={styles.label}>Foreground Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="foreground" color={scheme.schemeStorage.source?.foreground} />
{errMsg.has('foreground') && (
<span className={styles.error_msg}>{errMsg.get('foreground')}</span>
)}
</div>
<label className={styles.label}>Background Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="background" color={scheme.schemeStorage.source?.background} />
{errMsg.has('background') && (
<span className={styles.error_msg}>{errMsg.get('background')}</span>
)}
</div>
<h5 className={styles.segment_title}>Automated parameters</h5>
<label style={{ gridColumn: 2 }}>Chroma shifting</label>
<label style={{ gridColumn: 3 }}>Lightness shifting</label>
<label className={styles.label}>Hover</label>
<div className="input_wrapper">
<input
type="number"
name="hover_chroma"
defaultValue={((defaultSetting?.hover.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="hover_lightness"
defaultValue={((defaultSetting?.hover.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Active</label>
<div className="input_wrapper">
<input
type="number"
name="active_chroma"
defaultValue={((defaultSetting?.active.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="active_lightness"
defaultValue={((defaultSetting?.active.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Focus</label>
<div className="input_wrapper">
<input
type="number"
name="focus_chroma"
defaultValue={((defaultSetting?.focus.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="focus_lightness"
defaultValue={((defaultSetting?.focus.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Disabled</label>
<div className="input_wrapper">
<input
type="number"
name="disabled_chroma"
defaultValue={((defaultSetting?.disabled.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="disabled_lightness"
defaultValue={((defaultSetting?.disabled.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Convert to Dark scheme</label>
<div className="input_wrapper">
<input
type="number"
name="dark_chroma"
defaultValue={((defaultSetting?.dark_convert.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="dark_lightness"
defaultValue={((defaultSetting?.dark_convert.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<h5 className={styles.segment_title}>Settings</h5>
<label className={styles.label}>Color Expanding Method</label>
<div style={{ gridColumn: '2 / span 2' }}>
<VSegmentedControl
options={expandingMethods}
name="expanding"
defaultValue={defaultSetting?.expand_method}
/>
</div>
<label className={styles.label}>Follow WACG Standard</label>
<div style={{ gridColumn: '2 / span 2' }}>
<VSegmentedControl
options={wacgFollowStrategies}
name="wacg"
defaultValue={defaultSetting?.wacg_follows}
/>
</div>
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

Some files were not shown because too many files have changed in this diff Show More