Compare commits
82 Commits
e0d35d279f
...
wasm-load
Author | SHA1 | Date | |
---|---|---|---|
|
2ddffe1b12 | ||
|
3eae8116e7 | ||
|
12e76b658e | ||
|
2ec3578e1c | ||
|
f944d48e1b | ||
|
88e3d1f928 | ||
|
2144cd548a | ||
|
2c47369772 | ||
|
2f51a80c91 | ||
|
546ca97b10 | ||
|
6b262f536d | ||
|
6aa3875919 | ||
|
0369f238f2 | ||
|
c60aefaaff | ||
|
7bfe9a7620 | ||
|
1b044c66d7 | ||
|
e74ffc9721 | ||
|
1553c51621 | ||
|
14d775e956 | ||
|
e2806a0cc5 | ||
|
5d3fc2903b | ||
|
71feeb4efc | ||
|
2b9547a7c2 | ||
|
131c43c5cf | ||
|
505af1c67e | ||
|
320b750834 | ||
|
6728ca1be2 | ||
|
83dcb3f80f | ||
|
b8018e323d | ||
|
d68ac6a3df | ||
|
0f5805bb7f | ||
|
a3fb9b656b | ||
|
d817024bf3 | ||
|
d98e3a69d9 | ||
|
8b0e9699c7 | ||
|
8e71d3c555 | ||
|
e9c2d4cb16 | ||
|
2acb69da20 | ||
|
9664983b5c | ||
|
fc340f3f74 | ||
|
f9f855e818 | ||
|
853b9b6b75 | ||
|
41788c4944 | ||
|
2bc250fc3d | ||
|
7468e28928 | ||
|
ca83ce082b | ||
|
89b2a2f9d9 | ||
|
74dd9e7354 | ||
|
592244911f | ||
|
08fabb53a2 | ||
|
0350380df6 | ||
|
32d8457802 | ||
|
b124bb4eda | ||
|
92229b0de4 | ||
|
e3642cad97 | ||
|
59519e1408 | ||
|
56a4786675 | ||
|
838f0c0fa0 | ||
|
9fa05824d2 | ||
|
14851c8284 | ||
|
50646ffccf | ||
|
a9ad4dea5d | ||
|
7b26c95a9a | ||
|
8efb3ec318 | ||
|
c4f703906e | ||
|
6dba92a2c5 | ||
|
4b4428fd3b | ||
|
1b41fb4d22 | ||
|
a3de0f961a | ||
|
79794ed0f7 | ||
|
20757a789a | ||
|
f9f984a1b4 | ||
|
9606106c45 | ||
|
3882ae764f | ||
|
2b17a5de0f | ||
|
b2357811b6 | ||
|
32273256c0 | ||
|
7e2132662f | ||
|
cff2ad0439 | ||
|
dc411987bf | ||
|
26ebc3c7e3 | ||
|
ab4e0b440c |
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -2,30 +2,63 @@ use palette::{
|
||||
cam16::{Cam16Jch, Parameters},
|
||||
convert::FromColorUnclamped,
|
||||
luma::Luma,
|
||||
Hsl, IntoColor, IsWithinBounds, Oklab, Oklch, Srgb,
|
||||
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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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!(
|
||||
|
@@ -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")
|
||||
}
|
||||
}
|
||||
|
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -48,16 +48,19 @@ impl Baseline {
|
||||
let outline_color = neutral_swatch.get(background.l * 0.7);
|
||||
|
||||
Self {
|
||||
primary: ColorSet::new(primary, &neutral_swatch, &setting),
|
||||
secondary: secondary.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)),
|
||||
tertiary: tertiary.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)),
|
||||
accent: accent.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)),
|
||||
neutral: ColorSet::new(&neutral_color, &neutral_swatch, &setting),
|
||||
danger: ColorSet::new(danger, &neutral_swatch, &setting),
|
||||
success: ColorSet::new(success, &neutral_swatch, &setting),
|
||||
warning: ColorSet::new(warning, &neutral_swatch, &setting),
|
||||
info: ColorSet::new(info, &neutral_swatch, &setting),
|
||||
outline: ColorSet::new(&outline_color, &neutral_swatch, &setting),
|
||||
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,
|
||||
|
@@ -24,34 +24,43 @@ pub struct ColorSet {
|
||||
|
||||
fn fit_to_wacg(reference: &Oklch, neutral_swatch: &NeutralSwatch, ratio: f32) -> Oklch {
|
||||
let reference_luma = map_oklch_to_luma(reference);
|
||||
let mut new_target = neutral_swatch.get(reference.l);
|
||||
let quick_factor: f32 = if reference.l <= 0.5 { 0.05 } else { -0.05 };
|
||||
let fine_factor: f32 = if reference.l <= 0.5 { 0.01 } else { -0.01 };
|
||||
|
||||
let match_wacg = |original: &Oklch<f32>, reference: &Luma| {
|
||||
let luma = map_oklch_to_luma(original);
|
||||
luma.relative_contrast(*reference)
|
||||
};
|
||||
|
||||
while match_wacg(&new_target, &reference_luma) < ratio {
|
||||
new_target.l = new_target.l * (1.0 + quick_factor);
|
||||
if new_target.l > 1.0 {
|
||||
new_target.l = 1.0;
|
||||
break;
|
||||
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);
|
||||
}
|
||||
}
|
||||
while match_wacg(&new_target, &reference_luma) < ratio {
|
||||
new_target.l = new_target.l * (1.0 + fine_factor);
|
||||
if new_target.l > 1.0 {
|
||||
new_target.l = 1.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
new_target
|
||||
|
||||
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, setting: &SchemeSetting) -> Self {
|
||||
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;
|
||||
@@ -60,11 +69,11 @@ impl ColorSet {
|
||||
|
||||
let (on_root, on_hover, on_active, on_focus, on_disabled) = match setting.wacg_follows {
|
||||
WACGSetting::Fixed => (
|
||||
neutral_swatch.get(root.l),
|
||||
neutral_swatch.get(hover.l),
|
||||
neutral_swatch.get(active.l),
|
||||
neutral_swatch.get(focus.l),
|
||||
neutral_swatch.get(disabled.l),
|
||||
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),
|
||||
@@ -80,6 +89,13 @@ impl ColorSet {
|
||||
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 {
|
||||
|
@@ -4,7 +4,6 @@ use baseline::Baseline;
|
||||
use palette::FromColor;
|
||||
use scheme_setting::{ColorExpand, WACGSetting};
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
|
||||
|
||||
use crate::{errors, parse_option_to_oklch, parse_to_oklch};
|
||||
@@ -59,8 +58,8 @@ impl QScheme {
|
||||
&(success * setting.dark_convert),
|
||||
&(warning * setting.dark_convert),
|
||||
&(info * setting.dark_convert),
|
||||
&(foreground * setting.dark_convert),
|
||||
&(background * setting.dark_convert),
|
||||
&(&background * (setting.dark_convert / 2.0)),
|
||||
&(&foreground * setting.dark_convert),
|
||||
setting.clone(),
|
||||
),
|
||||
})
|
||||
@@ -113,7 +112,7 @@ impl QScheme {
|
||||
&(success * setting.dark_convert),
|
||||
&(warning * setting.dark_convert),
|
||||
&(info * setting.dark_convert),
|
||||
&(foreground * setting.dark_convert),
|
||||
&(foreground * (setting.dark_convert / 2.0)),
|
||||
&(background * setting.dark_convert),
|
||||
setting.clone(),
|
||||
),
|
||||
@@ -169,11 +168,11 @@ impl SchemeExport for QScheme {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> {
|
||||
let methods = ColorExpand::iter()
|
||||
let methods = enum_iterator::all::<ColorExpand>()
|
||||
.map(|variant| {
|
||||
serde_json::json!({
|
||||
"label": variant.label(),
|
||||
"value": variant.to_string(),
|
||||
"value": variant as u8,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -183,14 +182,19 @@ pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn q_scheme_wacg_settings() -> Result<JsValue, String> {
|
||||
let settings = WACGSetting::iter()
|
||||
let settings = enum_iterator::all::<WACGSetting>()
|
||||
.map(|setting| {
|
||||
serde_json::json!({
|
||||
"label": setting.label(),
|
||||
"value": setting.to_string(),
|
||||
"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()
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
use std::ops::Mul;
|
||||
use std::ops::{Div, Mul};
|
||||
|
||||
use enum_iterator::Sequence;
|
||||
use palette::Oklch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
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]
|
||||
@@ -12,6 +14,19 @@ pub struct ColorShifting {
|
||||
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>;
|
||||
|
||||
@@ -21,13 +36,13 @@ impl Mul<ColorShifting> for Oklch<f32> {
|
||||
+ if rhs.lightness > 0.0 {
|
||||
(1.0 - self.l) * rhs.lightness
|
||||
} else {
|
||||
self.l * rhs.lightness * -1.0
|
||||
self.l * rhs.lightness
|
||||
},
|
||||
self.chroma
|
||||
+ if rhs.chroma > 0.0 {
|
||||
(100.0 - self.chroma) * rhs.chroma
|
||||
} else {
|
||||
-(self.chroma * rhs.chroma)
|
||||
self.chroma * rhs.chroma
|
||||
},
|
||||
self.hue,
|
||||
)
|
||||
@@ -43,19 +58,41 @@ impl Mul<ColorShifting> for &Oklch<f32> {
|
||||
+ if rhs.lightness > 0.0 {
|
||||
(1.0 - self.l) * rhs.lightness
|
||||
} else {
|
||||
self.l * rhs.lightness * -1.0
|
||||
self.l * rhs.lightness
|
||||
},
|
||||
self.chroma
|
||||
+ if rhs.chroma > 0.0 {
|
||||
(100.0 - self.chroma) * rhs.chroma
|
||||
} else {
|
||||
-(self.chroma * rhs.chroma)
|
||||
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 {
|
||||
@@ -68,9 +105,9 @@ pub struct SchemeSetting {
|
||||
pub wacg_follows: WACGSetting,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Display, EnumString, EnumIter, Serialize, Deserialize)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[derive(Debug, Clone, Copy, Display, Sequence, Serialize_repr, Deserialize_repr)]
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
pub enum ColorExpand {
|
||||
Complementary,
|
||||
Analogous,
|
||||
@@ -95,13 +132,14 @@ impl ColorExpand {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Display, EnumString, EnumIter, Serialize, Deserialize)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[derive(Debug, Clone, Copy, Display, Sequence, Serialize_repr, Deserialize_repr)]
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
pub enum WACGSetting {
|
||||
Fixed,
|
||||
AutomaticAA,
|
||||
AutomaticAAA,
|
||||
HighContrast,
|
||||
}
|
||||
|
||||
impl WACGSetting {
|
||||
@@ -110,6 +148,7 @@ impl WACGSetting {
|
||||
WACGSetting::Fixed => "Fixed".to_string(),
|
||||
WACGSetting::AutomaticAA => "Automatic AA".to_string(),
|
||||
WACGSetting::AutomaticAAA => "Automatic AAA".to_string(),
|
||||
WACGSetting::HighContrast => "High Contrast".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +170,7 @@ impl Default for SchemeSetting {
|
||||
},
|
||||
disabled: ColorShifting {
|
||||
chroma: -0.9,
|
||||
lightness: -0.2,
|
||||
lightness: 0.2,
|
||||
},
|
||||
dark_convert: ColorShifting {
|
||||
chroma: -0.3,
|
||||
@@ -142,3 +181,32 @@ impl Default for SchemeSetting {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?)
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use palette::FromColor;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -19,13 +20,29 @@ pub struct SwatchScheme {
|
||||
dark: HashMap<String, Swatch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[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>,
|
||||
@@ -120,3 +137,8 @@ impl SchemeExport for SwatchScheme {
|
||||
object.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn swatch_scheme_default_settings() -> SwatchSchemeSetting {
|
||||
SwatchSchemeSetting::default()
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
|
||||
|
||||
use crate::schemes::q_style::ColorShifting;
|
||||
|
||||
@@ -17,8 +17,8 @@ impl Default for SwatchSchemeSetting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
amount: 10,
|
||||
min_lightness: 10.0,
|
||||
max_lightness: 90.0,
|
||||
min_lightness: 0.1,
|
||||
max_lightness: 0.9,
|
||||
include_primary: false,
|
||||
dark_convert: ColorShifting {
|
||||
chroma: -0.3,
|
||||
@@ -27,3 +27,28 @@ impl Default for SwatchSchemeSetting {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?)
|
||||
}
|
||||
}
|
||||
|
@@ -24,28 +24,26 @@ impl Swatch {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_interval(&self) -> (usize, usize) {
|
||||
fn find_interval(&self) -> usize {
|
||||
if !self.include_primary {
|
||||
return (0, 0);
|
||||
return 0;
|
||||
}
|
||||
if self.primary_key.l == self.min_key {
|
||||
return (0, 1);
|
||||
return 0;
|
||||
}
|
||||
if self.primary_key.l == self.max_key {
|
||||
return (self.color_amount - 2, self.color_amount - 1);
|
||||
return self.color_amount - 1;
|
||||
}
|
||||
let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32;
|
||||
let index = ((self.primary_key.l - self.min_key) / step) as usize;
|
||||
|
||||
(index, index + 1)
|
||||
((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();
|
||||
let primary_index = self.find_interval();
|
||||
if primary_index > 0 {
|
||||
let step = (self.max_key - self.min_key) / primary_index as f32;
|
||||
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 {
|
||||
@@ -55,8 +53,8 @@ impl Swatch {
|
||||
}
|
||||
}
|
||||
if primary_index < self.color_amount - 1 {
|
||||
let step =
|
||||
(self.max_key - self.min_key) / (self.color_amount - primary_index) as f32;
|
||||
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 {
|
||||
|
25
index.html
25
index.html
@@ -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>
|
||||
|
||||
<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>
|
@@ -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",
|
||||
|
@@ -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 /> },
|
||||
|
@@ -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);
|
||||
|
300
src/color_functions/color_module.d.ts
vendored
300
src/color_functions/color_module.d.ts
vendored
@@ -1,300 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function q_scheme_color_expanding_methods(): any;
|
||||
export function q_scheme_wacg_settings(): 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 shift_hue(color: string, degree: number): string;
|
||||
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 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 function color_categories(): any;
|
||||
export function search_color_cards(tag: string, category?: string): any;
|
||||
export function generate_material_design_3_scheme(source_color: string, error_color: string, custom_colors: any): any;
|
||||
export function generate_material_design_2_scheme(primary_color: string, secondary_color: string, error_color: string, custom_colors: any): any;
|
||||
export function generate_q_scheme_automatically(primary_color: string, danger_color: string, success_color: string, warning_color: string, info_color: string, fg_color: string, bg_color: string, setting: SchemeSetting): any;
|
||||
export function generate_q_scheme_manually(primary_color: string, secondary_color: string | undefined, tertiary_color: string | undefined, accent_color: string | undefined, danger_color: string, success_color: string, warning_color: string, info_color: string, fg_color: string, bg_color: string, setting: SchemeSetting): any;
|
||||
export function generate_swatch_scheme(colors: (SwatchEntry)[], setting: SwatchSchemeSetting): any;
|
||||
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 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 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 wacg_relative_contrast(fg_color: string, bg_color: string): number;
|
||||
export enum ColorExpand {
|
||||
Complementary = 0,
|
||||
Analogous = 1,
|
||||
AnalogousAndComplementary = 2,
|
||||
Triadic = 3,
|
||||
SplitComplementary = 4,
|
||||
Tetradic = 5,
|
||||
Square = 6,
|
||||
}
|
||||
export enum WACGSetting {
|
||||
Fixed = 0,
|
||||
AutomaticAA = 1,
|
||||
AutomaticAAA = 2,
|
||||
}
|
||||
export class ColorShifting {
|
||||
private constructor();
|
||||
free(): void;
|
||||
chroma: number;
|
||||
lightness: number;
|
||||
}
|
||||
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 class SchemeSetting {
|
||||
private constructor();
|
||||
free(): void;
|
||||
hover: ColorShifting;
|
||||
active: ColorShifting;
|
||||
focus: ColorShifting;
|
||||
disabled: ColorShifting;
|
||||
dark_convert: ColorShifting;
|
||||
expand_method: ColorExpand;
|
||||
wacg_follows: WACGSetting;
|
||||
}
|
||||
export class SwatchEntry {
|
||||
private constructor();
|
||||
free(): void;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
export class SwatchSchemeSetting {
|
||||
private constructor();
|
||||
free(): void;
|
||||
amount: number;
|
||||
min_lightness: number;
|
||||
max_lightness: number;
|
||||
include_primary: boolean;
|
||||
dark_convert: ColorShifting;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly q_scheme_color_expanding_methods: () => [number, number, number];
|
||||
readonly q_scheme_wacg_settings: () => [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 shift_hue: (a: number, b: number, c: number) => [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 __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_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 generate_palette_from_color: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
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_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_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 __wbg_swatchentry_free: (a: number, b: number) => void;
|
||||
readonly __wbg_get_swatchentry_name: (a: number) => [number, number];
|
||||
readonly __wbg_set_swatchentry_name: (a: number, b: number, c: number) => void;
|
||||
readonly __wbg_get_swatchentry_color: (a: number) => [number, number];
|
||||
readonly __wbg_set_swatchentry_color: (a: number, b: number, c: number) => void;
|
||||
readonly color_categories: () => [number, number, number];
|
||||
readonly search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
readonly __wbg_colorshifting_free: (a: number, b: number) => void;
|
||||
readonly __wbg_get_colorshifting_chroma: (a: number) => number;
|
||||
readonly __wbg_set_colorshifting_chroma: (a: number, b: number) => void;
|
||||
readonly __wbg_get_colorshifting_lightness: (a: number) => number;
|
||||
readonly __wbg_set_colorshifting_lightness: (a: number, b: number) => void;
|
||||
readonly __wbg_schemesetting_free: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_hover: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_hover: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_active: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_active: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_focus: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_focus: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_disabled: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_disabled: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_dark_convert: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_dark_convert: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_expand_method: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_expand_method: (a: number, b: number) => void;
|
||||
readonly __wbg_get_schemesetting_wacg_follows: (a: number) => number;
|
||||
readonly __wbg_set_schemesetting_wacg_follows: (a: number, b: number) => void;
|
||||
readonly generate_material_design_3_scheme: (a: number, b: number, c: number, d: number, e: any) => [number, number, number];
|
||||
readonly generate_material_design_2_scheme: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => [number, number, number];
|
||||
readonly generate_q_scheme_automatically: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number) => [number, number, number];
|
||||
readonly generate_q_scheme_manually: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number) => [number, number, number];
|
||||
readonly generate_swatch_scheme: (a: number, b: number, c: 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 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 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 wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
readonly __wbg_swatchschemesetting_free: (a: number, b: number) => void;
|
||||
readonly __wbg_get_swatchschemesetting_amount: (a: number) => number;
|
||||
readonly __wbg_set_swatchschemesetting_amount: (a: number, b: number) => void;
|
||||
readonly __wbg_get_swatchschemesetting_min_lightness: (a: number) => number;
|
||||
readonly __wbg_set_swatchschemesetting_min_lightness: (a: number, b: number) => void;
|
||||
readonly __wbg_get_swatchschemesetting_max_lightness: (a: number) => number;
|
||||
readonly __wbg_set_swatchschemesetting_max_lightness: (a: number, b: number) => void;
|
||||
readonly __wbg_get_swatchschemesetting_include_primary: (a: number) => number;
|
||||
readonly __wbg_set_swatchschemesetting_include_primary: (a: number, b: number) => void;
|
||||
readonly __wbg_get_swatchschemesetting_dark_convert: (a: number) => number;
|
||||
readonly __wbg_set_swatchschemesetting_dark_convert: (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 __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_export_4: 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
Binary file not shown.
138
src/color_functions/color_module_bg.wasm.d.ts
vendored
138
src/color_functions/color_module_bg.wasm.d.ts
vendored
@@ -1,138 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const q_scheme_color_expanding_methods: () => [number, number, number];
|
||||
export const q_scheme_wacg_settings: () => [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 shift_hue: (a: number, b: number, c: number) => [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 __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_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 generate_palette_from_color: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
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_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_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 __wbg_swatchentry_free: (a: number, b: number) => void;
|
||||
export const __wbg_get_swatchentry_name: (a: number) => [number, number];
|
||||
export const __wbg_set_swatchentry_name: (a: number, b: number, c: number) => void;
|
||||
export const __wbg_get_swatchentry_color: (a: number) => [number, number];
|
||||
export const __wbg_set_swatchentry_color: (a: number, b: number, c: number) => void;
|
||||
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 __wbg_colorshifting_free: (a: number, b: number) => void;
|
||||
export const __wbg_get_colorshifting_chroma: (a: number) => number;
|
||||
export const __wbg_set_colorshifting_chroma: (a: number, b: number) => void;
|
||||
export const __wbg_get_colorshifting_lightness: (a: number) => number;
|
||||
export const __wbg_set_colorshifting_lightness: (a: number, b: number) => void;
|
||||
export const __wbg_schemesetting_free: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_hover: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_hover: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_active: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_active: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_focus: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_focus: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_disabled: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_disabled: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_dark_convert: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_dark_convert: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_expand_method: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_expand_method: (a: number, b: number) => void;
|
||||
export const __wbg_get_schemesetting_wacg_follows: (a: number) => number;
|
||||
export const __wbg_set_schemesetting_wacg_follows: (a: number, b: number) => void;
|
||||
export const generate_material_design_3_scheme: (a: number, b: number, c: number, d: number, e: any) => [number, number, number];
|
||||
export const generate_material_design_2_scheme: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => [number, number, number];
|
||||
export const generate_q_scheme_automatically: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number) => [number, number, number];
|
||||
export const generate_q_scheme_manually: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number) => [number, number, number];
|
||||
export const generate_swatch_scheme: (a: number, b: number, c: 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 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 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 wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
export const __wbg_swatchschemesetting_free: (a: number, b: number) => void;
|
||||
export const __wbg_get_swatchschemesetting_amount: (a: number) => number;
|
||||
export const __wbg_set_swatchschemesetting_amount: (a: number, b: number) => void;
|
||||
export const __wbg_get_swatchschemesetting_min_lightness: (a: number) => number;
|
||||
export const __wbg_set_swatchschemesetting_min_lightness: (a: number, b: number) => void;
|
||||
export const __wbg_get_swatchschemesetting_max_lightness: (a: number) => number;
|
||||
export const __wbg_set_swatchschemesetting_max_lightness: (a: number, b: number) => void;
|
||||
export const __wbg_get_swatchschemesetting_include_primary: (a: number) => number;
|
||||
export const __wbg_set_swatchschemesetting_include_primary: (a: number, b: number) => void;
|
||||
export const __wbg_get_swatchschemesetting_dark_convert: (a: number) => number;
|
||||
export const __wbg_set_swatchschemesetting_dark_convert: (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 __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_exn_store: (a: number) => void;
|
||||
export const __externref_table_alloc: () => number;
|
||||
export const __wbindgen_export_4: 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;
|
@@ -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/*"
|
||||
]
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
10
src/components/ActionIcon.module.css
Normal file
10
src/components/ActionIcon.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@layer components {
|
||||
.action_icon {
|
||||
padding: var(--spacing-xs);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-xxs);
|
||||
line-height: 1em;
|
||||
.icon {
|
||||
}
|
||||
}
|
||||
}
|
25
src/components/ActionIcon.tsx
Normal file
25
src/components/ActionIcon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
|
||||
import cx from 'clsx';
|
||||
import { MouseEvent, MouseEventHandler, useCallback } from 'react';
|
||||
import styles from './ActionIcon.module.css';
|
||||
|
||||
type ActionIconProps = {
|
||||
icon: IconProps['icon'];
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
extendClassName?: HTMLButtonElement['className'];
|
||||
};
|
||||
|
||||
export function ActionIcon({ icon, onClick, extendClassName }: ActionIconProps) {
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>) => {
|
||||
onClick?.(event);
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<button type="button" onClick={handleClick} className={cx(styles.action_icon, extendClassName)}>
|
||||
<Icon icon={icon} className={styles.icon} />
|
||||
</button>
|
||||
);
|
||||
}
|
12
src/components/Badge.module.css
Normal file
12
src/components/Badge.module.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@layer components {
|
||||
.badge {
|
||||
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
12
src/components/Badge.tsx
Normal file
12
src/components/Badge.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import cx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
import styles from './Badge.module.css';
|
||||
|
||||
type BadgeProps = {
|
||||
extendClassName?: HTMLDivElement['className'];
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function Badge({ extendClassName, children }: BadgeProps) {
|
||||
return <div className={cx(styles.badge, extendClassName)}>{children}</div>;
|
||||
}
|
@@ -3,7 +3,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-xs);
|
||||
.extended_input_wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.rgb_input {
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
|
@@ -87,7 +87,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
|
||||
}
|
||||
};
|
||||
const updateH = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(evt.target.value, 10);
|
||||
let value = parseInt(evt.target.value, 10);
|
||||
if (value > 360) {
|
||||
value -= 360;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
|
||||
|
||||
return (
|
||||
<div className={styles.rgb_input}>
|
||||
<div className={cx('input_wrapper')}>
|
||||
<div className={cx('input_wrapper', styles.extended_input_wrapper)}>
|
||||
<Icon icon="tabler:hash" />
|
||||
<input type="text" value={hex} onChange={updateHex} className={styles.rgb_input} />
|
||||
</div>
|
||||
|
@@ -28,7 +28,7 @@ export function ColorRangePicker({
|
||||
}: ColorRangePickerProps) {
|
||||
const [pickerValue, setPickerValue] = useState(value);
|
||||
const handlePickerChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = evt.target.value as number;
|
||||
const value = Number(evt.target.value);
|
||||
setPickerValue(valueProcess(value));
|
||||
onChange?.(valueProcess(value));
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
|
||||
import styles from './EditableTitle.module.css';
|
||||
|
||||
type EditableTitleProps = {
|
||||
title: string;
|
||||
title?: string;
|
||||
onChange?: (newTitle: string) => void;
|
||||
};
|
||||
|
||||
|
44
src/components/FloatColorPicker.module.css
Normal file
44
src/components/FloatColorPicker.module.css
Normal file
@@ -0,0 +1,44 @@
|
||||
@layer components {
|
||||
.float_color_picker {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
.preview {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
z-index: 25;
|
||||
.preview_block {
|
||||
height: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: var(--border-radius-xxs);
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
.picker {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
padding: var(--spacing-s) var(--spacing-s);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
z-index: 260;
|
||||
.btns {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
src/components/FloatColorPicker.tsx
Normal file
56
src/components/FloatColorPicker.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ActionIcon } from './ActionIcon';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import styles from './FloatColorPicker.module.css';
|
||||
|
||||
type FloatColorPickerProps = {
|
||||
name?: string;
|
||||
color?: string | null;
|
||||
onPick?: (color: string | null | undefined) => void;
|
||||
};
|
||||
|
||||
export function FloatColorPicker({ name, color, onPick }: FloatColorPickerProps) {
|
||||
const [pickedColor, setPicked] = useState<string | null>(color ?? null);
|
||||
const [showPicker, setPickerShow] = useState(false);
|
||||
const handlePickAction = useCallback(
|
||||
(value: string) => {
|
||||
setPicked(value);
|
||||
onPick?.(value);
|
||||
},
|
||||
[onPick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(pickedColor, color)) {
|
||||
setPicked(color);
|
||||
}
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<div className={styles.float_color_picker}>
|
||||
<div className={styles.preview}>
|
||||
<div
|
||||
className={styles.preview_block}
|
||||
onClick={() => setPickerShow(true)}
|
||||
style={{
|
||||
backgroundColor: isNil(pickedColor) ? 'rgba(0, 0, 0, 0)' : `#${pickedColor}`,
|
||||
}}>
|
||||
{isNil(pickedColor) && <span>N/A</span>}
|
||||
</div>
|
||||
<ActionIcon icon="tabler:x" onClick={() => handlePickAction(null)} />
|
||||
</div>
|
||||
{showPicker && (
|
||||
<div className={styles.picker}>
|
||||
<ColorPicker color={pickedColor ?? null} onSelect={handlePickAction} />
|
||||
<div className={styles.btns}>
|
||||
<button type="button" className="primary" onClick={() => setPickerShow(false)}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isNil(name) && <input type="hidden" name={name} value={pickedColor ?? ''} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,17 +1,32 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { isEqual, isMap, isNil } from 'lodash-es';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { Option } from '../models';
|
||||
import styles from './HSegmentedControl.module.css';
|
||||
|
||||
type HSegmentedControlProps = {
|
||||
name?: string;
|
||||
defaultValue?: Option['value'];
|
||||
options?: Option[];
|
||||
value?: Option['value'];
|
||||
onChange?: (value: Option['value']) => void;
|
||||
extendClassName?: HTMLDivElement['className'];
|
||||
};
|
||||
|
||||
export function HSegmentedControl({ options = [], value, onChange }: HSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(value ?? options[0].value ?? null);
|
||||
export function HSegmentedControl({
|
||||
name,
|
||||
defaultValue,
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
extendClassName,
|
||||
}: HSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(
|
||||
value ??
|
||||
defaultValue ??
|
||||
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
|
||||
null,
|
||||
);
|
||||
const [sliderPosition, setSliderPosition] = useState(0);
|
||||
const [sliderWidth, setSliderWidth] = useState(0);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -28,17 +43,22 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.segmented_control}>
|
||||
<div className={cx(styles.segmented_control, extendClassName)}>
|
||||
<div className={styles.options}>
|
||||
{options.map((option, index) => (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -118,7 +118,7 @@ type ToastProps = {
|
||||
icon?: string;
|
||||
duration?: ToastDuration;
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
closeAction: () => void;
|
||||
closeAction: (tid?: string) => void;
|
||||
};
|
||||
const Toast = ({
|
||||
kind,
|
||||
@@ -157,7 +157,7 @@ export function useNotification() {
|
||||
type NotificationElement = {
|
||||
id: string;
|
||||
element: ReactNode;
|
||||
ref: RefObject<ReactNode>;
|
||||
ref: RefObject<ReactNode | HTMLDivElement>;
|
||||
};
|
||||
type NotificationsProps = {
|
||||
defaultDuration?: number;
|
||||
@@ -184,7 +184,7 @@ export function Notifications({
|
||||
duration?: number,
|
||||
) => {
|
||||
const id = v4();
|
||||
const ref = createRef(null);
|
||||
const ref = createRef<ReactNode | HTMLDivElement>();
|
||||
const newNotify = (
|
||||
<Notification
|
||||
kind={kind}
|
||||
@@ -194,6 +194,7 @@ export function Notifications({
|
||||
message={message}
|
||||
duration={duration ?? defaultDuration}
|
||||
closeAction={removeNotification}
|
||||
//@ts-expect-error TS2322
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
@@ -207,14 +208,9 @@ export function Notifications({
|
||||
setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id)));
|
||||
}, []);
|
||||
const showToast = useCallback(
|
||||
(
|
||||
kind: NotificationType,
|
||||
message?: string,
|
||||
icon?: IconifyIconProps['icon'],
|
||||
duration?: ToastDuration,
|
||||
) => {
|
||||
(kind: NotificationType, message?: string, icon?: string, duration?: ToastDuration) => {
|
||||
const id = v4();
|
||||
const ref = createRef(null);
|
||||
const ref = createRef<HTMLDivElement>();
|
||||
const newToast = (
|
||||
<Toast
|
||||
kind={kind}
|
||||
@@ -238,9 +234,6 @@ export function Notifications({
|
||||
value={{
|
||||
addNotification,
|
||||
removeNotification,
|
||||
showDialog: () => '',
|
||||
showModalDialog: () => '',
|
||||
closeDialog: () => {},
|
||||
showToast,
|
||||
}}>
|
||||
{children}
|
||||
@@ -250,6 +243,7 @@ export function Notifications({
|
||||
{notifications.slice(0, maxNotifications).map(({ id, element, ref }) => (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
//@ts-expect-error TS2322
|
||||
nodeRef={ref}
|
||||
unmountOnExit
|
||||
timeout={500}
|
||||
@@ -271,6 +265,7 @@ export function Notifications({
|
||||
{toasts.slice(0, 1).map(({ id, element, ref }) => (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
//@ts-expect-error TS2322
|
||||
nodeRef={ref}
|
||||
unmountOnExit
|
||||
timeout={500}
|
||||
|
21
src/components/SchemeSign.module.css
Normal file
21
src/components/SchemeSign.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
@layer components {
|
||||
.badge {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-yuebai);
|
||||
background-color: var(--color-mose);
|
||||
&.q {
|
||||
background-color: var(--color-mantianxingzi);
|
||||
}
|
||||
&.swatch {
|
||||
background-color: var(--color-pinlan);
|
||||
}
|
||||
&.m2 {
|
||||
background-color: #03dac6;
|
||||
color: var(--color-qihei);
|
||||
}
|
||||
&.m3 {
|
||||
background-color: #a78fff;
|
||||
color: var(--color-qihei);
|
||||
}
|
||||
}
|
||||
}
|
33
src/components/SchemeSign.tsx
Normal file
33
src/components/SchemeSign.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import cx from 'clsx';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { schemeType, SchemeType } from '../models';
|
||||
import { Badge } from './Badge';
|
||||
import styles from './SchemeSign.module.css';
|
||||
|
||||
type SchemeSignProps = {
|
||||
scheme?: SchemeType;
|
||||
short?: boolean;
|
||||
};
|
||||
|
||||
export function SchemeSign({ scheme, short = false }: SchemeSignProps) {
|
||||
const schemeName = schemeType(scheme, short);
|
||||
const signColorStyles = useMemo(() => {
|
||||
switch (scheme) {
|
||||
case 'q_scheme':
|
||||
return styles.q;
|
||||
case 'swatch_scheme':
|
||||
return styles.swatch;
|
||||
case 'material_2':
|
||||
return styles.m2;
|
||||
case 'material_3':
|
||||
return styles.m3;
|
||||
}
|
||||
}, [scheme]);
|
||||
|
||||
return (
|
||||
!isNil(scheme) && (
|
||||
<Badge extendClassName={cx(styles.badge, signColorStyles)}>{schemeName}</Badge>
|
||||
)
|
||||
);
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { clamp } from 'lodash-es';
|
||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { MouseEvent, RefObject, useEffect, useRef, useState, WheelEvent } from 'react';
|
||||
import styles from './ScrollArea.module.css';
|
||||
|
||||
type ScrollBarProps = {
|
||||
@@ -12,10 +12,12 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
const thumbRef = useRef<HTMLDivElement | null>(null);
|
||||
const handleMouseDown = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
const handleMouseMove = (evt: MouseEvent<HTMLDivElement>) => {
|
||||
evt.preventDefault();
|
||||
const container = containerRef?.current;
|
||||
const scrollbar = trackRef.current;
|
||||
@@ -34,7 +36,9 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
};
|
||||
const handleMouseUp = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
@@ -77,7 +81,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
const thumbRef = useRef<HTMLDivElement | null>(null);
|
||||
const handleMouseDown = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
@@ -99,7 +105,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
};
|
||||
const handleMouseUp = (evt: MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
//@ts-expect-error TS2769
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
@@ -129,7 +137,7 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
|
||||
className={styles.h_thumb}
|
||||
ref={thumbRef}
|
||||
style={{ left: thumbPos }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={(e) => handleMouseDown(e)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -148,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} />}
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -1,20 +1,30 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styles from './Tab.module.css';
|
||||
|
||||
type TabProps = {
|
||||
tabs: { title: string; id: unknown }[];
|
||||
activeTab?: unknown;
|
||||
onActive?: (id: unknown) => void;
|
||||
};
|
||||
|
||||
export function Tab({ tabs = [], onActive }: TabProps) {
|
||||
const [active, setActive] = useState(0);
|
||||
export function Tab({ tabs = [], activeTab, onActive }: TabProps) {
|
||||
const [active, setActive] = useState(() =>
|
||||
isNil(activeTab) ? 0 : tabs.findIndex((tab) => isEqual(tab.id, activeTab)),
|
||||
);
|
||||
const handleActivate = useCallback((index: number) => {
|
||||
setActive(index);
|
||||
onActive?.(tabs[index].id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const activeIndex = tabs.findIndex((tab) => isEqual(tab.id, activeTab));
|
||||
if (!isNil(activeIndex) && !isEqual(activeIndex, -1) && !isEqual(activeIndex, active)) {
|
||||
setActive(activeIndex);
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
<div className={styles.tabs_container}>
|
||||
{tabs.map((tab, index) => (
|
||||
|
@@ -18,7 +18,7 @@ const positionMap = {
|
||||
|
||||
export function Tooltip({ content, position = 'top', children }: TooltipProps) {
|
||||
const [show, setShow] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@@ -1,17 +1,32 @@
|
||||
import cx from 'clsx';
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { isEqual, isMap, isNil } from 'lodash-es';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { Option } from '../models';
|
||||
import styles from './VSegmentedControl.module.css';
|
||||
|
||||
type VSegmentedControlProps = {
|
||||
name?: string;
|
||||
defaultValue?: Option['value'];
|
||||
options?: Option[];
|
||||
value?: Option['value'];
|
||||
onChange?: (value: Option['value']) => void;
|
||||
extendClassName?: HTMLDivElement['className'];
|
||||
};
|
||||
|
||||
export function VSegmentedControl({ options = [], value, onChange }: VSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(value ?? options[0].value ?? null);
|
||||
export function VSegmentedControl({
|
||||
name,
|
||||
defaultValue,
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
extendClassName,
|
||||
}: VSegmentedControlProps) {
|
||||
const [selected, setSelected] = useState(
|
||||
value ??
|
||||
defaultValue ??
|
||||
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
|
||||
null,
|
||||
);
|
||||
const [sliderPosition, setSliderPosition] = useState(0);
|
||||
const [sliderHeight, setSliderHeight] = useState(0);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -28,17 +43,22 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.segmented_control}>
|
||||
<div className={cx(styles.segmented_control, extendClassName)}>
|
||||
<div className={styles.options}>
|
||||
{options.map((option, index) => (
|
||||
<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
28
src/hooks/useCopy.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { isEmpty, isNil } from 'lodash-es';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { NotificationType, useNotification } from '../components/Notifications';
|
||||
|
||||
export function useCopy() {
|
||||
const { showToast } = useNotification();
|
||||
const [cpState, copyToClipboard] = useCopyToClipboard();
|
||||
const copyAction = useCallback((content?: string | null) => {
|
||||
if (isNil(content) || isEmpty(content)) return;
|
||||
copyToClipboard(content);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNil(cpState.error)) {
|
||||
showToast(NotificationType.ERROR, 'Failed to copy to clipboard', 'tabler:alert-circle', 3000);
|
||||
} else if (!isNil(cpState.value)) {
|
||||
showToast(
|
||||
NotificationType.SUCCESS,
|
||||
`Content copied to clipboard.`,
|
||||
'tabler:circle-check',
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}, [cpState]);
|
||||
|
||||
return copyAction;
|
||||
}
|
@@ -22,13 +22,16 @@ export type MaterialDesign2Scheme = {
|
||||
};
|
||||
|
||||
export type MaterialDesign2SchemeSource = {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
error: string;
|
||||
custom_colors: Record<string, string>;
|
||||
primary: string | null;
|
||||
secondary: string | null;
|
||||
error: string | null;
|
||||
custom_colors?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type Materialdesign2SchemeStorage = {
|
||||
source: MaterialDesign2SchemeSource;
|
||||
scheme: MaterialDesign2Scheme;
|
||||
export type MaterialDesign2SchemeStorage = {
|
||||
source?: MaterialDesign2SchemeSource;
|
||||
scheme?: MaterialDesign2Scheme;
|
||||
cssVariables?: string;
|
||||
scssVariables?: string;
|
||||
jsVariables?: string;
|
||||
};
|
||||
|
@@ -46,12 +46,15 @@ export type MaterialDesign3Scheme = {
|
||||
};
|
||||
|
||||
export type MaterialDesign3SchemeSource = {
|
||||
source: string;
|
||||
error: string;
|
||||
custom_colors: Record<string, string>;
|
||||
source: string | null;
|
||||
error: string | null;
|
||||
custom_colors?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type MaterialDesign3SchemeStorage = {
|
||||
source: MaterialDesign3SchemeSource;
|
||||
scheme: MaterialDesign3Scheme;
|
||||
source?: MaterialDesign3SchemeSource;
|
||||
scheme?: MaterialDesign3Scheme;
|
||||
cssVariables?: string;
|
||||
scssVariables?: string;
|
||||
jsVariables?: string;
|
||||
};
|
||||
|
@@ -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;
|
||||
@@ -22,6 +30,30 @@ export type ColorDescription = {
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -35,3 +67,9 @@ export type ColorShifting = {
|
||||
chroma: number;
|
||||
lightness: number;
|
||||
};
|
||||
|
||||
export type SchemeStorage =
|
||||
| QSchemeStorage
|
||||
| SwatchSchemeStorage
|
||||
| MaterialDesign2SchemeStorage
|
||||
| MaterialDesign3SchemeStorage;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -23,18 +23,18 @@ export function Darkens({ color, darkens, mix, step, maximum, copyMode }: Darken
|
||||
switch (mix) {
|
||||
case 'progressive':
|
||||
for (let i = 1; i <= darkens; i++) {
|
||||
const darkenColor = colorFn.darken(last(darkenColors), step);
|
||||
const darkenColor = colorFn.darken(last(darkenColors) ?? '', step ?? 0);
|
||||
darkenColors.push(darkenColor);
|
||||
}
|
||||
break;
|
||||
case 'linear':
|
||||
for (let i = 1; i <= darkens; i++) {
|
||||
const darkenColor = colorFn.darken(color, step * i);
|
||||
const darkenColor = colorFn.darken(color, (step ?? 0) * i);
|
||||
darkenColors.push(darkenColor);
|
||||
}
|
||||
break;
|
||||
case 'average': {
|
||||
const interval = maximum / darkens / 100;
|
||||
const interval = (maximum ?? 0) / darkens / 100;
|
||||
for (let i = 1; i <= darkens; i++) {
|
||||
const darkenColor = colorFn.darken(color, interval * i);
|
||||
darkenColors.push(darkenColor);
|
||||
|
@@ -23,18 +23,18 @@ export function Lightens({ color, lightens, mix, step, maximum, copyMode }: Ligh
|
||||
switch (mix) {
|
||||
case 'progressive':
|
||||
for (let i = 1; i <= lightens; i++) {
|
||||
const lightenColor = colorFn.lighten(last(lightenColors), step);
|
||||
const lightenColor = colorFn.lighten(last(lightenColors) ?? '', step ?? 0);
|
||||
lightenColors.push(lightenColor);
|
||||
}
|
||||
break;
|
||||
case 'linear':
|
||||
for (let i = 1; i <= lightens; i++) {
|
||||
const lightenColor = colorFn.lighten(color, step * i);
|
||||
const lightenColor = colorFn.lighten(color, (step ?? 0) * i);
|
||||
lightenColors.push(lightenColor);
|
||||
}
|
||||
break;
|
||||
case 'average': {
|
||||
const interval = maximum / lightens / 100;
|
||||
const interval = (maximum ?? 0) / lightens / 100;
|
||||
for (let i = 1; i <= lightens; i++) {
|
||||
const lightenColor = colorFn.lighten(color, interval * i);
|
||||
lightenColors.push(lightenColor);
|
||||
|
13
src/page-components/scheme/ColorEntry.module.css
Normal file
13
src/page-components/scheme/ColorEntry.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@layer components {
|
||||
.delete_btn {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-yuebai);
|
||||
background-color: oklch(from var(--color-danger) l c h / 0.25);
|
||||
&:hover {
|
||||
background-color: oklch(from var(--color-danger-hover) l c h / 0.65);
|
||||
}
|
||||
&:active {
|
||||
background-color: oklch(from var(--color-danger-active) l c h / 0.65);
|
||||
}
|
||||
}
|
||||
}
|
38
src/page-components/scheme/ColorEntry.tsx
Normal file
38
src/page-components/scheme/ColorEntry.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ActionIcon } from '../../components/ActionIcon';
|
||||
import { FloatColorPicker } from '../../components/FloatColorPicker';
|
||||
import styles from './colorEntry.module.css';
|
||||
|
||||
export type IdenticalColorEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type ColorEntryProps = {
|
||||
entry: IdenticalColorEntry;
|
||||
onDelete?: (index: string) => void;
|
||||
};
|
||||
|
||||
export function ColorEntry({ entry, onDelete }: ColorEntryProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="input_wrapper">
|
||||
<input type="text" name={`name_${entry.id}`} defaultValue={entry.name} />
|
||||
</div>
|
||||
<div>
|
||||
<FloatColorPicker
|
||||
name={`color_${entry.id}`}
|
||||
color={isEmpty(entry.color) ? undefined : entry.color}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ActionIcon
|
||||
icon="tabler:trash"
|
||||
extendClassName={styles.delete_btn}
|
||||
onClick={() => onDelete?.(entry.id)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
6
src/page-components/scheme/CorruptedScheme.module.css
Normal file
6
src/page-components/scheme/CorruptedScheme.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@layer pages {
|
||||
.corrupted {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
8
src/page-components/scheme/CorruptedScheme.tsx
Normal file
8
src/page-components/scheme/CorruptedScheme.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import styles from './CorruptedScheme.module.css';
|
||||
export function CorruptedScheme() {
|
||||
return (
|
||||
<div className="center">
|
||||
<div className={styles.corrupted}>Unrecognizable or corrupted scheme</div>
|
||||
</div>
|
||||
);
|
||||
}
|
16
src/page-components/scheme/Export.module.css
Normal file
16
src/page-components/scheme/Export.module.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@layer pages {
|
||||
.export_layout {
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
.tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
}
|
50
src/page-components/scheme/Export.tsx
Normal file
50
src/page-components/scheme/Export.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { HSegmentedControl } from '../../components/HSegmentedControl';
|
||||
import { Labeled } from '../../components/Labeled';
|
||||
import { ScrollArea } from '../../components/ScrollArea';
|
||||
import { useCopy } from '../../hooks/useCopy';
|
||||
import { Option, SchemeContent, SchemeStorage } from '../../models';
|
||||
import styles from './Export.module.css';
|
||||
|
||||
const exportOptions: Option[] = [
|
||||
{ label: 'CSS', value: 'css' },
|
||||
{ label: 'SCSS', value: 'scss' },
|
||||
{ label: 'Javascript Object', value: 'js_object' },
|
||||
];
|
||||
|
||||
type SchemeExportProps = {
|
||||
scheme: SchemeContent<SchemeStorage>;
|
||||
};
|
||||
|
||||
export function SchemeExport({ scheme }: SchemeExportProps) {
|
||||
const [activeExport, setActiveExport] = useState<Option['value']>(exportOptions[0].value);
|
||||
const exportContent = useMemo(() => {
|
||||
switch (activeExport) {
|
||||
case 'css':
|
||||
return scheme.schemeStorage.cssVariables;
|
||||
case 'scss':
|
||||
return scheme.schemeStorage.scssVariables;
|
||||
case 'js_object':
|
||||
return scheme.schemeStorage.jsVariables;
|
||||
}
|
||||
}, [scheme, activeExport]);
|
||||
const copyToClipboard = useCopy();
|
||||
|
||||
return (
|
||||
<ScrollArea enableY>
|
||||
<div className={styles.export_layout}>
|
||||
<div className={styles.tools}>
|
||||
<Labeled label="Export Options" inline>
|
||||
<HSegmentedControl
|
||||
options={exportOptions}
|
||||
value={activeExport}
|
||||
onChange={setActiveExport}
|
||||
/>
|
||||
</Labeled>
|
||||
<button onClick={() => copyToClipboard(exportContent)}>Copy</button>
|
||||
</div>
|
||||
<pre>{exportContent}</pre>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/M2Scheme.tsx
Normal file
35
src/page-components/scheme/M2Scheme.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { Tab } from '../../components/Tab';
|
||||
import { MaterialDesign2SchemeStorage } from '../../material-2-scheme';
|
||||
import { SchemeContent } from '../../models';
|
||||
import { SchemeExport } from './Export';
|
||||
import { M2SchemeBuilder } from './m2-scheme/Builder';
|
||||
import { M2SchemePreview } from './m2-scheme/Preview';
|
||||
|
||||
const tabOptions = [
|
||||
{ title: 'Overview', id: 'overview' },
|
||||
{ title: 'Builder', id: 'builder' },
|
||||
{ title: 'Exports', id: 'export' },
|
||||
];
|
||||
|
||||
type M2SchemeProps = {
|
||||
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
|
||||
};
|
||||
|
||||
export function M2Scheme({ scheme }: M2SchemeProps) {
|
||||
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
|
||||
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
|
||||
{isEqual(activeTab, 'overview') && <M2SchemePreview scheme={scheme} />}
|
||||
{isEqual(activeTab, 'builder') && (
|
||||
<M2SchemeBuilder scheme={scheme} onBuildComplete={() => setActiveTab('overview')} />
|
||||
)}
|
||||
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
|
||||
</>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/M3Scheme.tsx
Normal file
35
src/page-components/scheme/M3Scheme.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isEqual, isNil } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { Tab } from '../../components/Tab';
|
||||
import { MaterialDesign3SchemeStorage } from '../../material-3-scheme';
|
||||
import { SchemeContent } from '../../models';
|
||||
import { SchemeExport } from './Export';
|
||||
import { M3SchemeBuilder } from './m3-scheme/Builder';
|
||||
import { M3SchemePreview } from './m3-scheme/Preview';
|
||||
|
||||
const tabOptions = [
|
||||
{ title: 'Overview', id: 'overview' },
|
||||
{ title: 'Builder', id: 'builder' },
|
||||
{ title: 'Exports', id: 'export' },
|
||||
];
|
||||
|
||||
type M3SchemeProps = {
|
||||
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
|
||||
};
|
||||
|
||||
export function M3Scheme({ scheme }: M3SchemeProps) {
|
||||
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
|
||||
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
|
||||
{isEqual(activeTab, 'overview') && <M3SchemePreview scheme={scheme} />}
|
||||
{isEqual(activeTab, 'builder') && (
|
||||
<M3SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
|
||||
)}
|
||||
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
|
||||
</>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/QScheme.tsx
Normal file
35
src/page-components/scheme/QScheme.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/SwatchScheme.tsx
Normal file
35
src/page-components/scheme/SwatchScheme.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
}
|
46
src/page-components/scheme/m2-scheme/Builder.module.css
Normal file
46
src/page-components/scheme/m2-scheme/Builder.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
178
src/page-components/scheme/m2-scheme/Builder.tsx
Normal file
178
src/page-components/scheme/m2-scheme/Builder.tsx
Normal 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>
|
||||
);
|
||||
}
|
41
src/page-components/scheme/m2-scheme/Preview.module.css
Normal file
41
src/page-components/scheme/m2-scheme/Preview.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
94
src/page-components/scheme/m2-scheme/Preview.tsx
Normal file
94
src/page-components/scheme/m2-scheme/Preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
46
src/page-components/scheme/m3-scheme/Builder.module.css
Normal file
46
src/page-components/scheme/m3-scheme/Builder.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
165
src/page-components/scheme/m3-scheme/Builder.tsx
Normal file
165
src/page-components/scheme/m3-scheme/Builder.tsx
Normal 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>
|
||||
);
|
||||
}
|
89
src/page-components/scheme/m3-scheme/Preview.module.css
Normal file
89
src/page-components/scheme/m3-scheme/Preview.module.css
Normal 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;
|
||||
}
|
||||
}
|
272
src/page-components/scheme/m3-scheme/Preview.tsx
Normal file
272
src/page-components/scheme/m3-scheme/Preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
38
src/page-components/scheme/q-scheme/Builder.module.css
Normal file
38
src/page-components/scheme/q-scheme/Builder.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
362
src/page-components/scheme/q-scheme/Builder.tsx
Normal file
362
src/page-components/scheme/q-scheme/Builder.tsx
Normal 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>
|
||||
);
|
||||
}
|
35
src/page-components/scheme/q-scheme/Preview.module.css
Normal file
35
src/page-components/scheme/q-scheme/Preview.module.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@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-xl) var(--spacing-m);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: var(--spacing-xs);
|
||||
h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: bold;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
}
|
||||
.preview_cell {
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xxs);
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1.5em;
|
||||
.wacg {
|
||||
font-size: var(--font-size-xxs);
|
||||
line-height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
100
src/page-components/scheme/q-scheme/Preview.tsx
Normal file
100
src/page-components/scheme/q-scheme/Preview.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useColorFunction } from '../../../ColorFunctionContext';
|
||||
import { ScrollArea } from '../../../components/ScrollArea';
|
||||
import { SchemeContent } from '../../../models';
|
||||
import { Baseline, ColorSet, QSchemeStorage } from '../../../q-scheme';
|
||||
import styles from './Preview.module.css';
|
||||
|
||||
type PreviewCellProps = {
|
||||
bg: string;
|
||||
fg: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function PreviewCell({ bg, fg, children }: PreviewCellProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const wacgRatio = useMemo(() => {
|
||||
try {
|
||||
if (!colorFn) return null;
|
||||
return colorFn.wacg_relative_contrast(fg, bg);
|
||||
} catch (e) {
|
||||
console.error('[Error on calc WACG Ratio]', e);
|
||||
}
|
||||
return null;
|
||||
}, [bg, fg]);
|
||||
|
||||
return (
|
||||
<div className={styles.preview_cell} style={{ backgroundColor: `#${bg}`, color: `#${fg}` }}>
|
||||
<span>{children}</span>
|
||||
{wacgRatio && <span className={styles.wacg}>WACG {wacgRatio?.toFixed(2)}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PreviewCellSetProps = {
|
||||
colorSet: ColorSet;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function PreviewCellSetGroup({ colorSet, name }: PreviewCellSetProps) {
|
||||
return (
|
||||
<>
|
||||
<PreviewCell bg={colorSet.root} fg={colorSet.onRoot}>
|
||||
{name}
|
||||
</PreviewCell>
|
||||
<PreviewCell bg={colorSet.hover} fg={colorSet.onHover}>
|
||||
{name} Hover
|
||||
</PreviewCell>
|
||||
<PreviewCell bg={colorSet.active} fg={colorSet.onActive}>
|
||||
{name} Active
|
||||
</PreviewCell>
|
||||
<PreviewCell bg={colorSet.focus} fg={colorSet.onFocus}>
|
||||
{name} Focus
|
||||
</PreviewCell>
|
||||
<PreviewCell bg={colorSet.disabled} fg={colorSet.onDisabled}>
|
||||
{name} Disabled
|
||||
</PreviewCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type PreviewBlockProps = {
|
||||
baseline: Baseline;
|
||||
title: string;
|
||||
};
|
||||
|
||||
function PreviewBlock({ baseline, title }: PreviewBlockProps) {
|
||||
return (
|
||||
<div className={styles.preview_block} style={{ backgroundColor: `#${baseline.background}` }}>
|
||||
<h2 style={{ color: `#${baseline.foreground}`, gridColumn: '1 / span 5' }}>{title}</h2>
|
||||
<PreviewCellSetGroup colorSet={baseline.primary} name="Primary" />
|
||||
{baseline.secondary && <PreviewCellSetGroup colorSet={baseline.secondary} name="Secondary" />}
|
||||
{baseline.tertiary && <PreviewCellSetGroup colorSet={baseline.tertiary} name="Tertiary" />}
|
||||
{baseline.accent && <PreviewCellSetGroup colorSet={baseline.accent} name="Accent" />}
|
||||
<PreviewCellSetGroup colorSet={baseline.neutral} name="Neutral" />
|
||||
<PreviewCellSetGroup colorSet={baseline.danger} name="Danger" />
|
||||
<PreviewCellSetGroup colorSet={baseline.warning} name="Warning" />
|
||||
<PreviewCellSetGroup colorSet={baseline.success} name="Success" />
|
||||
<PreviewCellSetGroup colorSet={baseline.info} name="Info" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PreviewProps = {
|
||||
scheme: SchemeContent<QSchemeStorage>;
|
||||
};
|
||||
|
||||
export function QSchemePreview({ scheme }: PreviewProps) {
|
||||
return (
|
||||
<ScrollArea enableY>
|
||||
<div className={styles.preview_layout}>
|
||||
{scheme.schemeStorage.scheme?.light && (
|
||||
<PreviewBlock baseline={scheme.schemeStorage.scheme.light} title="Light Scheme" />
|
||||
)}
|
||||
{scheme.schemeStorage.scheme?.dark && (
|
||||
<PreviewBlock baseline={scheme.schemeStorage.scheme.dark} title="Dark Scheme" />
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
49
src/page-components/scheme/swatch-scheme/Builder.module.css
Normal file
49
src/page-components/scheme/swatch-scheme/Builder.module.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@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;
|
||||
}
|
||||
.parameter_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;
|
||||
}
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
247
src/page-components/scheme/swatch-scheme/Builder.tsx
Normal file
247
src/page-components/scheme/swatch-scheme/Builder.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ColorShifting, SwatchEntry, SwatchSchemeSetting } from 'color-module';
|
||||
import { includes, isEmpty, isEqual, isNaN } from 'lodash-es';
|
||||
import { useActionState, useCallback, useMemo, useState } from 'react';
|
||||
import { useColorFunction } from '../../../ColorFunctionContext';
|
||||
import { ScrollArea } from '../../../components/ScrollArea';
|
||||
import { Switch } from '../../../components/Switch';
|
||||
import { SchemeContent } from '../../../models';
|
||||
import { useUpdateScheme } from '../../../stores/schemes';
|
||||
import {
|
||||
QSwatchEntry,
|
||||
QSwatchSchemeSetting,
|
||||
SwatchScheme,
|
||||
SwatchSchemeStorage,
|
||||
} from '../../../swatch_scheme';
|
||||
import { mapToObject } from '../../../utls';
|
||||
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
|
||||
import styles from './Builder.module.css';
|
||||
|
||||
type SwatchSchemeBuilderProps = {
|
||||
scheme: SchemeContent<SwatchSchemeStorage>;
|
||||
onBuildCompleted?: () => void;
|
||||
};
|
||||
|
||||
export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBuilderProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const updateScheme = useUpdateScheme(scheme.id);
|
||||
const originalColors = useMemo(() => {
|
||||
return (scheme.schemeStorage.source?.colors ?? []).map(
|
||||
(color, index) =>
|
||||
({ id: `oc_${index}`, name: color.name, color: color.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(() => {
|
||||
return [...originalColors, ...newColors]
|
||||
.map((color) => color.id)
|
||||
.filter((id) => !includes(deleted, id));
|
||||
}, [originalColors, newColors, deleted]);
|
||||
const defaultSetting = useMemo(() => {
|
||||
try {
|
||||
if (!colorFn) throw 'Web Assembly functions is not available';
|
||||
const defaultValues = colorFn.swatch_scheme_default_settings();
|
||||
if (scheme.schemeStorage.source?.setting) {
|
||||
return new SwatchSchemeSetting(
|
||||
scheme.schemeStorage.source.setting.amount ?? defaultValues.amount,
|
||||
scheme.schemeStorage.source.setting.min_lightness ?? defaultValues.min_lightness,
|
||||
scheme.schemeStorage.source.setting.max_lightness ?? defaultValues.max_lightness,
|
||||
scheme.schemeStorage.source.setting.include_primary ?? defaultValues.include_primary,
|
||||
new ColorShifting(
|
||||
scheme.schemeStorage.source.setting.dark_convert.chroma ??
|
||||
defaultValues.dark_convert.chroma,
|
||||
scheme.schemeStorage.source.setting.dark_convert.lightness ??
|
||||
defaultValues.dark_convert.lightness,
|
||||
),
|
||||
);
|
||||
}
|
||||
return defaultValues;
|
||||
} catch (e) {
|
||||
console.error('[Q scheme builder]', e);
|
||||
}
|
||||
return null;
|
||||
}, [scheme.schemeStorage.source]);
|
||||
|
||||
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
|
||||
(_state, formData) => {
|
||||
const errMsg = new Map<string, string>();
|
||||
|
||||
try {
|
||||
const swatchAmount = Number(formData.get('amount'));
|
||||
if (isNaN(swatchAmount) || swatchAmount <= 0) {
|
||||
errMsg.set('amount', 'MUST be a positive number');
|
||||
}
|
||||
if (swatchAmount > 30) {
|
||||
errMsg.set('amount', 'MUST be less than 30');
|
||||
}
|
||||
|
||||
const minLightness = Number(formData.get('min_lightness'));
|
||||
if (isNaN(minLightness) || minLightness < 0 || minLightness > 100) {
|
||||
errMsg.set('min', 'MUST be a number between 0 and 100');
|
||||
}
|
||||
|
||||
const maxLightness = Number(formData.get('max_lightness'));
|
||||
if (isNaN(maxLightness) || maxLightness < 0 || maxLightness > 100) {
|
||||
errMsg.set('max', 'MUST be a number between 0 and 100');
|
||||
}
|
||||
|
||||
const includePrimary = isEqual(formData.get('include_primary'), 'true');
|
||||
const darkConvertChroma = Number(formData.get('dark_chroma')) / 100.0;
|
||||
const darkConvertLightness = Number(formData.get('dark_lightness')) / 100.0;
|
||||
|
||||
const swatchSetting = new SwatchSchemeSetting(
|
||||
swatchAmount,
|
||||
minLightness / 100.0,
|
||||
maxLightness / 100.0,
|
||||
includePrimary,
|
||||
new ColorShifting(darkConvertChroma, darkConvertLightness),
|
||||
);
|
||||
const dumpedSettings = swatchSetting.toJsValue() as QSwatchSchemeSetting;
|
||||
const entries: SwatchEntry[] = [];
|
||||
for (const key of colorKeys) {
|
||||
const name = String(formData.get(`name_${key}`));
|
||||
const color = String(formData.get(`color_${key}`));
|
||||
if (isEmpty(name) || isEmpty(color)) continue;
|
||||
entries.push(new SwatchEntry(name, color));
|
||||
}
|
||||
const dumpedEntries = entries.map((entry) => entry.toJsValue() as QSwatchEntry);
|
||||
if (isEmpty(entries)) {
|
||||
errMsg.set('color', 'At least one color is required');
|
||||
}
|
||||
|
||||
if (!isEmpty(errMsg)) return errMsg;
|
||||
|
||||
const generatedScheme = colorFn?.generate_swatch_scheme(entries, swatchSetting);
|
||||
console.debug('[generated scheme]', generatedScheme);
|
||||
updateScheme((prev) => {
|
||||
prev.schemeStorage.source = {
|
||||
colors: dumpedEntries,
|
||||
setting: dumpedSettings,
|
||||
};
|
||||
prev.schemeStorage.scheme = mapToObject(generatedScheme[0]) as SwatchScheme;
|
||||
prev.schemeStorage.cssVariables = generatedScheme[1];
|
||||
prev.schemeStorage.scssVariables = generatedScheme[2];
|
||||
prev.schemeStorage.jsVariables = generatedScheme[3];
|
||||
return prev;
|
||||
});
|
||||
|
||||
onBuildCompleted?.();
|
||||
} catch (e) {
|
||||
console.error('[build swatch scheme]', e);
|
||||
}
|
||||
|
||||
return errMsg;
|
||||
},
|
||||
new Map<string, string>(),
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollArea enableY>
|
||||
<form action={handleSubmitAction} className={styles.builder_layout}>
|
||||
<h5 className={styles.segment_title}>Automatic Parameters</h5>
|
||||
<label className={styles.label}>Swatch Amount</label>
|
||||
<div className={styles.parameter_row}>
|
||||
<div className="input_wrapper">
|
||||
<input
|
||||
type="number"
|
||||
name="amount"
|
||||
defaultValue={defaultSetting?.amount ?? 0}
|
||||
className={styles.parameter_input}
|
||||
/>
|
||||
</div>
|
||||
{errMsg.has('amount') && <span className={styles.error_msg}>{errMsg.get('amount')}</span>}
|
||||
</div>
|
||||
<label className={styles.label}>Minimum Lightness</label>
|
||||
<div className={styles.parameter_row}>
|
||||
<div className="input_wrapper">
|
||||
<input
|
||||
type="number"
|
||||
name="min_lightness"
|
||||
defaultValue={((defaultSetting?.min_lightness ?? 0) * 100).toFixed(2)}
|
||||
className={styles.parameter_input}
|
||||
/>
|
||||
<span>%</span>
|
||||
</div>
|
||||
{errMsg.has('min') && <span className={styles.error_msg}>{errMsg.get('min')}</span>}
|
||||
</div>
|
||||
<label className={styles.label}>Maximum Lightness</label>
|
||||
<div className={styles.parameter_row}>
|
||||
<div className="input_wrapper">
|
||||
<input
|
||||
type="number"
|
||||
name="max_lightness"
|
||||
defaultValue={((defaultSetting?.max_lightness ?? 0) * 100).toFixed(2)}
|
||||
className={styles.parameter_input}
|
||||
/>
|
||||
<span>%</span>
|
||||
</div>
|
||||
{errMsg.has('max') && <span className={styles.error_msg}>{errMsg.get('max')}</span>}
|
||||
</div>
|
||||
<label className={styles.label}>Include Primary Color</label>
|
||||
<div>
|
||||
<Switch name="include_primary" checked={defaultSetting?.include_primary ?? false} />
|
||||
</div>
|
||||
<label style={{ gridColumn: 2 }}>Chroma shifting</label>
|
||||
<label style={{ gridColumn: 3 }}>Lightness shifting</label>
|
||||
<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}>Swatch 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((c) => !includes(deleted, c.id))
|
||||
.map((color) => (
|
||||
<ColorEntry
|
||||
key={color.id}
|
||||
entry={color}
|
||||
onDelete={(index) => setDeleted((prev) => [...prev, index])}
|
||||
/>
|
||||
))}
|
||||
{newColors
|
||||
.filter((c) => !includes(deleted, c.id))
|
||||
.map((color) => (
|
||||
<ColorEntry
|
||||
key={color.id}
|
||||
entry={color}
|
||||
onDelete={(index) => setDeleted((prev) => [...prev, index])}
|
||||
/>
|
||||
))}
|
||||
{errMsg.has('color') && (
|
||||
<div style={{ gridColumn: '1 / span 2' }}>
|
||||
<span className={styles.error_msg}>{errMsg.get('color')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ gridColumn: '2 / span 2' }}>
|
||||
<button type="submit" className="primary">
|
||||
Build Scheme
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
27
src/page-components/scheme/swatch-scheme/Preview.module.css
Normal file
27
src/page-components/scheme/swatch-scheme/Preview.module.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@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);
|
||||
}
|
||||
.scheme_block {
|
||||
display: grid;
|
||||
gap: var(--spacing-m) var(--spacing-xs);
|
||||
align-items: center;
|
||||
}
|
||||
.scheme_cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-s);
|
||||
.block {
|
||||
height: 3em;
|
||||
}
|
||||
.label {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
67
src/page-components/scheme/swatch-scheme/Preview.tsx
Normal file
67
src/page-components/scheme/swatch-scheme/Preview.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ScrollArea } from '../../../components/ScrollArea';
|
||||
import { SchemeContent } from '../../../models';
|
||||
import { SwatchSchemeStorage } from '../../../swatch_scheme';
|
||||
import styles from './Preview.module.css';
|
||||
|
||||
type SwatchCellProps = {
|
||||
color: string;
|
||||
};
|
||||
|
||||
export function SwatchCell({ color }: SwatchCellProps) {
|
||||
return (
|
||||
<div className={styles.scheme_cell}>
|
||||
<div className={styles.block} style={{ backgroundColor: `#${color}` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SchemeBlockProps = {
|
||||
amount: number;
|
||||
scheme: Record<string, string[]>;
|
||||
};
|
||||
|
||||
export function SchemeBlock({ amount, scheme }: SchemeBlockProps) {
|
||||
return (
|
||||
<div
|
||||
className={styles.scheme_block}
|
||||
style={{ gridTemplateColumns: `minmax(120px, 1fr) repeat(${amount}, 1fr)` }}>
|
||||
<div />
|
||||
{Array.from({ length: amount }).map((_, index) => (
|
||||
<div key={index} className={styles.scheme_cell}>
|
||||
<div className={styles.label}>{index * 100}</div>
|
||||
</div>
|
||||
))}
|
||||
{Object.entries(scheme).map(([name, colors]) => (
|
||||
<>
|
||||
<h6>{name}</h6>
|
||||
{colors.map((color, index) => (
|
||||
<SwatchCell key={index} color={color} />
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SwatchSchemePreviewProps = {
|
||||
scheme: SchemeContent<SwatchSchemeStorage>;
|
||||
};
|
||||
|
||||
export function SwatchSchemePreview({ scheme }: SwatchSchemePreviewProps) {
|
||||
return (
|
||||
<ScrollArea enableY>
|
||||
<div className={styles.preview_layout}>
|
||||
<h2>Light Scheme</h2>
|
||||
<SchemeBlock
|
||||
amount={scheme.schemeStorage.source?.setting?.amount ?? 0}
|
||||
scheme={scheme.schemeStorage.scheme!.light}
|
||||
/>
|
||||
<h2>Dark Scheme</h2>
|
||||
<SchemeBlock
|
||||
amount={scheme.schemeStorage.source?.setting?.amount ?? 0}
|
||||
scheme={scheme.schemeStorage.scheme!.dark}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
@@ -20,6 +20,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
.scheme_item {
|
||||
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 8);
|
||||
&.selected {
|
||||
@@ -53,6 +54,42 @@
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.active_btn {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-yuebai);
|
||||
background-color: oklch(from var(--color-info) l c h / 0.25);
|
||||
&:hover {
|
||||
background-color: oklch(from var(--color-info-hover) l c h / 0.65);
|
||||
}
|
||||
&:active {
|
||||
background-color: oklch(from var(--color-info-active) l c h / 0.65);
|
||||
}
|
||||
&.deactive {
|
||||
background-color: oklch(from var(--color-warn) l c h / 0.25);
|
||||
&:hover {
|
||||
background-color: oklch(from var(--color-warn-hover) l c h / 0.65);
|
||||
}
|
||||
&:active {
|
||||
background-color: oklch(from var(--color-warn-active) l c h / 0.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
.active_badge {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-yuebai);
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
}
|
||||
.empty_prompt {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { Icon } from '@iconify/react/dist/iconify.js';
|
||||
import cx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { activeSchemeAtom, useSchemeList } from '../../stores/schemes';
|
||||
import { ActionIcon } from '../../components/ActionIcon';
|
||||
import { Badge } from '../../components/Badge';
|
||||
import { SchemeSign } from '../../components/SchemeSign';
|
||||
import { activeSchemeAtom, useRemoveScheme, useSchemeList } from '../../stores/schemes';
|
||||
import styles from './SchemeList.module.css';
|
||||
|
||||
function OperateButtons() {
|
||||
@@ -25,9 +26,20 @@ type SchemeItemProps = {
|
||||
function SchemeItem({ item }: SchemeItemProps) {
|
||||
const navParams = useParams();
|
||||
const navigate = useNavigate();
|
||||
const activedScheme = useAtomValue(activeSchemeAtom);
|
||||
const [activedScheme, setActiveScheme] = useAtom(activeSchemeAtom);
|
||||
const removeScheme = useRemoveScheme(item.id);
|
||||
const isActived = useMemo(() => isEqual(activedScheme, item.id), [activedScheme, item.id]);
|
||||
const isSelected = useMemo(() => isEqual(navParams['id'], item.id), [navParams, item.id]);
|
||||
const handleActiveScheme = useCallback(() => {
|
||||
setActiveScheme((prev) => (prev ? null : item.id));
|
||||
}, [item]);
|
||||
const handleRemoveScheme = useCallback(() => {
|
||||
removeScheme();
|
||||
if (isActived) {
|
||||
setActiveScheme(null);
|
||||
}
|
||||
navigate(-1);
|
||||
}, [item, isActived]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -35,11 +47,23 @@ function SchemeItem({ item }: SchemeItemProps) {
|
||||
onClick={() => navigate(item.id)}>
|
||||
<div className={styles.name}>{item.name}</div>
|
||||
<div className={styles.status}>
|
||||
<div className={styles.create_time}>
|
||||
created at {dayjs(item.createdAt).format('YYYY-MM-DD')}
|
||||
</div>
|
||||
<SchemeSign scheme={item.type} short />
|
||||
{isActived && <Badge extendClassName={styles.active_badge}>ACTIVE</Badge>}
|
||||
<div className="spacer"></div>
|
||||
{isActived && <Icon icon="tabler:check" className={styles.active_icon} />}
|
||||
{isSelected && (
|
||||
<>
|
||||
<ActionIcon
|
||||
icon="tabler:trash"
|
||||
extendClassName={styles.delete_btn}
|
||||
onClick={handleRemoveScheme}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon="tabler:checkbox"
|
||||
extendClassName={cx(styles.active_btn, isActived && styles.deactive)}
|
||||
onClick={handleActiveScheme}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -23,18 +23,18 @@ export function Shades({ color, shades, mix, step, maximum, copyMode }: ShadesLi
|
||||
switch (mix) {
|
||||
case 'progressive':
|
||||
for (let i = 1; i <= shades; i++) {
|
||||
const shade = colorFn!.shade(last(genColors), step);
|
||||
const shade = colorFn!.shade(last(genColors) ?? '', step ?? 0);
|
||||
genColors.push(shade);
|
||||
}
|
||||
break;
|
||||
case 'linear':
|
||||
for (let i = 1; i <= shades; i++) {
|
||||
const shade = colorFn!.shade(color, step * i);
|
||||
const shade = colorFn!.shade(color, (step ?? 0) * i);
|
||||
genColors.push(shade);
|
||||
}
|
||||
break;
|
||||
case 'average': {
|
||||
const interval = maximum / shades / 100;
|
||||
const interval = (maximum ?? 0) / shades / 100;
|
||||
for (let i = 1; i <= shades; i++) {
|
||||
const shade = colorFn!.shade(color, interval * i);
|
||||
genColors.push(shade);
|
||||
|
@@ -23,18 +23,18 @@ export function Tints({ color, tints, mix, step, maximum, copyMode }: TintsListP
|
||||
switch (mix) {
|
||||
case 'progressive':
|
||||
for (let i = 1; i <= tints; i++) {
|
||||
const tint = colorFn!.tint(last(genColors), step);
|
||||
const tint = colorFn!.tint(last(genColors) ?? '', step ?? 0);
|
||||
genColors.push(tint);
|
||||
}
|
||||
break;
|
||||
case 'linear':
|
||||
for (let i = 1; i <= tints; i++) {
|
||||
const tint = colorFn!.tint(color, step * i);
|
||||
const tint = colorFn!.tint(color, (step ?? 0) * i);
|
||||
genColors.push(tint);
|
||||
}
|
||||
break;
|
||||
case 'average': {
|
||||
const interval = maximum / tints / 100;
|
||||
const interval = (maximum ?? 0) / tints / 100;
|
||||
for (let i = 1; i <= tints; i++) {
|
||||
const tint = colorFn!.tint(color, interval * i);
|
||||
genColors.push(tint);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user