Skip to content

Instantly share code, notes, and snippets.

@gre
Created May 16, 2023 17:43
Show Gist options
  • Save gre/5b3167863dd1009b4b73d64e6b8b109f to your computer and use it in GitHub Desktop.
Save gre/5b3167863dd1009b4b73d64e6b8b109f to your computer and use it in GitHub Desktop.
use std::f64::consts::PI;
use svg::node::element::path::Data;
use svg::node::element::{Group, Image, Path};
fn heart_function(t: f64) -> (f64, f64) {
let x = 16.0 * f64::sin(t).powi(3);
let y = -13.0 * f64::cos(t)
+ 5.0 * f64::cos(2.0 * t)
+ 2.0 * f64::cos(3.0 * t)
+ f64::cos(4.0 * t);
(x, y)
}
fn build_hexagon(center: (f64, f64), radius: f64) -> Vec<(f64, f64)> {
let mut points = Vec::new();
let angle_step = 2.0 * PI / 6.0;
for i in 0..6 {
let angle = i as f64 * angle_step;
let x = center.0 + radius * angle.cos();
let y = center.1 + radius * angle.sin();
points.push((x, y));
}
points
}
fn point_in_polygon(point: (f64, f64), polygon: &Vec<(f64, f64)>) -> bool {
let mut inside = false;
let mut j = polygon.len() - 1;
for i in 0..polygon.len() {
let (xi, yi) = polygon[i];
let (xj, yj) = polygon[j];
let intersect = ((yi > point.1) != (yj > point.1))
&& (point.0 < (xj - xi) * (point.1 - yi) / (yj - yi) + xi);
if intersect {
inside = !inside;
}
j = i;
}
inside
}
pub fn art() -> svg::Document {
let width = 200.;
let height = 200.;
let hex_border = 6.0;
let hex_rad = 92.0;
let hex_poly = build_hexagon((width / 2.0, height / 2.0), hex_rad);
let mut routes_bg = Vec::new();
let mut routes_bg_fill = Vec::new();
let stroke_width = 0.5;
let heart_size = 0.2;
let distx = 24.0 * heart_size;
let disty = 24.0 * heart_size;
let count = 100;
let mut base_shape: Vec<(f64, f64)> = (0..count)
.map(|i| {
let t = i as f64 * 2.0 * PI / (count as f64);
let p = heart_function(t);
(p.0 * heart_size, p.1 * heart_size)
})
.collect();
base_shape.push(base_shape[0]);
let mut xi = 0usize;
let mut x = 0.0;
while x < width {
let mut y = 0.0;
let mut yi = 0usize;
while y < height {
if !point_in_polygon((x, y), &hex_poly) {
y += disty;
yi += 1;
continue;
}
let route: Vec<(f64, f64)> =
base_shape.iter().map(|p| (p.0 + x, p.1 + y)).collect();
let is_outside = |p| !point_in_polygon(p, &hex_poly);
routes_bg.extend(clip_routes(&vec![route.clone()], &is_outside, 0.5, 4));
if (xi + 2 * yi) % 4 == 0 {
routes_bg_fill.push(route.clone());
}
y += disty;
yi += 1;
}
x += distx;
xi += 1;
}
let mut document = svg::Document::new()
.set("viewBox", (0, 0, width, height))
.set("width", format!("{}mm", width))
.set("height", format!("{}mm", height))
.set(
"xmlns:inkscape",
"http://www.inkscape.org/namespaces/inkscape",
)
.set("xmlns", "http://www.w3.org/2000/svg");
let mut data1 = Data::new();
for route in routes_bg {
data1 = render_route(data1, route);
}
let mut data2 = Data::new();
data2 = render_route(data2, hex_poly);
data2 = data2.close();
let mut g = Group::new()
//.set("opacity", 0.5)
.add(
Path::new()
.set("fill", "none")
.set("stroke", "black")
.set("stroke-width", stroke_width)
.set("d", data1),
);
for route in routes_bg_fill {
let mut data = Data::new();
data = render_route(data, route);
g = g.add(
Path::new()
.set("fill", "black")
.set("stroke", "none")
.set("d", data),
);
}
document = document.add(g);
document = document.add(
Path::new()
.set("fill", "none")
.set("stroke", "black")
.set("stroke-width", hex_border)
.set("d", data2),
);
/*
document = document.add(
Image::new()
.set("width", width)
.set("height", height)
.set("href", "/Users/gre/dev/urne-mariage/embedded.svg"),
);
*/
document
}
fn render_route(data: Data, route: Vec<(f64, f64)>) -> Data {
if route.len() == 0 {
return data;
}
let first_p = route[0];
let mut d = data.move_to(first_p);
for p in route {
d = d.line_to(p);
}
return d;
}
fn clip_routes(
input_routes: &Vec<Vec<(f64, f64)>>,
is_outside: &dyn Fn((f64, f64)) -> bool,
stepping: f64,
dichotomic_iterations: usize,
) -> Vec<Vec<(f64, f64)>> {
// locate the intersection where inside and outside cross
let search = |inside: (f64, f64), outside: (f64, f64), n| {
let mut a = inside;
let mut b = outside;
for _i in 0..n {
let middle = lerp_point(a, b, 0.5);
if is_outside(middle) {
b = middle;
} else {
a = middle;
}
}
return lerp_point(a, b, 0.5);
};
let mut routes = vec![];
for input_route in input_routes.iter() {
if input_route.len() < 2 {
continue;
}
let mut prev = input_route[0];
let mut prev_is_outside = is_outside(prev);
let mut route = vec![];
if !prev_is_outside {
// prev is not to crop. we can start with it
route.push(prev);
}
for &p in input_route.iter().skip(1) {
// we iterate in small steps to detect any interruption
let static_prev = prev;
let dx = p.0 - prev.0;
let dy = p.1 - prev.1;
let d = (dx * dx + dy * dy).sqrt();
let vx = dx / d;
let vy = dy / d;
let iterations = (d / stepping).ceil() as usize;
let mut v = 0.0;
for _i in 0..iterations {
v = (v + stepping).min(d);
let q = (static_prev.0 + vx * v, static_prev.1 + vy * v);
let q_is_outside = is_outside(q);
if prev_is_outside != q_is_outside {
// we have a crossing. we search it precisely
let intersection = if prev_is_outside {
search(q, prev, dichotomic_iterations)
} else {
search(prev, q, dichotomic_iterations)
};
if q_is_outside {
// we close the path
route.push(intersection);
if route.len() > 1 {
// we have a valid route to accumulate
routes.push(route);
}
route = vec![];
} else {
// we open the path
route.push(intersection);
}
prev_is_outside = q_is_outside;
}
prev = q;
}
// prev should be == p
if !prev_is_outside {
// prev is not to crop. we can start with it
route.push(p);
}
}
if route.len() > 1 {
// we have a valid route to accumulate
routes.push(route);
}
}
routes
}
#[inline]
fn lerp_point(a: (f64, f64), b: (f64, f64), m: f64) -> (f64, f64) {
(a.0 * (1. - m) + b.0 * m, a.1 * (1. - m) + b.1 * m)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment