1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
//! Generate distance field bitmaps for rendering as pseudo-vector images in shaders.
//!
//! An example usecase for the library would be to automatically convert asset images. You can achieve this by having a `build.rs` similar to this:
//!
//! ```rust
//! use std::fs::File;
//! use distance_field::DistanceFieldExt;
//!
//! fn convert_image_to_dfield(input: &str, output: &str) {
//! // Load the 'input' image
//! let img = image::open(input).unwrap();
//!
//! // Generate a distance field from the image
//! let outbuf = img.grayscale().distance_field(distance_field::Options {
//! size: (128, 128),
//! max_distance: 256,
//! ..Default::default()
//! });
//!
//! // Save it to 'output' as a PNG
//! image::DynamicImage::ImageLuma8(outbuf).save(output).unwrap();
//! }
//!
//! fn main() {
//! convert_image_to_dfield("img/input.png", "output.png");
//! }
//! ```
use bitvec::vec::BitVec;
use image::{DynamicImage, ImageBuffer, Luma};
use spiral::ManhattanIterator;
/// The options passed as the argument to the `distance_field` method.
pub struct Options {
/// The dimensions of the output image (width, height). The default value is: (64,64).
pub size: (usize, usize),
/// The maximum distance for the projected point of the output image on the input image to
/// search for the nearest point. The defaul value is: 512.
pub max_distance: usize,
/// The image value at which to apply the treshold. In a black-white vector image 127 is
/// probably the best value. The default value is: 127.
pub image_treshold: u8,
}
/// Returns:
/// ```Options {
/// size: (64, 64),
/// max_distance: 512,
/// image_treshold: 127
/// }```
impl Default for Options {
fn default() -> Self {
Options {
size: (64, 64),
max_distance: 512,
image_treshold: 127,
}
}
}
/// A trait adding the `distance_field` function to image types.
pub trait DistanceFieldExt {
/// Generates a grayscale output image with the dimensions as specified in the `Options`
/// struct.
fn distance_field(&self, options: Options) -> ImageBuffer<Luma<u8>, Vec<u8>>;
}
/// A implementation of the `distance_field` function for the `DynamicImage` type. To call this
/// from a normal RGB image use `image.grayscale().distance_field(options)`.
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,
)])
})
}
}
/// Treshold image as a boolean vector.
struct TresholdImage {
bits: BitVec,
width: usize,
height: usize,
}
impl TresholdImage {
/// Convert a dynamic image to this type of image by applying a treshold.
pub fn from_dynamic_image(image: &DynamicImage, treshold: u8) -> Self {
// Get the image as grayscale
let luma_img = image.to_luma8();
// Convert the image to a boolean buffer, applying a treshold
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,
}
}
/// Get a pixel as a boolean.
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 {
// Calcule the projected center of the output pixel on the source image
let center = (
((out_x * input.width) / options.size.0) as isize,
((out_y * input.height) / options.size.1) as isize,
);
// Check if we are inside a filled area so we can get the 127-255 range
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)
// Ignore the boundary conditions
.filter(|(x, y)| {
*x >= 0 && *x < input.width as isize && *y >= 0 && *y < input.height as isize
})
// Continue if the center and this pixel are inside the filled area or
// the pixels are both outside the filled area
.find(|(x, y)| input.get_pixel(*x as usize, *y as usize) != is_inside)
// We found the nearest pixel, calculate the distance
.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);
// Convert the outside to a 0.0-0.5 and the inside to a 0.5-1.0 range
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
};
// Convert the 0.0-1.0 range to a u8
(distance_fraction * u8::max_value() as f32) as u8
}