1use hephaestus::backend::vello::VelloRenderer;
25use hephaestus::color::{rgb8, Color};
26use hephaestus::composition::{grid, Patch, Slot};
27use hephaestus::layout::Cell;
28use hephaestus::text::{draw_text_in_rect, TextRun, TextStyle};
29use hephaestus::{Affine, Brush, FillRule, Path, PickId, Renderer, SceneBuilder};
30use kurbo::Shape;
31
32fn text_cell(text: &str, size: f32) -> Cell {
33 Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
34}
35
36fn weighted_text_cell(text: &str, size: f32, weight: u16) -> Cell {
37 Cell::measured(TextRun::new(
38 text,
39 &TextStyle::new(size).weight(weight),
40 96.0,
41 ))
42}
43
44fn facet(id: &str, axis_left: &str, axis_bottom: &str) -> Patch {
45 Patch::new(id)
46 .slot(Slot::AxisLeft, text_cell(axis_left, 11.0))
47 .slot(Slot::AxisBottom, text_cell(axis_bottom, 11.0))
48 .slot(Slot::Panel, Cell::empty())
49}
50
51fn color_for_region(region: &str) -> Color {
52 match region {
53 "panel" => rgb8(40, 60, 90),
54 "title" => rgb8(220, 180, 90),
55 "subtitle" => rgb8(180, 150, 70),
56 "caption" => rgb8(180, 130, 90),
57 "axis_left" | "axis_bottom" => rgb8(160, 100, 130),
58 _ => rgb8(120, 120, 120),
59 }
60}
61
62fn main() {
63 let (w, h) = (1400u32, 800u32);
64 let dpi = 96.0;
65
66 let mut facet_cells = Vec::new();
70 for r in 1..=2 {
71 for c in 1..=3 {
72 let id = format!("f{}_{}", r, c);
73 facet_cells.push(facet(&id, "0\n50\n100", "0 25 50").into());
74 }
75 }
76 let composed = grid(2, 3, facet_cells)
77 .id("plot")
78 .slot(
79 Slot::Title,
80 weighted_text_cell("Reactor diagnostics across runs", 26.0, 700),
81 )
82 .slot(
83 Slot::Subtitle,
84 text_cell(
85 "Each panel: a separate 60-minute run; same scales throughout",
86 15.0,
87 ),
88 )
89 .slot(
90 Slot::AxisLeftTitle,
91 weighted_text_cell("Pressure (kPa)", 14.0, 500),
92 )
93 .slot(
94 Slot::AxisBottomTitle,
95 weighted_text_cell("Elapsed time (minutes)", 14.0, 500),
96 )
97 .slot(
98 Slot::Caption,
99 text_cell(
100 "Source: in-line telemetry, smoothed at 1s; runs ordered chronologically.",
101 11.0,
102 ),
103 );
104
105 let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
106
107 let mut renderer = VelloRenderer::new().expect("vello renderer init");
108 {
109 let scene = renderer.scene();
110 let stroke = hephaestus::stroke::Stroke::new(1.0);
111 let text_brush: Brush = rgb8(20, 20, 30).into();
112
113 for (_id, region, rect) in layout.iter() {
114 if region == "panel" {
115 continue;
116 }
117 let c = color_for_region(region);
118 let tint = Color::new([c.components[0], c.components[1], c.components[2], 0.20]);
119 let path: Path = rect.to_path(0.1);
120 scene.fill(
121 FillRule::NonZero,
122 Affine::IDENTITY,
123 &Brush::Solid(tint),
124 None,
125 &path,
126 PickId::Skip,
127 );
128 scene.stroke(
129 &stroke,
130 Affine::IDENTITY,
131 &Brush::Solid(c),
132 None,
133 &path,
134 PickId::Skip,
135 );
136 }
137 for (_id, region, rect) in layout.iter() {
138 if region != "panel" {
139 continue;
140 }
141 let path: Path = rect.to_path(0.1);
142 scene.fill(
143 FillRule::NonZero,
144 Affine::IDENTITY,
145 &Brush::Solid(color_for_region(region)),
146 None,
147 &path,
148 PickId::Skip,
149 );
150 scene.stroke(
151 &stroke,
152 Affine::IDENTITY,
153 &Brush::Solid(rgb8(255, 255, 255)),
154 None,
155 &path,
156 PickId::Skip,
157 );
158 }
159
160 for (id, region, rect) in layout.iter() {
161 let (text, size, weight) = match (id, region) {
162 ("plot", "title") => ("Reactor diagnostics across runs", 26.0, 700),
163 ("plot", "subtitle") => (
164 "Each panel: a separate 60-minute run; same scales throughout",
165 15.0,
166 400,
167 ),
168 ("plot", "caption") => (
169 "Source: in-line telemetry, smoothed at 1s; runs ordered chronologically.",
170 11.0,
171 400,
172 ),
173 ("plot", "axis_left_title") => ("Pressure (kPa)", 14.0, 500),
174 ("plot", "axis_bottom_title") => ("Elapsed time (minutes)", 14.0, 500),
175 (_, "axis_left") => ("0\n50\n100", 11.0, 400),
176 (_, "axis_bottom") => ("0 25 50", 11.0, 400),
177 _ => continue,
178 };
179 let run = TextRun::new(text, &TextStyle::new(size).weight(weight), 96.0);
180 draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
181 }
182 }
183
184 let mut pixels = vec![0u8; (w * h * 4) as usize];
185 let bg: Color = rgb8(248, 248, 252);
186 renderer
187 .render_to_buffer(w, h, bg, &mut pixels)
188 .expect("render");
189
190 let path = std::env::current_dir()
191 .unwrap()
192 .join("examples/nesting_faceted_title.png");
193 hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
194 println!("wrote {}", path.display());
195
196 let title = layout.get("plot", Slot::Title).unwrap();
198 let caption = layout.get("plot", Slot::Caption).unwrap();
199 let axis_left_title = layout.get("plot", Slot::AxisLeftTitle).unwrap();
200 let axis_bottom_title = layout.get("plot", Slot::AxisBottomTitle).unwrap();
201 let f1_1_panel = layout.get("f1_1", Slot::Panel).unwrap();
202 let f1_3_panel = layout.get("f1_3", Slot::Panel).unwrap();
203 let f2_3_panel = layout.get("f2_3", Slot::Panel).unwrap();
204 println!(
205 "plot.title: x0={:>5.0} x1={:>5.0} y0={:>5.0} y1={:>5.0}",
206 title.x0, title.x1, title.y0, title.y1
207 );
208 println!(
209 "plot.caption: x0={:>5.0} x1={:>5.0} y0={:>5.0} y1={:>5.0}",
210 caption.x0, caption.x1, caption.y0, caption.y1
211 );
212 println!(
213 "plot.axis_left_title: x0={:>5.0} x1={:>5.0} y0={:>5.0} y1={:>5.0}",
214 axis_left_title.x0, axis_left_title.x1, axis_left_title.y0, axis_left_title.y1
215 );
216 println!(
217 "plot.axis_bottom_title: x0={:>5.0} x1={:>5.0} y0={:>5.0} y1={:>5.0}",
218 axis_bottom_title.x0, axis_bottom_title.x1, axis_bottom_title.y0, axis_bottom_title.y1
219 );
220 println!(
221 "facets panel band: x0={:>5.0} x1={:>5.0} y0={:>5.0} y1={:>5.0}",
222 f1_1_panel.x0, f1_3_panel.x1, f1_1_panel.y0, f2_3_panel.y1
223 );
224}