#![allow(clippy::too_many_arguments)]
use image::RgbaImage;
use tiny_skia::{
FillRule, GradientStop, LinearGradient, Paint, PathBuilder, Pixmap, PixmapPaint, Point, PremultipliedColorU8, Rect,
Shader, SpreadMode, Stroke, Transform,
};
use crate::build::Doc;
use crate::error::{Error, Result};
use crate::model::Color;
use crate::theme::{Insets, OutputFormat, RenderOptions};
#[derive(Clone, Debug)]
pub struct Radar {
pub fill: Color,
pub stroke: Color,
pub stroke_w: f32,
pub grid: Color,
pub grid_w: f32,
pub rings: u32,
pub vertex_dot: Option<(f32, Color)>,
pub start_deg: f32,
}
impl Default for Radar {
fn default() -> Self {
Self {
fill: Color::rgba(0x4c, 0x63, 0xb6, 0x66),
stroke: Color::rgb(0x4c, 0x63, 0xb6),
stroke_w: 2.0,
grid: Color::rgba(0x8b, 0x94, 0x9e, 0x55),
grid_w: 1.0,
rings: 4,
vertex_dot: Some((3.0, Color::rgb(0x4c, 0x63, 0xb6))),
start_deg: -90.0,
}
}
}
pub struct Canvas {
pix: Pixmap,
scale: f32,
}
impl Canvas {
pub fn new(w: f32, h: f32, scale: f32) -> Result<Canvas> {
let scale = if scale.is_finite() { scale.clamp(0.25, 8.0) } else { 2.0 };
let pw = (w * scale).round().max(1.0) as u32;
let ph = (h * scale).round().max(1.0) as u32;
let pix = Pixmap::new(pw, ph).ok_or_else(|| Error::Layout("画布尺寸非法(过大或为 0)".into()))?;
Ok(Canvas { pix, scale })
}
pub fn scale(&self) -> f32 {
self.scale
}
pub fn width_px(&self) -> u32 {
self.pix.width()
}
pub fn height_px(&self) -> u32 {
self.pix.height()
}
fn s(&self, v: f32) -> f32 {
v * self.scale
}
pub fn fill(&mut self, color: Color) {
self.pix.fill(skia(color));
}
pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, color: Color) {
if w <= 0.0 || h <= 0.0 || color.a == 0 {
return;
}
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
self.fill_rrect(x, y, w, h, radius, &paint);
}
pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, line_w: f32, color: Color) {
if w <= 0.0 || h <= 0.0 || line_w <= 0.0 || color.a == 0 {
return;
}
let Some(path) = self.rrect_path(x, y, w, h, radius) else { return };
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let stroke = Stroke { width: self.s(line_w), ..Stroke::default() };
self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
pub fn v_gradient(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, top: Color, bottom: Color) {
if w <= 0.0 || h <= 0.0 {
return;
}
let (px, py, pw, ph) = (self.s(x), self.s(y), self.s(w), self.s(h));
let shader = LinearGradient::new(
Point::from_xy(px, py),
Point::from_xy(px, py + ph),
vec![GradientStop::new(0.0, skia(top)), GradientStop::new(1.0, skia(bottom))],
SpreadMode::Pad,
Transform::identity(),
);
let paint = Paint {
shader: shader.unwrap_or_else(|| Shader::SolidColor(skia(top))),
anti_alias: true,
..Default::default()
};
if let Some(path) = rrect_path_px(px, py, pw, ph, self.s(radius)) {
self.pix.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
}
pub fn line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, line_w: f32, color: Color) {
if line_w <= 0.0 || color.a == 0 {
return;
}
let mut pb = PathBuilder::new();
pb.move_to(self.s(x0), self.s(y0));
pb.line_to(self.s(x1), self.s(y1));
let Some(path) = pb.finish() else { return };
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let stroke = Stroke { width: self.s(line_w), line_cap: tiny_skia::LineCap::Round, ..Stroke::default() };
self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
pub fn disc(&mut self, cx: f32, cy: f32, r: f32, color: Color) {
if r <= 0.0 || color.a == 0 {
return;
}
let Some(path) = oval_path_px(self.s(cx), self.s(cy), self.s(r)) else { return };
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
self.pix.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
pub fn ring(&mut self, cx: f32, cy: f32, r: f32, line_w: f32, color: Color) {
if r <= 0.0 || line_w <= 0.0 || color.a == 0 {
return;
}
let Some(path) = oval_path_px(self.s(cx), self.s(cy), self.s(r)) else { return };
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let stroke = Stroke { width: self.s(line_w), ..Stroke::default() };
self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
pub fn arc(&mut self, cx: f32, cy: f32, r: f32, start_deg: f32, sweep_deg: f32, line_w: f32, color: Color) {
if r <= 0.0 || line_w <= 0.0 || color.a == 0 || sweep_deg == 0.0 {
return;
}
let (cx, cy, r) = (self.s(cx), self.s(cy), self.s(r));
let steps = ((sweep_deg.abs() / 4.0).ceil() as usize).max(2);
let mut pb = PathBuilder::new();
for i in 0..=steps {
let t = start_deg + sweep_deg * (i as f32 / steps as f32);
let (x, y) = (cx + r * t.to_radians().cos(), cy + r * t.to_radians().sin());
if i == 0 {
pb.move_to(x, y);
} else {
pb.line_to(x, y);
}
}
let Some(path) = pb.finish() else { return };
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let stroke = Stroke { width: self.s(line_w), line_cap: tiny_skia::LineCap::Round, ..Stroke::default() };
self.pix.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
pub fn polygon(&mut self, pts: &[(f32, f32)], fill: Option<Color>, stroke: Option<(f32, Color)>) {
if pts.len() < 2 {
return;
}
let Some(path) = self.poly_path(pts) else { return };
if let Some(c) = fill {
if c.a > 0 {
let mut paint = Paint::default();
paint.set_color_rgba8(c.r, c.g, c.b, c.a);
paint.anti_alias = true;
self.pix.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
}
if let Some((lw, c)) = stroke {
if lw > 0.0 && c.a > 0 {
let mut paint = Paint::default();
paint.set_color_rgba8(c.r, c.g, c.b, c.a);
paint.anti_alias = true;
let st = Stroke { width: self.s(lw), line_join: tiny_skia::LineJoin::Round, ..Stroke::default() };
self.pix.stroke_path(&path, &paint, &st, Transform::identity(), None);
}
}
}
pub fn radar(&mut self, cx: f32, cy: f32, r: f32, values: &[f32], st: &Radar) {
let n = values.len();
if n < 3 || r <= 0.0 {
return;
}
let angle = |i: usize| (st.start_deg + 360.0 * i as f32 / n as f32).to_radians();
let rings = st.rings.max(1);
for ring in 1..=rings {
let rr = r * ring as f32 / rings as f32;
let pts: Vec<(f32, f32)> = (0..n).map(|i| (cx + rr * angle(i).cos(), cy + rr * angle(i).sin())).collect();
self.polygon(&pts, None, Some((st.grid_w, st.grid)));
}
for i in 0..n {
let (ex, ey) = (cx + r * angle(i).cos(), cy + r * angle(i).sin());
self.line(cx, cy, ex, ey, st.grid_w, st.grid);
}
let data: Vec<(f32, f32)> = (0..n)
.map(|i| {
let v = values[i].clamp(0.0, 1.0);
(cx + r * v * angle(i).cos(), cy + r * v * angle(i).sin())
})
.collect();
self.polygon(&data, Some(st.fill), Some((st.stroke_w, st.stroke)));
if let Some((dr, dc)) = st.vertex_dot {
for &(x, y) in &data {
self.disc(x, y, dr, dc);
}
}
}
pub fn text(
&mut self,
x: f32,
y: f32,
box_w: f32,
opts: &RenderOptions,
build: impl FnOnce(&mut crate::build::ParaBuilder),
) -> Result<f32> {
let mut doc = Doc::new();
doc.paragraph(build);
self.text_doc(x, y, box_w, opts, &doc.build())
}
pub fn text_doc(&mut self, x: f32, y: f32, box_w: f32, opts: &RenderOptions, doc: &crate::Document) -> Result<f32> {
let img = render_text_block(doc, opts, box_w, self.scale)?;
let h = img.height() as f32 / self.scale;
self.blit(&img, self.s(x).round() as i32, self.s(y).round() as i32);
Ok(h)
}
pub fn text_mid(
&mut self,
x: f32,
cy: f32,
box_w: f32,
opts: &RenderOptions,
build: impl FnOnce(&mut crate::build::ParaBuilder),
) -> Result<f32> {
let mut doc = Doc::new();
doc.paragraph(build);
let img = render_text_block(&doc.build(), opts, box_w, self.scale)?;
let (ink_cy, advance) = match ink_box(&img) {
Some((_, y0, x1, y1)) => ((y0 + y1) as f32 / 2.0, (x1 + 1) as f32 / self.scale),
None => (img.height() as f32 / 2.0, 0.0),
};
let py = (self.s(cy) - ink_cy).round() as i32;
self.blit(&img, self.s(x).round() as i32, py);
Ok(advance)
}
pub fn blit(&mut self, img: &RgbaImage, px: i32, py: i32) {
if img.width() == 0 || img.height() == 0 {
return;
}
let Some(src) = rgba_to_pixmap(img) else { return };
self.pix.draw_pixmap(px, py, src.as_ref(), &PixmapPaint::default(), Transform::identity(), None);
}
pub fn encode(&self, format: OutputFormat) -> Result<Vec<u8>> {
crate::paint::encode_pixmap(&self.pix, format)
}
pub fn into_rgba(self) -> Result<RgbaImage> {
let (w, h) = (self.pix.width(), self.pix.height());
RgbaImage::from_raw(w, h, crate::paint::pixmap_to_rgba_bytes(&self.pix))
.ok_or_else(|| Error::Layout("RGBA 缓冲尺寸不符".into()))
}
fn fill_rrect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, paint: &Paint) {
if let Some(path) = self.rrect_path(x, y, w, h, radius) {
self.pix.fill_path(&path, paint, FillRule::Winding, Transform::identity(), None);
}
}
fn rrect_path(&self, x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<tiny_skia::Path> {
rrect_path_px(self.s(x), self.s(y), self.s(w), self.s(h), self.s(radius))
}
fn poly_path(&self, pts: &[(f32, f32)]) -> Option<tiny_skia::Path> {
let mut pb = PathBuilder::new();
pb.move_to(self.s(pts[0].0), self.s(pts[0].1));
for &(x, y) in &pts[1..] {
pb.line_to(self.s(x), self.s(y));
}
pb.close();
pb.finish()
}
}
fn rrect_path_px(x: f32, y: f32, w: f32, h: f32, r: f32) -> Option<tiny_skia::Path> {
if w <= 0.0 || h <= 0.0 {
return None;
}
let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
if r <= 0.0 {
return Rect::from_xywh(x, y, w, h).and_then(|rect| {
let mut pb = PathBuilder::new();
pb.push_rect(rect);
pb.finish()
});
}
let k = r * 0.552_285; let mut pb = PathBuilder::new();
pb.move_to(x + r, y);
pb.line_to(x + w - r, y);
pb.cubic_to(x + w - r + k, y, x + w, y + r - k, x + w, y + r);
pb.line_to(x + w, y + h - r);
pb.cubic_to(x + w, y + h - r + k, x + w - r + k, y + h, x + w - r, y + h);
pb.line_to(x + r, y + h);
pb.cubic_to(x + r - k, y + h, x, y + h - r + k, x, y + h - r);
pb.line_to(x, y + r);
pb.cubic_to(x, y + r - k, x + r - k, y, x + r, y);
pb.close();
pb.finish()
}
fn oval_path_px(cx: f32, cy: f32, r: f32) -> Option<tiny_skia::Path> {
let rect = Rect::from_xywh(cx - r, cy - r, r * 2.0, r * 2.0)?;
let mut pb = PathBuilder::new();
pb.push_oval(rect);
pb.finish()
}
fn render_text_block(doc: &crate::Document, base: &RenderOptions, box_w: f32, scale: f32) -> Result<RgbaImage> {
let mut o = base.clone();
o.width = box_w.max(1.0);
o.padding = Insets::all(0.0);
o.scale = scale;
o.header = None;
o.footer = None;
o.theme.background = Color::rgba(0, 0, 0, 0); let layout = crate::layout::layout_document(doc, &o)?;
crate::paint::paint_rgba(&layout, &o)
}
fn ink_box(img: &RgbaImage) -> Option<(u32, u32, u32, u32)> {
let (w, h) = (img.width(), img.height());
let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0u32, 0u32);
let mut any = false;
for y in 0..h {
for x in 0..w {
if img.get_pixel(x, y).0[3] > 12 {
any = true;
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
}
}
any.then_some((x0, y0, x1, y1))
}
fn rgba_to_pixmap(img: &RgbaImage) -> Option<Pixmap> {
let mut p = Pixmap::new(img.width(), img.height())?;
let buf = p.pixels_mut();
for (i, px) in img.pixels().enumerate() {
let [r, g, b, a] = px.0;
let pm = |c: u8| ((c as u16 * a as u16 + 127) / 255) as u8;
buf[i] = PremultipliedColorU8::from_rgba(pm(r), pm(g), pm(b), a)
.unwrap_or_else(|| PremultipliedColorU8::from_rgba(0, 0, 0, 0).unwrap());
}
Some(p)
}
fn skia(c: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
}