use anyhow::*;
use image::{buffer::ConvertBuffer, ImageBuffer, Rgb, Rgba};
use std::io::{Cursor, Write};
use base64::Engine;
use tiny_skia::*;
use spade::{DelaunayTriangulation, FloatTriangulation, HasPosition, Point2, Triangulation};
#[derive(Debug, Copy, Clone)]
struct PointWithHeight {
position: Point2<f64>,
height: f64,
}
impl PointWithHeight {
const fn new(x: f64, y: f64, height: f64) -> Self {
Self {
position: Point2::new(x, y),
height,
}
}
}
impl HasPosition for PointWithHeight {
type Scalar = f64;
fn position(&self) -> Point2<Self::Scalar> {
self.position
}
}
type TriangulationType = DelaunayTriangulation<PointWithHeight>;
const VERTICES: &[PointWithHeight] = &[
PointWithHeight::new(20.0, 20.0, 0.9),
PointWithHeight::new(20.0, -20.0, 0.9),
PointWithHeight::new(-20.0, 20.0, 0.9),
PointWithHeight::new(-20.0, -20.0, 0.9),
PointWithHeight::new(20.0, 10.0, 0.0),
PointWithHeight::new(10.0, 10.0, 0.5),
PointWithHeight::new(0.0, 23.0, 0.1),
PointWithHeight::new(0.0, -23.0, 0.1),
PointWithHeight::new(-10.0, 10.0, 0.0),
PointWithHeight::new(-10.0, -10.0, 0.1),
PointWithHeight::new(-15.0, 20.0, 0.5),
PointWithHeight::new(-5.0, 0.0, 0.25),
PointWithHeight::new(5.0, 7.0, 0.75),
PointWithHeight::new(12.0, -10.0, 0.4),
PointWithHeight::new(5.0, 10.0, 0.3),
PointWithHeight::new(5.0, 1.0, 0.2),
];
pub fn main() -> Result<()> {
let mut t = TriangulationType::default();
for vertex in VERTICES {
t.insert(*vertex)?;
}
const OFFSET: f32 = 25.0;
const SCALAR: f32 = 50.0 / 512.0;
fn px_to_coords(x: u32, y: u32) -> Point2<f64> {
Point2::new(
x as f64 * SCALAR as f64 - OFFSET as f64,
y as f64 * SCALAR as f64 - OFFSET as f64,
)
}
let dimensions = 512;
let mut nn_c0_pixmap =
Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?;
let mut nn_c1_pixmap_1 =
Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?;
let mut nn_c1_pixmap_05 =
Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?;
let mut barycentric_pixmap =
Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?;
let mut nearest_neighbor_pixmap =
Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?;
fn set_pixel(pixmap: &mut Pixmap, x: u32, y: u32, value: Option<f64>) {
let background_color = [255, 255, 255];
let [r, g, b] = value.map(float_to_color).unwrap_or(background_color);
let base = (y * pixmap.height() + x) as usize * 4;
pixmap.data_mut()[base] = r;
pixmap.data_mut()[base + 1] = g;
pixmap.data_mut()[base + 2] = b;
pixmap.data_mut()[base + 3] = 255;
}
let nn = t.natural_neighbor();
let barycentric = t.barycentric();
let grads = nn.estimate_gradients(|v| v.data().height);
for y in 0..dimensions {
for x in 0..dimensions {
let coords = px_to_coords(x, y);
let value_nn_c0 = nn.interpolate(|v| v.data().height, coords);
set_pixel(&mut nn_c0_pixmap, x, y, value_nn_c0);
let value_nn_c1_1 = nn.interpolate_gradient(|v| v.data().height, &grads, 1.0, coords);
set_pixel(&mut nn_c1_pixmap_1, x, y, value_nn_c1_1);
let value_nn_c1_05 = nn.interpolate_gradient(|v| v.data().height, &grads, 0.5, coords);
set_pixel(&mut nn_c1_pixmap_05, x, y, value_nn_c1_05);
let value_barycentric = barycentric.interpolate(|v| v.data().height, coords);
set_pixel(&mut barycentric_pixmap, x, y, value_barycentric);
let value_nearest_neighbor = t.nearest_neighbor(coords).map(|v| v.data().height);
set_pixel(&mut nearest_neighbor_pixmap, x, y, value_nearest_neighbor);
}
}
let path = {
let mut pb = PathBuilder::new();
for edge in t.undirected_edges() {
let [from, to] = edge.positions();
pb.move_to(from.x as f32, from.y as f32);
pb.line_to(to.x as f32, to.y as f32);
}
pb.finish().context("Could not build path")?
};
let stroke = Stroke {
width: 0.15,
..Default::default()
};
let mut paint = Paint::default();
paint.set_color_rgba8(0, 0, 0, 70);
paint.anti_alias = true;
for pixmap in [
&mut nn_c0_pixmap,
&mut nn_c1_pixmap_1,
&mut nn_c1_pixmap_05,
&mut barycentric_pixmap,
&mut nearest_neighbor_pixmap,
] {
pixmap.stroke_path(
&path,
&paint,
&stroke,
Transform::from_translate(OFFSET, OFFSET).post_scale(1.0 / SCALAR, 1.0 / SCALAR),
None,
);
}
fn save_pixmap(pixmap: Pixmap, name: &str) -> Result<()> {
let (width, height) = (pixmap.width(), pixmap.height());
let data = pixmap.take();
let buffer_with_alpha = ImageBuffer::<Rgba<u8>, _>::from_vec(width, height, data)
.context("Failed to convert to ImageBuffer")?;
let buffer: ImageBuffer<Rgb<u8>, _> = buffer_with_alpha.convert();
let mut data_jpeg: Cursor<Vec<u8>> = Cursor::new(Vec::new());
buffer.write_to(&mut data_jpeg, image::ImageFormat::Jpeg)?;
std::fs::write(format!("images/{}.jpg", name), data_jpeg.get_ref())?;
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(data_jpeg.get_ref());
let mut file = std::fs::File::create(format!("images/{}.img", name))?;
write!(file, r#"<img src="data:image/jpg;base64,{}" />"#, encoded)?;
Ok(())
}
save_pixmap(nn_c0_pixmap, "interpolation_nn_c0")?;
save_pixmap(nn_c1_pixmap_1, "interpolation_nn_c1_flatness_1")?;
save_pixmap(nn_c1_pixmap_05, "interpolation_nn_c1_flatness_05")?;
save_pixmap(barycentric_pixmap, "interpolation_barycentric")?;
save_pixmap(nearest_neighbor_pixmap, "interpolation_nearest_neighbor")?;
Ok(())
}
fn float_to_color(value: f64) -> [u8; 3] {
let value = value.clamp(0.0, 1.0);
const LIGHTNESS: f64 = 0.45;
const SATURATION: f64 = 0.55;
let c = (1.0 - (2.0 * LIGHTNESS - 1.0).abs()) * SATURATION;
let hue = value * 360.0;
let hs = hue / 60.0;
let x = c * (1.0 - (hs % 2.0 - 1.0).abs());
let m = LIGHTNESS - c * 0.5;
let (r, g, b) = if (0.0..60.0).contains(&hue) {
(c, x, 0.0)
} else if (60.0..120.0).contains(&hue) {
(x, c, 0.0)
} else if (120.0..180.0).contains(&hue) {
(0.0, c, x)
} else if (180.0..240.0).contains(&hue) {
(0.0, x, c)
} else if (240.0..300.0).contains(&hue) {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
let r = ((r + m) * 255.0) as u8;
let g = ((g + m) * 255.0) as u8;
let b = ((b + m) * 255.0) as u8;
[r, g, b]
}