1use hephaestus::backend::vello::VelloRenderer;
11use hephaestus::color::{rgb8, Color};
12use hephaestus::composition::{beside, Composition, Patch, Slot, Span};
13use hephaestus::layout::{Cell, Track};
14use hephaestus::stroke::Stroke;
15use hephaestus::text::{draw_text_in_rect, TextRun, TextStyle};
16use hephaestus::{Affine, Brush, FillRule, Path, PickId, Renderer, SceneBuilder};
17use kurbo::Shape;
18
19fn text_cell(text: &str, size: f32) -> Cell {
20 Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
21}
22
23fn weighted_text_cell(text: &str, size: f32, weight: u16) -> Cell {
24 Cell::measured(TextRun::new(
25 text,
26 &TextStyle::new(size).weight(weight),
27 96.0,
28 ))
29}
30
31fn color_for_region(region: &str) -> Color {
32 match region {
33 "panel" => rgb8(40, 50, 70),
34 "title" => rgb8(220, 180, 90),
35 "subtitle" => rgb8(180, 150, 70),
36 "caption" => rgb8(180, 130, 90),
37 "axis_left" | "axis_right" | "axis_top" | "axis_bottom" => rgb8(160, 100, 130),
38 "axis_left_title" | "axis_right_title" | "axis_top_title" | "axis_bottom_title" => {
39 rgb8(200, 120, 160)
40 }
41 "strip_left" | "strip_right" | "strip_top" | "strip_bottom" => rgb8(90, 140, 180),
42 "legend_left" | "legend_right" | "legend_top" | "legend_bottom" => rgb8(110, 180, 140),
43 _ => rgb8(120, 120, 120),
44 }
45}
46
47fn build_inner_patch(id: &str, y_axis_text: &str, y_axis_title: &str, x_axis_title: &str) -> Patch {
50 Patch::new(id)
51 .slot(Slot::AxisLeft, text_cell(y_axis_text, 12.0))
52 .slot(
53 Slot::AxisLeftTitle,
54 weighted_text_cell(y_axis_title, 13.0, 500),
55 )
56 .slot(Slot::AxisBottom, text_cell("0 25 50 75 100", 12.0))
57 .slot(
58 Slot::AxisBottomTitle,
59 weighted_text_cell(x_axis_title, 13.0, 500),
60 )
61 .slot(Slot::Panel, Cell::empty())
62}
63
64fn main() {
65 let (w, h) = (1200u32, 700u32);
66 let dpi = 96.0;
67
68 let inner_a = build_inner_patch("plot_a", "0\n50\n100", "Pressure (kPa)", "Time (s)");
69 let inner_b = build_inner_patch(
70 "plot_b",
71 "0\n10000\n20000\n30000",
72 "Particle count",
73 "Time (s)",
74 );
75
76 let header = Patch::new("header")
80 .slot(
81 Slot::Title,
82 weighted_text_cell("Reactor diagnostics", 28.0, 700),
83 )
84 .slot(
85 Slot::Subtitle,
86 text_cell("Pressure and particle count over the morning run", 16.0),
87 );
88 let footer = Patch::new("footer").slot(
89 Slot::Caption,
90 text_cell("Source: in-line telemetry, smoothed at 1s intervals", 11.0),
91 );
92 let composed = Composition::empty(3, 1)
93 .heights(vec![Track::Fr(0.0), Track::Fr(1.0), Track::Fr(0.0)])
94 .place(1, 1, Span::cell(), header)
95 .place(2, 1, Span::cell(), beside(inner_a, inner_b))
96 .place(3, 1, Span::cell(), footer);
97
98 let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
99
100 let mut renderer = VelloRenderer::new().expect("vello renderer init");
101 {
102 let scene = renderer.scene();
103 let stroke = Stroke::new(1.0);
104 let text_brush: Brush = rgb8(30, 30, 40).into();
105
106 for (_id, region, rect) in layout.iter() {
108 if region == "panel" {
109 continue;
110 }
111 let color = color_for_region(region);
112 let tinted = Color::new([
113 color.components[0],
114 color.components[1],
115 color.components[2],
116 0.18,
117 ]);
118 let path: Path = rect.to_path(0.1);
119 scene.fill(
120 FillRule::NonZero,
121 Affine::IDENTITY,
122 &Brush::Solid(tinted),
123 None,
124 &path,
125 PickId::Skip,
126 );
127 scene.stroke(
128 &stroke,
129 Affine::IDENTITY,
130 &Brush::Solid(color),
131 None,
132 &path,
133 PickId::Skip,
134 );
135 }
136
137 for (_id, region, rect) in layout.iter() {
139 if region != "panel" {
140 continue;
141 }
142 let path: Path = rect.to_path(0.1);
143 scene.fill(
144 FillRule::NonZero,
145 Affine::IDENTITY,
146 &Brush::Solid(color_for_region(region)),
147 None,
148 &path,
149 PickId::Skip,
150 );
151 scene.stroke(
152 &stroke,
153 Affine::IDENTITY,
154 &Brush::Solid(rgb8(255, 255, 255)),
155 None,
156 &path,
157 PickId::Skip,
158 );
159 }
160
161 for (id, region, rect) in layout.iter() {
166 if region == "panel" {
167 continue;
168 }
169 let text = match (id, region) {
170 ("header", "title") => "Reactor diagnostics",
171 ("header", "subtitle") => "Pressure and particle count over the morning run",
172 ("footer", "caption") => "Source: in-line telemetry, smoothed at 1s intervals",
173 ("plot_a", "axis_left") => "0\n50\n100",
174 ("plot_a", "axis_left_title") => "Pressure (kPa)",
175 ("plot_a", "axis_bottom") => "0 25 50 75 100",
176 ("plot_a", "axis_bottom_title") => "Time (s)",
177 ("plot_b", "axis_left") => "0\n10000\n20000\n30000",
178 ("plot_b", "axis_left_title") => "Particle count",
179 ("plot_b", "axis_bottom") => "0 25 50 75 100",
180 ("plot_b", "axis_bottom_title") => "Time (s)",
181 _ => continue,
182 };
183 let size = match region {
184 "title" => 28.0,
185 "subtitle" => 16.0,
186 "caption" => 11.0,
187 "axis_left_title" | "axis_bottom_title" => 13.0,
188 _ => 12.0,
189 };
190 let weight = match region {
191 "title" => 700,
192 "axis_left_title" | "axis_bottom_title" => 500,
193 _ => 400,
194 };
195 let style = TextStyle::new(size).weight(weight);
196 let run = TextRun::new(text, &style, 96.0);
197 draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
198 }
199 }
200
201 let mut pixels = vec![0u8; (w * h * 4) as usize];
202 let bg: Color = rgb8(248, 248, 252);
203 renderer
204 .render_to_buffer(w, h, bg, &mut pixels)
205 .expect("render");
206
207 let path = std::env::current_dir()
208 .unwrap()
209 .join("examples/composition_demo.png");
210 hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
211 println!("wrote {}", path.display());
212
213 let panel_a = layout.get("plot_a", Slot::Panel).unwrap();
214 let panel_b = layout.get("plot_b", Slot::Panel).unwrap();
215 let title = layout.get("header", Slot::Title).unwrap();
216 println!("plot_a.panel = {:?}", panel_a);
217 println!("plot_b.panel = {:?}", panel_b);
218 println!("header.title = {:?}", title);
219}