1#![allow(clippy::too_many_arguments)]
25
26use image::RgbaImage;
27use tiny_skia::{
28 FillRule, GradientStop, LinearGradient, Paint, PathBuilder, Pixmap, PixmapPaint, Point, PremultipliedColorU8, Rect,
29 Shader, SpreadMode, Stroke, Transform,
30};
31
32use crate::build::Doc;
33use crate::error::{Error, Result};
34use crate::model::Color;
35use crate::theme::{Insets, OutputFormat, RenderOptions};
36
37#[derive(Clone, Debug)]
39pub struct Radar {
40 pub fill: Color,
42 pub stroke: Color,
44 pub stroke_w: f32,
46 pub grid: Color,
48 pub grid_w: f32,
50 pub rings: u32,
52 pub vertex_dot: Option<(f32, Color)>,
54 pub start_deg: f32,
56}
57
58impl Default for Radar {
59 fn default() -> Self {
60 Self {
61 fill: Color::rgba(0x4c, 0x63, 0xb6, 0x66),
62 stroke: Color::rgb(0x4c, 0x63, 0xb6),
63 stroke_w: 2.0,
64 grid: Color::rgba(0x8b, 0x94, 0x9e, 0x55),
65 grid_w: 1.0,
66 rings: 4,
67 vertex_dot: Some((3.0, Color::rgb(0x4c, 0x63, 0xb6))),
68 start_deg: -90.0,
69 }
70 }
71}
72
73pub struct Canvas {
75 pix: Pixmap,
76 scale: f32,
77}
78
79impl Canvas {
80 pub fn new(w: f32, h: f32, scale: f32) -> Result<Canvas> {
82 let scale = if scale.is_finite() { scale.clamp(0.25, 8.0) } else { 2.0 };
83 let pw = (w * scale).round().max(1.0) as u32;
84 let ph = (h * scale).round().max(1.0) as u32;
85 let pix = Pixmap::new(pw, ph).ok_or_else(|| Error::Layout("画布尺寸非法(过大或为 0)".into()))?;
86 Ok(Canvas { pix, scale })
87 }
88
89 pub fn scale(&self) -> f32 {
91 self.scale
92 }
93 pub fn width_px(&self) -> u32 {
95 self.pix.width()
96 }
97 pub fn height_px(&self) -> u32 {
99 self.pix.height()
100 }
101
102 fn s(&self, v: f32) -> f32 {
104 v * self.scale
105 }
106
107 pub fn fill(&mut self, color: Color) {
109 self.pix.fill(skia(color));
110 }
111
112 pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, color: Color) {
114 if w <= 0.0 || h <= 0.0 || color.a == 0 {
115 return;
116 }
117 let mut paint = Paint::default();
118 paint.set_color_rgba8(color.r, color.g, color.b, color.a);
119 paint.anti_alias = true;
120 self.fill_rrect(x, y, w, h, radius, &paint);
121 }
122
123 pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, line_w: f32, color: Color) {
125 if w <= 0.0 || h <= 0.0 || line_w <= 0.0 || color.a == 0 {
126 return;
127 }
128 let Some(path) = self.rrect_path(x, y, w, h, radius) else { return };
129 let mut paint = Paint::default();
130 paint.set_color_rgba8(color.r, color.g, color.b, color.a);
131 paint.anti_alias = true;
132 let stroke = Stroke { width: self.s(line_w), ..Stroke::default() };
133 self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
134 }
135
136 pub fn v_gradient(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, top: Color, bottom: Color) {
138 if w <= 0.0 || h <= 0.0 {
139 return;
140 }
141 let (px, py, pw, ph) = (self.s(x), self.s(y), self.s(w), self.s(h));
142 let shader = LinearGradient::new(
143 Point::from_xy(px, py),
144 Point::from_xy(px, py + ph),
145 vec![GradientStop::new(0.0, skia(top)), GradientStop::new(1.0, skia(bottom))],
146 SpreadMode::Pad,
147 Transform::identity(),
148 );
149 let paint = Paint {
150 shader: shader.unwrap_or_else(|| Shader::SolidColor(skia(top))),
151 anti_alias: true,
152 ..Default::default()
153 };
154 if let Some(path) = rrect_path_px(px, py, pw, ph, self.s(radius)) {
156 self.pix.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
157 }
158 }
159
160 pub fn line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, line_w: f32, color: Color) {
162 if line_w <= 0.0 || color.a == 0 {
163 return;
164 }
165 let mut pb = PathBuilder::new();
166 pb.move_to(self.s(x0), self.s(y0));
167 pb.line_to(self.s(x1), self.s(y1));
168 let Some(path) = pb.finish() else { return };
169 let mut paint = Paint::default();
170 paint.set_color_rgba8(color.r, color.g, color.b, color.a);
171 paint.anti_alias = true;
172 let stroke = Stroke { width: self.s(line_w), line_cap: tiny_skia::LineCap::Round, ..Stroke::default() };
173 self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
174 }
175
176 pub fn disc(&mut self, cx: f32, cy: f32, r: f32, color: Color) {
178 if r <= 0.0 || color.a == 0 {
179 return;
180 }
181 let Some(path) = oval_path_px(self.s(cx), self.s(cy), self.s(r)) else { return };
182 let mut paint = Paint::default();
183 paint.set_color_rgba8(color.r, color.g, color.b, color.a);
184 paint.anti_alias = true;
185 self.pix.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
186 }
187
188 pub fn ring(&mut self, cx: f32, cy: f32, r: f32, line_w: f32, color: Color) {
190 if r <= 0.0 || line_w <= 0.0 || color.a == 0 {
191 return;
192 }
193 let Some(path) = oval_path_px(self.s(cx), self.s(cy), self.s(r)) else { return };
194 let mut paint = Paint::default();
195 paint.set_color_rgba8(color.r, color.g, color.b, color.a);
196 paint.anti_alias = true;
197 let stroke = Stroke { width: self.s(line_w), ..Stroke::default() };
198 self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
199 }
200
201 pub fn arc(&mut self, cx: f32, cy: f32, r: f32, start_deg: f32, sweep_deg: f32, line_w: f32, color: Color) {
204 if r <= 0.0 || line_w <= 0.0 || color.a == 0 || sweep_deg == 0.0 {
205 return;
206 }
207 let (cx, cy, r) = (self.s(cx), self.s(cy), self.s(r));
208 let steps = ((sweep_deg.abs() / 4.0).ceil() as usize).max(2);
209 let mut pb = PathBuilder::new();
210 for i in 0..=steps {
211 let t = start_deg + sweep_deg * (i as f32 / steps as f32);
212 let (x, y) = (cx + r * t.to_radians().cos(), cy + r * t.to_radians().sin());
213 if i == 0 {
214 pb.move_to(x, y);
215 } else {
216 pb.line_to(x, y);
217 }
218 }
219 let Some(path) = pb.finish() else { return };
220 let mut paint = Paint::default();
221 paint.set_color_rgba8(color.r, color.g, color.b, color.a);
222 paint.anti_alias = true;
223 let stroke = Stroke { width: self.s(line_w), line_cap: tiny_skia::LineCap::Round, ..Stroke::default() };
224 self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
225 }
226
227 pub fn polygon(&mut self, pts: &[(f32, f32)], fill: Option<Color>, stroke: Option<(f32, Color)>) {
229 if pts.len() < 2 {
230 return;
231 }
232 let Some(path) = self.poly_path(pts) else { return };
233 if let Some(c) = fill {
234 if c.a > 0 {
235 let mut paint = Paint::default();
236 paint.set_color_rgba8(c.r, c.g, c.b, c.a);
237 paint.anti_alias = true;
238 self.pix.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
239 }
240 }
241 if let Some((lw, c)) = stroke {
242 if lw > 0.0 && c.a > 0 {
243 let mut paint = Paint::default();
244 paint.set_color_rgba8(c.r, c.g, c.b, c.a);
245 paint.anti_alias = true;
246 let st = Stroke { width: self.s(lw), line_join: tiny_skia::LineJoin::Round, ..Stroke::default() };
247 self.pix.stroke_path(&path, &paint, &st, Transform::identity(), None);
248 }
249 }
250 }
251
252 pub fn radar(&mut self, cx: f32, cy: f32, r: f32, values: &[f32], st: &Radar) {
254 let n = values.len();
255 if n < 3 || r <= 0.0 {
256 return;
257 }
258 let angle = |i: usize| (st.start_deg + 360.0 * i as f32 / n as f32).to_radians();
259 let rings = st.rings.max(1);
261 for ring in 1..=rings {
262 let rr = r * ring as f32 / rings as f32;
263 let pts: Vec<(f32, f32)> = (0..n).map(|i| (cx + rr * angle(i).cos(), cy + rr * angle(i).sin())).collect();
264 self.polygon(&pts, None, Some((st.grid_w, st.grid)));
265 }
266 for i in 0..n {
268 let (ex, ey) = (cx + r * angle(i).cos(), cy + r * angle(i).sin());
269 self.line(cx, cy, ex, ey, st.grid_w, st.grid);
270 }
271 let data: Vec<(f32, f32)> = (0..n)
273 .map(|i| {
274 let v = values[i].clamp(0.0, 1.0);
275 (cx + r * v * angle(i).cos(), cy + r * v * angle(i).sin())
276 })
277 .collect();
278 self.polygon(&data, Some(st.fill), Some((st.stroke_w, st.stroke)));
279 if let Some((dr, dc)) = st.vertex_dot {
280 for &(x, y) in &data {
281 self.disc(x, y, dr, dc);
282 }
283 }
284 }
285
286 pub fn text(
289 &mut self,
290 x: f32,
291 y: f32,
292 box_w: f32,
293 opts: &RenderOptions,
294 build: impl FnOnce(&mut crate::build::ParaBuilder),
295 ) -> Result<f32> {
296 let mut doc = Doc::new();
297 doc.paragraph(build);
298 self.text_doc(x, y, box_w, opts, &doc.build())
299 }
300
301 pub fn text_doc(&mut self, x: f32, y: f32, box_w: f32, opts: &RenderOptions, doc: &crate::Document) -> Result<f32> {
303 let img = render_text_block(doc, opts, box_w, self.scale)?;
304 let h = img.height() as f32 / self.scale;
305 self.blit(&img, self.s(x).round() as i32, self.s(y).round() as i32);
306 Ok(h)
307 }
308
309 pub fn text_mid(
316 &mut self,
317 x: f32,
318 cy: f32,
319 box_w: f32,
320 opts: &RenderOptions,
321 build: impl FnOnce(&mut crate::build::ParaBuilder),
322 ) -> Result<f32> {
323 let mut doc = Doc::new();
324 doc.paragraph(build);
325 let img = render_text_block(&doc.build(), opts, box_w, self.scale)?;
326 let (ink_cy, advance) = match ink_box(&img) {
327 Some((_, y0, x1, y1)) => ((y0 + y1) as f32 / 2.0, (x1 + 1) as f32 / self.scale),
328 None => (img.height() as f32 / 2.0, 0.0),
329 };
330 let py = (self.s(cy) - ink_cy).round() as i32;
331 self.blit(&img, self.s(x).round() as i32, py);
332 Ok(advance)
333 }
334
335 pub fn blit(&mut self, img: &RgbaImage, px: i32, py: i32) {
337 if img.width() == 0 || img.height() == 0 {
338 return;
339 }
340 let Some(src) = rgba_to_pixmap(img) else { return };
341 self.pix.draw_pixmap(px, py, src.as_ref(), &PixmapPaint::default(), Transform::identity(), None);
342 }
343
344 pub fn encode(&self, format: OutputFormat) -> Result<Vec<u8>> {
346 crate::paint::encode_pixmap(&self.pix, format)
347 }
348
349 pub fn into_rgba(self) -> Result<RgbaImage> {
351 let (w, h) = (self.pix.width(), self.pix.height());
352 RgbaImage::from_raw(w, h, crate::paint::pixmap_to_rgba_bytes(&self.pix))
353 .ok_or_else(|| Error::Layout("RGBA 缓冲尺寸不符".into()))
354 }
355
356 fn fill_rrect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, paint: &Paint) {
359 if let Some(path) = self.rrect_path(x, y, w, h, radius) {
360 self.pix.fill_path(&path, paint, FillRule::Winding, Transform::identity(), None);
361 }
362 }
363
364 fn rrect_path(&self, x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<tiny_skia::Path> {
365 rrect_path_px(self.s(x), self.s(y), self.s(w), self.s(h), self.s(radius))
366 }
367
368 fn poly_path(&self, pts: &[(f32, f32)]) -> Option<tiny_skia::Path> {
369 let mut pb = PathBuilder::new();
370 pb.move_to(self.s(pts[0].0), self.s(pts[0].1));
371 for &(x, y) in &pts[1..] {
372 pb.line_to(self.s(x), self.s(y));
373 }
374 pb.close();
375 pb.finish()
376 }
377}
378
379fn rrect_path_px(x: f32, y: f32, w: f32, h: f32, r: f32) -> Option<tiny_skia::Path> {
381 if w <= 0.0 || h <= 0.0 {
382 return None;
383 }
384 let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
385 if r <= 0.0 {
386 return Rect::from_xywh(x, y, w, h).and_then(|rect| {
387 let mut pb = PathBuilder::new();
388 pb.push_rect(rect);
389 pb.finish()
390 });
391 }
392 let k = r * 0.552_285; let mut pb = PathBuilder::new();
394 pb.move_to(x + r, y);
395 pb.line_to(x + w - r, y);
396 pb.cubic_to(x + w - r + k, y, x + w, y + r - k, x + w, y + r);
397 pb.line_to(x + w, y + h - r);
398 pb.cubic_to(x + w, y + h - r + k, x + w - r + k, y + h, x + w - r, y + h);
399 pb.line_to(x + r, y + h);
400 pb.cubic_to(x + r - k, y + h, x, y + h - r + k, x, y + h - r);
401 pb.line_to(x, y + r);
402 pb.cubic_to(x, y + r - k, x + r - k, y, x + r, y);
403 pb.close();
404 pb.finish()
405}
406
407fn oval_path_px(cx: f32, cy: f32, r: f32) -> Option<tiny_skia::Path> {
409 let rect = Rect::from_xywh(cx - r, cy - r, r * 2.0, r * 2.0)?;
410 let mut pb = PathBuilder::new();
411 pb.push_oval(rect);
412 pb.finish()
413}
414
415fn render_text_block(doc: &crate::Document, base: &RenderOptions, box_w: f32, scale: f32) -> Result<RgbaImage> {
417 let mut o = base.clone();
418 o.width = box_w.max(1.0);
419 o.padding = Insets::all(0.0);
420 o.scale = scale;
421 o.header = None;
422 o.footer = None;
423 o.theme.background = Color::rgba(0, 0, 0, 0); let layout = crate::layout::layout_document(doc, &o)?;
425 crate::paint::paint_rgba(&layout, &o)
426}
427
428fn ink_box(img: &RgbaImage) -> Option<(u32, u32, u32, u32)> {
431 let (w, h) = (img.width(), img.height());
432 let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0u32, 0u32);
433 let mut any = false;
434 for y in 0..h {
435 for x in 0..w {
436 if img.get_pixel(x, y).0[3] > 12 {
437 any = true;
438 x0 = x0.min(x);
439 y0 = y0.min(y);
440 x1 = x1.max(x);
441 y1 = y1.max(y);
442 }
443 }
444 }
445 any.then_some((x0, y0, x1, y1))
446}
447
448fn rgba_to_pixmap(img: &RgbaImage) -> Option<Pixmap> {
450 let mut p = Pixmap::new(img.width(), img.height())?;
451 let buf = p.pixels_mut();
452 for (i, px) in img.pixels().enumerate() {
453 let [r, g, b, a] = px.0;
454 let pm = |c: u8| ((c as u16 * a as u16 + 127) / 255) as u8;
455 buf[i] = PremultipliedColorU8::from_rgba(pm(r), pm(g), pm(b), a)
456 .unwrap_or_else(|| PremultipliedColorU8::from_rgba(0, 0, 0, 0).unwrap());
457 }
458 Some(p)
459}
460
461fn skia(c: Color) -> tiny_skia::Color {
462 tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
463}