use bitvec::vec::BitVec;
use image::{DynamicImage, ImageBuffer, Luma};
use spiral::ManhattanIterator;
pub struct Options {
pub size: (usize, usize),
pub max_distance: usize,
pub image_treshold: u8,
}
impl Default for Options {
fn default() -> Self {
Options {
size: (64, 64),
max_distance: 512,
image_treshold: 127,
}
}
}
pub trait DistanceFieldExt {
fn distance_field(&self, options: Options) -> ImageBuffer<Luma<u8>, Vec<u8>>;
}
impl DistanceFieldExt for DynamicImage {
fn distance_field(&self, options: Options) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let treshold = TresholdImage::from_dynamic_image(self, options.image_treshold);
ImageBuffer::from_fn(options.size.0 as u32, options.size.1 as u32, |x, y| {
Luma([get_nearest_pixel_distance(
&treshold, x as usize, y as usize, &options,
)])
})
}
}
struct TresholdImage {
bits: BitVec,
width: usize,
height: usize,
}
impl TresholdImage {
pub fn from_dynamic_image(image: &DynamicImage, treshold: u8) -> Self {
let luma_img = image.to_luma8();
let bits = luma_img
.pixels()
.map(|pixel| pixel.0[0] > treshold)
.collect::<BitVec>();
Self {
bits,
width: luma_img.width() as usize,
height: luma_img.height() as usize,
}
}
pub fn get_pixel(&self, x: usize, y: usize) -> bool {
let index = x + y * self.width;
self.bits[index]
}
}
fn get_nearest_pixel_distance(
input: &TresholdImage,
out_x: usize,
out_y: usize,
options: &Options,
) -> u8 {
let center = (
((out_x * input.width) / options.size.0) as isize,
((out_y * input.height) / options.size.1) as isize,
);
let is_inside = input.get_pixel(center.0 as usize, center.1 as usize);
let closest_distance =
ManhattanIterator::new(center.0, center.1, options.max_distance as isize)
.filter(|(x, y)| {
*x >= 0 && *x < input.width as isize && *y >= 0 && *y < input.height as isize
})
.find(|(x, y)| input.get_pixel(*x as usize, *y as usize) != is_inside)
.map(|(x, y)| {
let dx = center.0.abs_diff(x);
let dy = center.1.abs_diff(y);
((dx * dx + dy * dy) as f32).sqrt()
})
.unwrap_or(options.max_distance as f32);
let distance_fraction = if is_inside {
0.5 + (closest_distance / 2.0) / options.max_distance as f32
} else {
0.5 - (closest_distance / 2.0) / options.max_distance as f32
};
(distance_fraction * u8::max_value() as f32) as u8
}