use hephaestus::backend::vello::VelloRenderer;
use hephaestus::color::{rgb8, Color};
use hephaestus::geometry::Affine;
use hephaestus::mesh::Mesh;
use hephaestus::pick::PickId;
use hephaestus::primitives::{
polygon_gradient, polygon_ribbon_full, polyline_gradient, polyline_ribbon,
polyline_ribbon_full, RibbonOptions,
};
use hephaestus::scene::SceneBuilder;
use hephaestus::stroke::{Cap, Join};
use hephaestus::{Point, Renderer};
fn main() {
let dpi = 96.0;
let bg: Color = rgb8(248, 248, 252);
let mut renderer = VelloRenderer::new().expect("vello renderer init");
{
let (w, h) = (1200u32, 300u32);
let n = 80;
let xs: Vec<f64> = (0..n)
.map(|i| 80.0 + i as f64 / (n - 1) as f64 * 1040.0)
.collect();
let ys: Vec<f64> = xs
.iter()
.enumerate()
.map(|(i, _)| 150.0 + 60.0 * (i as f64 * 0.18).sin())
.collect();
let points: Vec<Point> = xs
.iter()
.zip(ys.iter())
.map(|(x, y)| Point::new(*x, *y))
.collect();
let stops = [
rgb8(60, 80, 200),
rgb8(80, 160, 200),
rgb8(220, 220, 220),
rgb8(220, 130, 80),
rgb8(200, 50, 60),
];
let colors: Vec<Color> = (0..n)
.map(|i| interpolate_stops(&stops, i as f64 / (n - 1) as f64))
.collect();
let opts = RibbonOptions {
half_width: pt_to_px(12.0, dpi) * 0.5,
cap: Cap::Round,
join: Join::Round,
miter_limit: 4.0,
};
let mesh = polyline_gradient(&points, &colors, &opts);
render_mesh(
&mut renderer,
&mesh,
w,
h,
dpi,
bg,
"examples/ribbon_1_gradient_along.png",
);
}
{
let (w, h) = (1200u32, 300u32);
let n = 80;
let xs: Vec<f64> = (0..n)
.map(|i| 80.0 + i as f64 / (n - 1) as f64 * 1040.0)
.collect();
let ys: Vec<f64> = (0..n)
.map(|i| 150.0 + 60.0 * (i as f64 * 0.18).sin())
.collect();
let points: Vec<Point> = xs
.iter()
.zip(ys.iter())
.map(|(x, y)| Point::new(*x, *y))
.collect();
let half_widths: Vec<f64> = (0..n)
.map(|i| {
let t = i as f64 / (n - 1) as f64;
pt_to_px(2.0 + (20.0 - 2.0) * t, dpi) * 0.5
})
.collect();
let opts = RibbonOptions {
half_width: 1.0,
cap: Cap::Round,
join: Join::Round,
miter_limit: 4.0,
};
let stroke = rgb8(40, 90, 180);
let colors = vec![stroke; n];
let mesh = polyline_ribbon_full(&points, Some(&colors), Some(&half_widths), &opts);
render_mesh(
&mut renderer,
&mesh,
w,
h,
dpi,
bg,
"examples/ribbon_2_variable_width.png",
);
}
{
let (w, h) = (1200u32, 400u32);
let n = 240;
let cx = 600.0;
let cy = 200.0;
let amp_x = 500.0;
let amp_y = 120.0;
let xs: Vec<f64> = (0..n)
.map(|i| {
let t = i as f64 / (n - 1) as f64 * std::f64::consts::TAU;
cx + amp_x * t.sin()
})
.collect();
let ys: Vec<f64> = (0..n)
.map(|i| {
let t = i as f64 / (n - 1) as f64 * std::f64::consts::TAU;
cy + amp_y * (2.0 * t).sin()
})
.collect();
let points: Vec<Point> = xs
.iter()
.zip(ys.iter())
.map(|(x, y)| Point::new(*x, *y))
.collect();
let stops = [
rgb8(60, 80, 200),
rgb8(80, 160, 200),
rgb8(220, 220, 220),
rgb8(220, 130, 80),
rgb8(200, 50, 60),
];
let colors: Vec<Color> = (0..n)
.map(|i| interpolate_stops(&stops, i as f64 / (n - 1) as f64))
.collect();
let half_widths: Vec<f64> = (0..n)
.map(|i| {
let t = i as f64 / (n - 1) as f64;
pt_to_px(2.0 + (24.0 - 2.0) * t, dpi) * 0.5
})
.collect();
let opts = RibbonOptions {
half_width: 1.0,
cap: Cap::Round,
join: Join::Round,
miter_limit: 4.0,
};
let mesh = polyline_ribbon_full(&points, Some(&colors), Some(&half_widths), &opts);
render_mesh(
&mut renderer,
&mesh,
w,
h,
dpi,
bg,
"examples/ribbon_3_full.png",
);
}
{
let (w, h) = (900u32, 700u32);
let caps = [
("butt", Cap::Butt),
("square", Cap::Square),
("round", Cap::Round),
];
let joins = [
("miter", Join::Miter),
("bevel", Join::Bevel),
("round", Join::Round),
];
let mut all_meshes: Vec<Mesh> = Vec::new();
let cell_w = w as f64 / 3.0;
let cell_h = h as f64 / 3.0;
for (r_idx, (_cap_name, cap)) in caps.iter().enumerate() {
for (c_idx, (_join_name, join)) in joins.iter().enumerate() {
let cx = c_idx as f64 * cell_w;
let cy = r_idx as f64 * cell_h;
let pad = 30.0;
let pts = vec![
Point::new(cx + pad, cy + cell_h - pad),
Point::new(cx + cell_w * 0.5, cy + pad),
Point::new(cx + cell_w - pad, cy + cell_h - pad),
];
let opts = RibbonOptions {
half_width: pt_to_px(18.0, dpi) * 0.5,
cap: *cap,
join: *join,
miter_limit: 4.0,
};
let stroke = rgb8(40, 90, 180);
all_meshes.push(polyline_ribbon(&pts, stroke, &opts));
}
}
render_meshes(
&mut renderer,
&all_meshes,
w,
h,
dpi,
bg,
"examples/ribbon_4_caps_joins.png",
);
}
{
let (w, h) = (700u32, 700u32);
let cx = 350.0;
let cy = 350.0;
let radius = 260.0;
let n = 12;
let points: Vec<Point> = (0..n)
.map(|i| {
let theta = i as f64 / n as f64 * std::f64::consts::TAU;
Point::new(cx + radius * theta.cos(), cy + radius * theta.sin())
})
.collect();
let colors: Vec<Color> = (0..n)
.map(|i| hsv_to_color(i as f64 / n as f64, 0.85, 0.95))
.collect();
let opts = RibbonOptions {
half_width: pt_to_px(28.0, dpi) * 0.5,
cap: Cap::Butt,
join: Join::Round,
miter_limit: 4.0,
};
let mesh = polygon_gradient(&points, &colors, &opts);
render_mesh(
&mut renderer,
&mesh,
w,
h,
dpi,
bg,
"examples/ribbon_5_closed_rainbow.png",
);
}
{
let (w, h) = (900u32, 900u32);
let cx = 450.0;
let cy = 450.0;
let base_r = 280.0;
let amp_r = 70.0;
let lobes = 5.0;
let n = 240;
let points: Vec<Point> = (0..n)
.map(|i| {
let theta = i as f64 / n as f64 * std::f64::consts::TAU;
let r = base_r + amp_r * (lobes * theta).sin();
Point::new(cx + r * theta.cos(), cy + r * theta.sin())
})
.collect();
let colors: Vec<Color> = (0..n)
.map(|i| hsv_to_color(i as f64 / n as f64, 0.85, 0.95))
.collect();
let half_widths: Vec<f64> = (0..n)
.map(|i| {
let theta = i as f64 / n as f64 * std::f64::consts::TAU;
let pt = 13.0 + 9.0 * (2.0 * lobes * theta).sin();
pt_to_px(pt, dpi) * 0.5
})
.collect();
let opts = RibbonOptions {
half_width: 1.0,
cap: Cap::Butt, join: Join::Round,
miter_limit: 4.0,
};
let mesh = polygon_ribbon_full(&points, Some(&colors), Some(&half_widths), &opts);
render_mesh(
&mut renderer,
&mesh,
w,
h,
dpi,
bg,
"examples/ribbon_6_closed_full.png",
);
}
}
fn hsv_to_color(h: f64, s: f64, v: f64) -> Color {
let h6 = (h.rem_euclid(1.0)) * 6.0;
let c = v * s;
let x = c * (1.0 - ((h6 % 2.0) - 1.0).abs());
let m = v - c;
let (r, g, b) = match h6 as u32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
Color::new([(r + m) as f32, (g + m) as f32, (b + m) as f32, 1.0])
}
fn pt_to_px(pt: f64, dpi: f64) -> f64 {
pt * dpi / 72.0
}
fn interpolate_stops(stops: &[Color], t: f64) -> Color {
if stops.is_empty() {
return Color::new([0.0, 0.0, 0.0, 1.0]);
}
if stops.len() == 1 {
return stops[0];
}
let scaled = t.clamp(0.0, 1.0) * (stops.len() - 1) as f64;
let lo = scaled.floor() as usize;
let hi = (lo + 1).min(stops.len() - 1);
let frac = (scaled - lo as f64) as f32;
let a = stops[lo].components;
let b = stops[hi].components;
Color::new([
a[0] * (1.0 - frac) + b[0] * frac,
a[1] * (1.0 - frac) + b[1] * frac,
a[2] * (1.0 - frac) + b[2] * frac,
a[3] * (1.0 - frac) + b[3] * frac,
])
}
fn render_mesh(
renderer: &mut VelloRenderer,
mesh: &Mesh,
w: u32,
h: u32,
dpi: f64,
bg: Color,
out: &str,
) {
let _ = dpi;
{
let scene = renderer.scene();
scene.clear();
scene.draw_mesh(mesh, Affine::IDENTITY, PickId::Skip);
}
let mut pixels = vec![0u8; (w * h * 4) as usize];
renderer
.render_to_buffer(w, h, bg, &mut pixels)
.expect("render");
let path = std::env::current_dir().unwrap().join(out);
hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
println!("wrote {}", path.display());
}
fn render_meshes(
renderer: &mut VelloRenderer,
meshes: &[Mesh],
w: u32,
h: u32,
dpi: f64,
bg: Color,
out: &str,
) {
let _ = dpi;
{
let scene = renderer.scene();
scene.clear();
for m in meshes {
scene.draw_mesh(m, Affine::IDENTITY, PickId::Skip);
}
}
let mut pixels = vec![0u8; (w * h * 4) as usize];
renderer
.render_to_buffer(w, h, bg, &mut pixels)
.expect("render");
let path = std::env::current_dir().unwrap().join(out);
hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
println!("wrote {}", path.display());
}