use serde::{Deserialize, Serialize};
use crate::render::charset::Charset;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AaStrength {
#[default]
Off,
Subtle,
Strong,
}
impl AaStrength {
pub fn cycle(self) -> Self {
match self {
AaStrength::Off => AaStrength::Subtle,
AaStrength::Subtle => AaStrength::Strong,
AaStrength::Strong => AaStrength::Off,
}
}
pub fn as_label(self) -> &'static str {
match self {
AaStrength::Off => "Off",
AaStrength::Subtle => "Subtle",
AaStrength::Strong => "Strong",
}
}
pub fn as_cli(self) -> &'static str {
match self {
AaStrength::Off => "off",
AaStrength::Subtle => "subtle",
AaStrength::Strong => "strong",
}
}
pub fn parse_cli(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"off" => Some(AaStrength::Off),
"subtle" => Some(AaStrength::Subtle),
"strong" => Some(AaStrength::Strong),
_ => None,
}
}
}
impl std::str::FromStr for AaStrength {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_cli(s).ok_or_else(|| {
format!("invalid color-aa mode: {s}. Valid options: off, subtle, strong")
})
}
}
pub fn charset_aa_eligible(charset: &Charset) -> bool {
matches!(
charset,
Charset::Braille | Charset::Quadrant | Charset::HalfBlock | Charset::Ascii
)
}
pub fn blur_field(src: &[f32], width: usize, height: usize, strength: AaStrength) -> Vec<f32> {
if strength == AaStrength::Off || width == 0 || height == 0 {
return src.to_vec();
}
debug_assert_eq!(
src.len(),
width * height,
"blur_field: src must be width*height"
);
let mut out = vec![0.0_f32; src.len()];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
let center = src[idx];
let mut neighbor_sum = 0.0_f32;
let mut neighbor_count = 0u32;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = x as i32 + dx;
let ny = y as i32 + dy;
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
let nidx = ny as usize * width + nx as usize;
neighbor_sum += src[nidx];
neighbor_count += 1;
}
}
}
out[idx] = match strength {
AaStrength::Strong => {
(center + neighbor_sum) / (neighbor_count as f32 + 1.0)
}
AaStrength::Subtle => {
let neighbor_avg = if neighbor_count > 0 {
neighbor_sum / neighbor_count as f32
} else {
center
};
0.5 * center + 0.5 * neighbor_avg
}
AaStrength::Off => center,
};
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::charset::Charset;
#[test]
fn off_returns_clone() {
let src = vec![0.1, 0.9, 0.4, 0.6];
let out = blur_field(&src, 2, 2, AaStrength::Off);
assert_eq!(out, src);
}
#[test]
fn uniform_field_is_unchanged() {
let src = vec![0.5_f32; 9];
let strong = blur_field(&src, 3, 3, AaStrength::Strong);
for v in strong {
assert!((v - 0.5).abs() < 1e-6);
}
}
#[test]
fn strong_spreads_a_center_spike() {
let mut src = vec![0.0_f32; 9];
src[4] = 1.0;
let out = blur_field(&src, 3, 3, AaStrength::Strong);
assert!((out[4] - (1.0 / 9.0)).abs() < 1e-6);
assert!((out[0] - 0.25).abs() < 1e-6);
}
#[test]
fn subtle_preserves_center_more_than_strong() {
let mut src = vec![0.0_f32; 9];
src[4] = 1.0;
let subtle = blur_field(&src, 3, 3, AaStrength::Subtle);
let strong = blur_field(&src, 3, 3, AaStrength::Strong);
assert!(subtle[4] > strong[4]);
assert!((subtle[4] - 0.5).abs() < 1e-6); }
#[test]
fn cycle_wraps() {
assert_eq!(AaStrength::Off.cycle(), AaStrength::Subtle);
assert_eq!(AaStrength::Subtle.cycle(), AaStrength::Strong);
assert_eq!(AaStrength::Strong.cycle(), AaStrength::Off);
}
#[test]
fn parse_cli_roundtrip() {
for s in [AaStrength::Off, AaStrength::Subtle, AaStrength::Strong] {
assert_eq!(AaStrength::parse_cli(s.as_cli()), Some(s));
}
assert_eq!(AaStrength::parse_cli("STRONG"), Some(AaStrength::Strong));
assert_eq!(AaStrength::parse_cli("nope"), None);
}
#[test]
fn eligibility() {
assert!(charset_aa_eligible(&Charset::Braille));
assert!(charset_aa_eligible(&Charset::Quadrant));
assert!(charset_aa_eligible(&Charset::HalfBlock));
assert!(charset_aa_eligible(&Charset::Ascii));
assert!(!charset_aa_eligible(&Charset::HalfBlockDual));
assert!(!charset_aa_eligible(&Charset::Sculpted));
assert!(!charset_aa_eligible(&Charset::Shade));
}
}