use crate::base::Rgba;
use crate::gfx::mosaic::MosaicCell;
use crate::canvas::braille_bit;
pub(crate) use crate::canvas::QUADRANT_CHARS;
pub(crate) const SEXTANT_CHARS: [char; 64] = [
' ', '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎', '🬏', '🬐', '🬑',
'🬒', '🬓', '▌', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝', '🬞', '🬟', '🬠', '🬡', '🬢', '🬣',
'🬤', '🬥', '🬦', '🬧', '▐', '🬨', '🬩', '🬪', '🬫', '🬬', '🬭', '🬮', '🬯', '🬰', '🬱', '🬲', '🬳', '🬴', '🬵',
'🬶', '🬷', '🬸', '🬹', '🬺', '🬻', '█',
];
pub(crate) fn fit_half_block(top: Rgba, bottom: Rgba) -> MosaicCell {
if top == bottom {
MosaicCell {
ch: ' ',
fg: bottom,
bg: bottom,
}
} else {
MosaicCell {
ch: '\u{2580}',
fg: top,
bg: bottom,
}
}
}
pub(crate) fn fit_two_color(sub: &[Rgba], chars: &[char]) -> MosaicCell {
let n = sub.len();
debug_assert!(n <= 8 && chars.len() == 1 << n);
let mut wc = [[0u32; 3]; 8];
let mut w = [0u32; 8];
let mut tot_w = 0u64;
let mut tot_c = [0u64; 3];
let mut tot_a = 0u32;
for i in 0..n {
let p = sub[i];
let wi = p.a as u32;
w[i] = wi;
wc[i] = [wi * p.r as u32, wi * p.g as u32, wi * p.b as u32];
tot_w += wi as u64;
for ch in 0..3 {
tot_c[ch] += wc[i][ch] as u64;
}
tot_a += p.a as u32;
}
if tot_w == 0 {
return MosaicCell::EMPTY;
}
let mut best_pattern = 0usize;
let mut best_score = f64::NEG_INFINITY;
let mut pattern = 0usize;
while pattern < (1 << n) {
let mut fw = 0u64;
let mut fc = [0u64; 3];
let mut bits = pattern;
while bits != 0 {
let i = bits.trailing_zeros() as usize;
bits &= bits - 1;
fw += w[i] as u64;
for ch in 0..3 {
fc[ch] += wc[i][ch] as u64;
}
}
let bw = tot_w - fw;
let bc = [tot_c[0] - fc[0], tot_c[1] - fc[1], tot_c[2] - fc[2]];
let side = |c: &[u64; 3], sw: u64| -> f64 {
if sw == 0 {
0.0
} else {
let s = (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]) as f64;
s / sw as f64
}
};
let score = side(&fc, fw) + side(&bc, bw);
if score > best_score {
best_score = score;
best_pattern = pattern;
}
pattern += 2;
}
let mut fw = 0u64;
let mut fc = [0u64; 3];
let mut fa = 0u32;
let mut fcount = 0u32;
let mut bits = best_pattern;
while bits != 0 {
let i = bits.trailing_zeros() as usize;
bits &= bits - 1;
fw += w[i] as u64;
for ch in 0..3 {
fc[ch] += wc[i][ch] as u64;
}
fa += sub[i].a as u32;
fcount += 1;
}
let bw = tot_w - fw;
let bc = [tot_c[0] - fc[0], tot_c[1] - fc[1], tot_c[2] - fc[2]];
let ba = tot_a - fa;
let bcount = n as u32 - fcount;
#[allow(clippy::manual_checked_ops)]
let mean = |s: &[u64; 3], sw: u64| -> [u8; 3] {
if sw == 0 {
[0, 0, 0]
} else {
[
((s[0] + sw / 2) / sw) as u8,
((s[1] + sw / 2) / sw) as u8,
((s[2] + sw / 2) / sw) as u8,
]
}
};
let fm = mean(&fc, fw);
let bm = mean(&bc, bw);
#[allow(clippy::manual_checked_ops)]
let alpha = |sum_a: u32, count: u32| -> u8 {
if count == 0 {
0
} else {
((sum_a + count / 2) / count) as u8
}
};
let best_fg = if fw == 0 {
Rgba::TRANSPARENT
} else {
Rgba::new(fm[0], fm[1], fm[2], alpha(fa, fcount))
};
let best_bg = if bw == 0 {
Rgba::TRANSPARENT
} else {
Rgba::new(bm[0], bm[1], bm[2], alpha(ba, bcount))
};
MosaicCell {
ch: chars[best_pattern],
fg: best_fg,
bg: best_bg,
}
}
pub(crate) fn fit_braille(sub: &[Rgba]) -> MosaicCell {
debug_assert_eq!(sub.len(), 8);
let luma = |p: Rgba| -> u64 {
(2126 * p.r as u64 + 7152 * p.g as u64 + 722 * p.b as u64) * p.a as u64
};
let mut lum = [0u64; 8];
let mut total = 0u64;
let mut any_visible = false;
for (i, &p) in sub.iter().enumerate() {
lum[i] = luma(p);
total += lum[i];
any_visible |= p.a != 0;
}
if !any_visible {
return MosaicCell::EMPTY;
}
let mean = total / 8;
let mut bits = 0u32;
let mut lit_c = [0u64; 3];
let mut lit_w = 0u64;
let mut lit_a = 0u32;
let mut lit_n = 0u32;
let mut unlit_c = [0u64; 3];
let mut unlit_w = 0u64;
let mut unlit_a = 0u32;
let mut unlit_n = 0u32;
for (i, &p) in sub.iter().enumerate() {
let w = p.a as u64;
if lum[i] > mean {
bits |= u32::from(braille_bit((i % 2) as i32, (i / 2) as i32));
lit_w += w;
lit_a += p.a as u32;
lit_n += 1;
lit_c[0] += w * p.r as u64;
lit_c[1] += w * p.g as u64;
lit_c[2] += w * p.b as u64;
} else {
unlit_w += w;
unlit_a += p.a as u32;
unlit_n += 1;
unlit_c[0] += w * p.r as u64;
unlit_c[1] += w * p.g as u64;
unlit_c[2] += w * p.b as u64;
}
}
let side = |c: &[u64; 3], sw: u64, sa: u32, n: u32| -> Rgba {
if sw == 0 || n == 0 {
Rgba::TRANSPARENT
} else {
Rgba::new(
((c[0] + sw / 2) / sw) as u8,
((c[1] + sw / 2) / sw) as u8,
((c[2] + sw / 2) / sw) as u8,
((sa + n / 2) / n) as u8,
)
}
};
MosaicCell {
ch: char::from_u32(0x2800 + bits).expect("braille block is contiguous"),
fg: side(&lit_c, lit_w, lit_a, lit_n),
bg: side(&unlit_c, unlit_w, unlit_a, unlit_n),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sextant_table_is_consistent() {
assert_eq!(SEXTANT_CHARS[0], ' ');
assert_eq!(SEXTANT_CHARS[21], '\u{258C}');
assert_eq!(SEXTANT_CHARS[42], '\u{2590}');
assert_eq!(SEXTANT_CHARS[63], '\u{2588}');
for bits in 1u32..63 {
if bits == 21 || bits == 42 {
continue;
}
let skipped = u32::from(bits > 21) + u32::from(bits > 42);
let expected = char::from_u32(0x1FB00 + bits - 1 - skipped).unwrap();
assert_eq!(
SEXTANT_CHARS[bits as usize],
expected,
"sextant bits {bits:06b} must map to U+{:04X}",
0x1FB00 + bits - 1 - skipped
);
}
let uniq: std::collections::HashSet<char> = SEXTANT_CHARS.iter().copied().collect();
assert_eq!(uniq.len(), 64);
}
#[test]
fn quadrant_table_is_consistent() {
assert_eq!(QUADRANT_CHARS[0], ' ');
assert_eq!(QUADRANT_CHARS[0b0011], '\u{2580}'); assert_eq!(QUADRANT_CHARS[0b0101], '\u{258C}'); assert_eq!(QUADRANT_CHARS[0b1111], '\u{2588}');
let uniq: std::collections::HashSet<char> = QUADRANT_CHARS.iter().copied().collect();
assert_eq!(uniq.len(), 16);
}
#[test]
fn braille_bit_table_covers_all_dots() {
let mut all = 0u32;
for i in 0..8usize {
let b = u32::from(braille_bit((i % 2) as i32, (i / 2) as i32));
assert_eq!(all & b, 0, "duplicate bit");
all |= b;
}
assert_eq!(all, 0xFF);
}
#[test]
fn two_color_fit_moment_headroom() {
let sub = [Rgba::WHITE; 6];
let cell = fit_two_color(&sub, &SEXTANT_CHARS);
assert_eq!(cell.ch, ' ', "uniform white = space + bg");
assert_eq!((cell.bg.r, cell.bg.g, cell.bg.b), (255, 255, 255));
}
}