pub(crate) mod oklch;
use crate::resolve::{ResolvedCall, ResolvedValue};
struct Hue {
name: &'static str,
hue: f64,
chroma: f64,
}
const HUES: &[Hue] = &[
Hue {
name: "red",
hue: 28.0,
chroma: 1.00,
},
Hue {
name: "rose",
hue: 6.0,
chroma: 1.00,
},
Hue {
name: "orange",
hue: 55.0,
chroma: 1.00,
},
Hue {
name: "amber",
hue: 80.0,
chroma: 1.08,
},
Hue {
name: "lime",
hue: 120.0,
chroma: 1.02,
},
Hue {
name: "green",
hue: 158.0,
chroma: 1.00,
},
Hue {
name: "teal",
hue: 188.0,
chroma: 1.00,
},
Hue {
name: "sky",
hue: 222.0,
chroma: 1.00,
},
Hue {
name: "blue",
hue: 255.0,
chroma: 1.05,
},
Hue {
name: "purple",
hue: 285.0,
chroma: 1.05,
},
Hue {
name: "gray",
hue: 250.0,
chroma: 0.00,
},
];
struct Tier {
suffix: &'static str,
light: (f64, f64),
dark: (f64, f64),
}
const TIERS: &[Tier] = &[
Tier {
suffix: "-wash",
light: (0.95, 0.022),
dark: (0.315, 0.048),
},
Tier {
suffix: "-soft",
light: (0.86, 0.072),
dark: (0.450, 0.082),
},
Tier {
suffix: "",
light: (0.72, 0.127),
dark: (0.610, 0.130),
},
Tier {
suffix: "-ink",
light: (0.520, 0.150),
dark: (0.825, 0.100),
},
];
const ALIASES: &[(&str, &str)] = &[
("yellow", "amber"),
("pink", "rose"),
("indigo", "purple"),
("cyan", "teal"),
];
pub fn palette_vars() -> Vec<(String, ResolvedValue)> {
let mut out = Vec::with_capacity(HUES.len() * TIERS.len() + ALIASES.len());
for hue in HUES {
for tier in TIERS {
let arm = |(l, c): (f64, f64)| {
ResolvedValue::Hex(oklch::oklch_to_hex(l, c * hue.chroma, hue.hue))
};
out.push((
format!("{}{}", hue.name, tier.suffix),
light_dark(arm(tier.light), arm(tier.dark)),
));
}
}
for (alias, target) in ALIASES {
out.push((
alias.to_string(),
ResolvedValue::LiveVar {
name: (*target).into(),
raw: false,
},
));
}
out
}
fn light_dark(light: ResolvedValue, dark: ResolvedValue) -> ResolvedValue {
ResolvedValue::Call(ResolvedCall {
name: "light-dark".into(),
args: vec![light, dark],
})
}
#[cfg(test)]
mod tests {
use super::*;
fn vars() -> Vec<(String, ResolvedValue)> {
palette_vars()
}
#[test]
fn every_hue_has_four_tiers_plus_aliases() {
let v = vars();
assert_eq!(v.len(), HUES.len() * TIERS.len() + ALIASES.len());
for suffix in ["-wash", "-soft", "", "-ink"] {
assert!(
v.iter().any(|(n, _)| *n == format!("teal{suffix}")),
"missing teal{suffix}"
);
}
}
#[test]
fn tiers_are_light_dark_literals() {
let v = vars();
let (_, teal) = v.iter().find(|(n, _)| n == "teal").unwrap();
match teal {
ResolvedValue::Call(c) => {
assert_eq!(c.name, "light-dark");
assert!(matches!(c.args[0], ResolvedValue::Hex(_)));
assert!(matches!(c.args[1], ResolvedValue::Hex(_)));
}
other => panic!("expected light-dark(), got {other:?}"),
}
}
#[test]
fn aliases_point_at_their_hue() {
let v = vars();
let (_, yellow) = v.iter().find(|(n, _)| n == "yellow").unwrap();
assert!(matches!(yellow, ResolvedValue::LiveVar { name, .. } if name == "amber"));
}
#[test]
fn grey_is_neutral() {
let v = vars();
let (_, gray) = v.iter().find(|(n, _)| n == "gray").unwrap();
let ResolvedValue::Call(c) = gray else {
panic!()
};
for arm in &c.args {
let ResolvedValue::Hex(h) = arm else { panic!() };
assert_eq!(&h[0..2], &h[2..4], "grey not neutral: {h}");
assert_eq!(&h[2..4], &h[4..6], "grey not neutral: {h}");
}
}
}