const INF: f32 = 1e20;
pub(crate) fn signed_distances(coverage: &[u8], w: usize, h: usize) -> Vec<f32> {
debug_assert_eq!(coverage.len(), w * h);
let (mut outer, mut inner) = seed_from_alpha(coverage);
edt_2d(&mut outer, w, h);
edt_2d(&mut inner, w, h);
(0..w * h)
.map(|i| inner[i].sqrt() - outer[i].sqrt())
.collect()
}
fn seed_from_alpha(coverage: &[u8]) -> (Vec<f32>, Vec<f32>) {
let mut outer = vec![INF; coverage.len()];
let mut inner = vec![0.0f32; coverage.len()];
for (i, &c) in coverage.iter().enumerate() {
match c {
0 => {}
255 => {
outer[i] = 0.0;
inner[i] = INF;
}
_ => {
let d = 0.5 - c as f32 / 255.0;
outer[i] = if d > 0.0 { d * d } else { 0.0 };
inner[i] = if d < 0.0 { d * d } else { 0.0 };
}
}
}
(outer, inner)
}
fn edt_2d(grid: &mut [f32], w: usize, h: usize) {
let mut scratch = Scratch::new(w.max(h));
let mut column = vec![0.0f32; h];
for x in 0..w {
for y in 0..h {
column[y] = grid[y * w + x];
}
scratch.edt_1d(&column, h);
for y in 0..h {
grid[y * w + x] = scratch.d[y];
}
}
let mut row = vec![0.0f32; w];
for y in 0..h {
row.copy_from_slice(&grid[y * w..(y + 1) * w]);
scratch.edt_1d(&row, w);
grid[y * w..(y + 1) * w].copy_from_slice(&scratch.d[..w]);
}
}
struct Scratch {
d: Vec<f32>,
v: Vec<usize>,
z: Vec<f32>,
}
impl Scratch {
fn new(n: usize) -> Self {
Scratch {
d: vec![0.0; n],
v: vec![0; n],
z: vec![0.0; n + 1],
}
}
fn edt_1d(&mut self, f: &[f32], n: usize) {
let (v, z, d) = (&mut self.v, &mut self.z, &mut self.d);
let mut k = 0;
v[0] = 0;
z[0] = -INF;
z[1] = INF;
for q in 1..n {
let mut s = intersect(f, q, v[k]);
while s <= z[k] {
k -= 1;
s = intersect(f, q, v[k]);
}
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = INF;
}
k = 0;
for (q, out) in d.iter_mut().enumerate().take(n) {
while z[k + 1] < q as f32 {
k += 1;
}
let dq = q as f32 - v[k] as f32;
*out = dq * dq + f[v[k]];
}
}
}
fn intersect(f: &[f32], q: usize, p: usize) -> f32 {
let (fq, fp) = (f[q] + (q * q) as f32, f[p] + (p * p) as f32);
(fq - fp) / (2 * q - 2 * p) as f32
}
pub(crate) fn encode(field: &[f32], spread_px: f32) -> Vec<u8> {
field
.iter()
.map(|d| ((0.5 + d / (2.0 * spread_px)).clamp(0.0, 1.0) * 255.0).round() as u8)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn circle_distances_match_geometry() {
let (w, h, r) = (64usize, 64usize, 20.0f32);
let cov: Vec<u8> = (0..w * h)
.map(|i| {
let (x, y) = ((i % w) as f32 - 32.0, (i / w) as f32 - 32.0);
if (x * x + y * y).sqrt() <= r {
255
} else {
0
}
})
.collect();
let field = signed_distances(&cov, w, h);
for (i, d) in field.iter().enumerate() {
let (x, y) = ((i % w) as f32 - 32.0, (i / w) as f32 - 32.0);
let expected = r - (x * x + y * y).sqrt();
assert!(
(d - expected).abs() <= 1.0,
"at ({x},{y}): sdf {d} vs geometric {expected}"
);
}
}
#[test]
fn partial_coverage_moves_the_edge_subpixel() {
let (w, h) = (16usize, 8usize);
let strip = |edge_alpha: u8| -> Vec<f32> {
let cov: Vec<u8> = (0..w * h)
.map(|i| match (i % w).cmp(&8) {
std::cmp::Ordering::Less => 255,
std::cmp::Ordering::Equal => edge_alpha,
std::cmp::Ordering::Greater => 0,
})
.collect();
signed_distances(&cov, w, h)
};
let quarter = strip(64);
let three_quarters = strip(191);
let at = |f: &[f32], x: usize| f[4 * w + x];
assert!(at(&three_quarters, 8) > at(&quarter, 8));
assert!(at(&three_quarters, 7) >= at(&quarter, 7));
}
}