#![cfg_attr(feature = "fluent", doc = r#"
```no_run
use chocodye::{Dye, Lang};
let bundle = Lang::English.into_bundle();
let mut dyes = Dye::VALUES;
dyes.sort_unstable_by_key(|dye| 255 - dye.luma());
for dye in dyes {
print!("{} ", dye.ansi_color_name(&bundle));
}
println!();
```
"#)]
#![cfg_attr(not(feature = "fluent"), doc = r#"
```no_run
use chocodye::Dye;
let mut dyes = Dye::VALUES;
dyes.sort_unstable_by_key(|dye| 255 - dye.luma());
println!("{:#?}", dyes);
```
"#)]
#![cfg_attr(feature = "fluent", doc = r#"
```no_run
use chocodye::{Category, Lang};
let bundle = Lang::English.into_bundle();
for category in Category::VALUES {
print!("{} -- ", category.ansi_full_name(&bundle));
for dye in category.dyes() {
print!("{} ", dye.ansi_color_name(&bundle));
}
println!();
}
```
"#)]
#![cfg_attr(not(feature = "fluent"), doc = r#"
```no_run
use chocodye::Category;
for category in Category::VALUES {
println!("{:?} {:#?}", category, category.dyes());
}
```
"#)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use dye::{Category, Dye};
pub use rgb::{ParseHexError, Rgb};
pub use snack::{Snack, SnackList};
#[cfg(feature = "fluent")]
pub use crate::fluent::{FluentBundle, Lang, ParseLangError};
#[cfg(feature = "fluent")]
#[doc(hidden)]
pub use crate::fluent::{__format_message, __FluentArgs};
#[cfg(feature = "truecolor")]
pub use crate::truecolor::ansi_text;
mod dye;
mod fluent;
mod rgb;
mod snack;
mod truecolor;
#[must_use]
pub fn make_meal(starting_dye: Dye, final_dye: Dye) -> Vec<Snack> {
let mut meal = Vec::new();
let final_color = final_dye.color();
let mut current_color = starting_dye.color();
let mut current_distance = current_color.distance(final_color);
loop {
struct Possibility<const N: usize> {
snacks: [Snack; N],
next_color: Rgb,
next_distance: u32
}
impl<const N: usize> Possibility<N> {
fn from(snacks: [Snack; N], current_color: Rgb, final_color: Rgb) -> Option<Possibility<N>> {
snacks.iter().copied().try_fold(current_color, |current_color, snack| snack.alter(current_color)).map(|next_color| Possibility { snacks, next_color, next_distance: next_color.distance(final_color) })
}
}
impl Possibility<1> {
fn iter(current_color: Rgb, final_color: Rgb) -> impl Iterator<Item = Possibility<1>> {
Snack::VALUES.into_iter().filter_map(move |s| Self::from([s], current_color, final_color))
}
fn get(current_color: Rgb, final_color: Rgb) -> Possibility<1> {
Self::iter(current_color, final_color).min_by_key(|p| p.next_distance).unwrap()
}
}
impl Possibility<2> {
fn iter(current_color: Rgb, final_color: Rgb) -> impl Iterator<Item = Possibility<2>> {
use Snack::*;
const _PAIRS: [(Snack, Snack); Snack::VALUES.len() * (Snack::VALUES.len() - 2)] = [
(Apple, Pear), (Apple, Berries), (Apple, Fruit), (Apple, Pineapple),
(Pear, Apple), (Pear, Berries), (Pear, Plum), (Pear, Pineapple),
(Berries, Apple), (Berries, Pear), (Berries, Plum), (Berries, Fruit),
(Plum, Pear), (Plum, Berries), (Plum, Fruit), (Plum, Pineapple),
(Fruit, Apple), (Fruit, Berries), (Fruit, Plum), (Fruit, Pineapple),
(Pineapple, Apple), (Pineapple, Pear), (Pineapple, Plum), (Pineapple, Fruit)
];
const USED_PAIRS: [(Snack, Snack); 5] = [
(Apple, Pear), (Apple, Berries), (Pear, Berries), (Plum, Pineapple), (Fruit, Pineapple)
];
USED_PAIRS.into_iter().filter_map(move |st| Self::from(st.into(), current_color, final_color))
}
fn get(current_color: Rgb, final_color: Rgb) -> Possibility<2> {
Self::iter(current_color, final_color).min_by_key(|p| p.next_distance).unwrap()
}
}
macro_rules! try_possibilities {
($N:literal, $($M:literal),*) => { #[allow(clippy::redundant_else)] {
let best_choice = Possibility::<$N>::get(current_color, final_color);
if current_distance < best_choice.next_distance {
let current_dye = Dye::try_from(current_color).unwrap_or_else(|d| d);
if current_dye == final_dye {
break;
}
else {
try_possibilities! { $($M),* }
}
}
else {
meal.extend(best_choice.snacks);
current_color = best_choice.next_color;
current_distance = best_choice.next_distance;
}
}};
($N:literal) => {{ try_possibilities! { $N, } }};
() => {{ unreachable!("Possibility<3>") }};
}
try_possibilities! { 1, 2 }
}
meal
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn make_menu(starting_dye: Dye, snacks: SnackList) -> Vec<(Snack, u8)> {
#[inline]
const fn max(c: u8, d: i8) -> u8 {
if d > 0 {
(u8::MAX - c) / (d as u8)
}
else {
(-(c as i16) / (d as i16)) as u8
}
}
#[allow(non_snake_case)]
fn Q(c: Rgb, s: Snack) -> u8 {
let qr = max(c.r, s.effect().0);
let qg = max(c.g, s.effect().1);
let qb = max(c.b, s.effect().2);
qr.min(qg).min(qb)
}
fn backtrack(remaining: SnackList, current_color: Rgb, current_menu: Vec<(Snack, u8)>) -> Vec<(Snack, u8)> {
let mut menu = Vec::new();
for (snack, count) in remaining {
let n = Ord::min(Q(current_color, snack), count);
if n == 0 {
continue;
}
let mut bt_remaning = remaining;
bt_remaning.set(snack, count - n);
let bt_color = Rgb {
r: ((current_color.r as i16) + (n as i16) * (snack.effect().0 as i16)) as u8,
g: ((current_color.g as i16) + (n as i16) * (snack.effect().1 as i16)) as u8,
b: ((current_color.b as i16) + (n as i16) * (snack.effect().2 as i16)) as u8
};
let mut bt_menu = Vec::with_capacity(current_menu.len() + 1);
bt_menu.extend_from_slice(¤t_menu);
bt_menu.push((snack, n));
let bt_result = backtrack(bt_remaning, bt_color, bt_menu);
if bt_result.len() < menu.len() || menu.is_empty() {
menu = bt_result;
}
}
if !menu.is_empty() {
menu
}
else {
debug_assert!(remaining.is_empty(), "remaining {remaining:?} not empty at {current_color:?}");
current_menu
}
}
backtrack(snacks, starting_dye.color(), Vec::new())
}
#[cfg(test)]
mod lib {
mod test {
use std::convert::identity;
use super::super::*;
#[test]
#[cfg_attr(miri, ignore)]
fn all_is_ok() {
for src in Dye::VALUES {
for dst in Dye::VALUES {
let meal = make_meal(src, dst);
let snacks = SnackList::from(meal.as_slice());
let mut rgb = src.color();
for snack in meal {
rgb = snack.alter(rgb).unwrap();
}
let dye = Dye::try_from(rgb).unwrap_or_else(identity);
assert_eq!(dye, dst, "make_meal({src:?}, {dst:?}) returned {dye:?} (d = {})", dye.distance(dst));
let menu = make_menu(src, snacks);
let mut rgb = src.color();
for (snack, count) in menu.clone() {
for i in 0..count {
rgb = match snack.alter(rgb) {
Some(rgb) => rgb,
None => panic!("integer overflow on ({:?} {:?}).alter({:?}) (i = {}/{})", snack, snack.effect(), rgb, i, count - 1)
}
}
}
let dye = Dye::try_from(rgb).unwrap_or_else(identity);
assert_eq!(dye, dst, "make_menu({src:?}, {dst:?}) returned {dye:?} (d = {}, sl = {snacks:#?}, menu = {menu:#?})", dye.distance(dst));
}
}
}
}
}