pub const SIZE: u32 = 32;
const RED: [u8; 3] = [0xE5, 0x39, 0x35];
const WHITE: [u8; 3] = [0xFF, 0xFF, 0xFF];
fn glyph(c: char) -> Option<[u8; 5]> {
Some(match c {
'0' => [0b111, 0b101, 0b101, 0b101, 0b111],
'1' => [0b010, 0b110, 0b010, 0b010, 0b111],
'2' => [0b111, 0b001, 0b111, 0b100, 0b111],
'3' => [0b111, 0b001, 0b111, 0b001, 0b111],
'4' => [0b101, 0b101, 0b111, 0b001, 0b001],
'5' => [0b111, 0b100, 0b111, 0b001, 0b111],
'6' => [0b111, 0b100, 0b111, 0b101, 0b111],
'7' => [0b111, 0b001, 0b010, 0b010, 0b010],
'8' => [0b111, 0b101, 0b111, 0b101, 0b111],
'9' => [0b111, 0b101, 0b111, 0b001, 0b111],
'+' => [0b000, 0b010, 0b111, 0b010, 0b000],
_ => return None,
})
}
pub fn badge_text(count: u64) -> String {
if count > 99 {
"99+".into()
} else {
count.to_string()
}
}
pub fn render(count: u64) -> Vec<u8> {
let text = badge_text(count);
let mut rgba = vec![0u8; (SIZE * SIZE * 4) as usize];
let center = SIZE as f32 / 2.0;
let radius = center - 0.5;
for y in 0..SIZE {
for x in 0..SIZE {
let (dx, dy) = (x as f32 + 0.5 - center, y as f32 + 0.5 - center);
let coverage = (radius - (dx * dx + dy * dy).sqrt() + 0.5).clamp(0.0, 1.0);
if coverage > 0.0 {
put(&mut rgba, x, y, RED, (coverage * 255.0).round() as u8);
}
}
}
let chars: Vec<[u8; 5]> = text.chars().filter_map(glyph).collect();
let scale: u32 = match chars.len() {
1 => 4,
2 => 3,
_ => 2,
};
let n = chars.len() as u32;
let width = n * 3 * scale + n.saturating_sub(1) * scale;
let height = 5 * scale;
let (x0, y0) = ((SIZE - width) / 2, (SIZE - height) / 2);
for (i, rows) in chars.iter().enumerate() {
let gx = x0 + i as u32 * 4 * scale;
for (row, bits) in rows.iter().enumerate() {
for col in 0..3u32 {
if bits & (0b100 >> col) == 0 {
continue;
}
for sy in 0..scale {
for sx in 0..scale {
put(&mut rgba, gx + col * scale + sx, y0 + row as u32 * scale + sy, WHITE, 255);
}
}
}
}
}
rgba
}
fn put(rgba: &mut [u8], x: u32, y: u32, color: [u8; 3], alpha: u8) {
let i = ((y * SIZE + x) * 4) as usize;
rgba[i..i + 3].copy_from_slice(&color);
rgba[i + 3] = alpha;
}
#[cfg(test)]
mod tests {
use super::*;
fn pixel(rgba: &[u8], x: u32, y: u32) -> [u8; 4] {
let i = ((y * SIZE + x) * 4) as usize;
[rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]]
}
fn white_pixels(rgba: &[u8]) -> usize {
rgba.chunks(4).filter(|p| p == &[0xFF, 0xFF, 0xFF, 0xFF]).count()
}
#[test]
fn text_caps_at_99() {
assert_eq!(badge_text(1), "1");
assert_eq!(badge_text(99), "99");
assert_eq!(badge_text(100), "99+");
assert_eq!(badge_text(12345), "99+");
}
#[test]
fn renders_red_dot_with_white_digits() {
let one = render(1);
assert_eq!(one.len(), (SIZE * SIZE * 4) as usize);
assert_eq!(pixel(&one, 0, 0)[3], 0);
assert_eq!(pixel(&one, SIZE - 1, SIZE - 1)[3], 0);
assert_eq!(pixel(&one, 2, SIZE / 2), [0xE5, 0x39, 0x35, 0xFF]);
assert_eq!(pixel(&one, SIZE / 2, SIZE / 2), [0xFF, 0xFF, 0xFF, 0xFF]);
assert!(white_pixels(&one) > 0);
assert_ne!(render(5), render(6));
assert_eq!(render(100), render(1000));
let many = render(100);
let columns: Vec<u32> = (0..SIZE).filter(|x| (0..SIZE).any(|y| pixel(&many, *x, y) == [0xFF, 0xFF, 0xFF, 0xFF])).collect();
assert_eq!(columns.first(), Some(&5));
assert_eq!(columns.last(), Some(&26));
}
}