我正在寻找四种可区分的颜色,有些互补/视觉上有吸引力,并且没有任何共同点。我的意思如下:
例如,如果我们将这些颜色视为唯一标识符,那么我们应该能够以有趣的方式将这些颜色混合在无符号 32 位整数中。
COLOR_1
代表与 COLOR_1 | COLOR_2
不同的颜色,依此类推。四种颜色中的每一种都是独特的,每种可能的组合都是独特的。
但是,我有一个额外的限制,即这四种颜色中的任何两位都不应重叠。这意味着如果我们有以下 u32
let mix: u32 = COLOR_1 | COLOR_2 | COLOR_3;
,那么以下操作应该为真:(mix & !COLOR_1) == COLOR_2 | COLOR_3
。这相当于从整数中删除 COLOR_1
,而不影响属于 COLOR_2
和 COLOR_3
的任何位。
我认为探索所有可能的四分体将是一个好的开始,所以我编写了这个程序(这是四分体的图片以及由该圆上的四个点表示的图片)。
但是,似乎没有具有我正在寻找的属性的四分体,因为该程序没有返回任何积极的结果。它以任意四分体开始,并简单地递增四种颜色中的每一种,检查四种颜色的组合,没有重叠位。我相信这保持了四种颜色形成四分色的特性。此过程将一直持续到检查完整个 RGB 色彩空间为止。有没有办法获得具有我正在寻找的特性的接近四分体的东西?或者也许还有另一种色彩理论方法?目标是 4 种易于区分的颜色,无论是单独使用还是通过上述按位或运算混合时都可以很好地互补。
use crossterm::{
queue,
style::{Color, Print, ResetColor, SetForegroundColor},
};
use std::io::{self};
macro_rules! rgb {
($r:expr, $g:expr, $b:expr) => {
((($r as u32) & 0xFF) << 16) | ((($g as u32) & 0xFF) << 8) | (($b as u32) & 0xFF)
};
}
const U24_MAX: u32 = 0xFFFFFF;
fn all_unique_bits(tetradic: &[u32]) -> bool {
for (i, t) in tetradic.iter().enumerate() {
for (i2, t2) in tetradic.iter().enumerate() {
if i == i2 {
continue;
}
if (t & t2) != 0 {
return false;
}
}
}
true
}
fn main() {
// Start with a known tetrad.
let mut tetradic: [u32; 4] = [
rgb!(155, 237, 0),
rgb!(18, 62, 171),
rgb!(255, 171, 0),
rgb!(206, 0, 113),
];
let start_val = tetradic[0];
let mut iteration = start_val;
'searching_for_tetrads: loop {
// Increment all values by one so 90 degree relationship remains. Is this true?
for t in &mut tetradic {
*t = (*t + 1) % U24_MAX;
}
if all_unique_bits(tetradic.as_slice()) {
for t in tetradic {
queue!(
io::stdout(),
SetForegroundColor(Color::Rgb {
r: ((t & 0xFF0000) >> 16) as u8,
g: ((t & 0xFF00) >> 8) as u8,
b: (t & 0xFF) as u8,
}),
Print('█'),
ResetColor,
Print(format!(
"({:3}, {:3}, {:3}) ",
(t & 0xFF0000) >> 16,
(t & 0xFF00) >> 8,
(t & 0xFF)
)),
)
.expect("could not print.");
}
println!();
}
iteration = (iteration + 1) % U24_MAX;
if iteration == start_val {
break 'searching_for_tetrads;
}
}
}
从数学角度来看,这似乎非常棘手。我对你的理解是否正确,你正在寻找 4 种颜色:
第一个是硬性要求,第二个和第三个可能没那么多。
我不知道如何想出一组这样的颜色。你可以用暴力破解,因为“只有”~10^⁶·⁸的可能性,但让我们尝试一些更有趣的东西:
只需取 24 个 1 位并将它们随机分配给 4 种颜色之一,即可随机生成一组 4 种颜色:
fn generate_candidate(&mut self) -> [u32; 4] {
let mut rng = thread_rng();
let mut colorbits = [0u32; 4];
for i in 0..24 {
*colorbits.choose_mut(&mut rng).unwrap() |= 1 << i;
}
colorbits
}
// Yeah, this can't generate all possible combinations, it'll never have less than 24 1-bits. I don't think having less is useful.
您还可以通过取一位并将其移动到不同的颜色来从现有的候选项中生成新的相似候选项
fn tweak_candidate(&mut self, candidate: &[u32; 4]) -> [u32; 4] {
let mut rng = thread_rng();
let mut new = self.clone_candidate(candidate);
let bit = rng.gen_range(0..24);
// unset bit everywhere
for c in &mut new {
*c = *c & !(1 << bit);
}
// set bit in randomly chosen color
*new.choose_mut(&mut rng).unwrap() |= 1 << bit;
new
}
这足以允许走过可能颜色四倍的空间。要在那次步行中找到好的四元组,您需要定义什么是好的四元组,但是对于暴力破解,您也必须执行相同的操作:
fn rank_candidate(&mut self, candidate: &[u32; 4]) -> f64 {
let rgbs =
candidate.map(|bits| Rgb::from(<[u8; 3]>::try_from(&bits.to_be_bytes()[1..]).unwrap()));
let hsls = rgbs.clone().map(|rgb| Hsl::from(rgb));
let mut hues = hsls.clone().map(|c| c.hue());
hues.sort_by(f64::total_cmp);
let angles: [f64; 3] = array::from_fn(|i| (hues[i + 1] - hues[i]));
- angles.map(|a| (a - 90.).powf(2.)).into_iter().sum::<f64>()
- hsls
.map(|c| (100. - c.saturation()).powf(2.) + (100. - c.lightness() * 2.).powf(2.))
.iter()
.sum::<f64>() * 0.1;
}
这样,您就可以应用模拟退火等元启发式方法来找到一组不错的颜色。
有趣的是,似乎有关位和角度的条件将光度限制在~0.25。