use anyhow::Result;
use ht32_panel_hw::lcd::framebuffer::{rgb888_to_rgb565, Framebuffer};
use std::collections::VecDeque;
use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Rect, Stroke, Transform};
use super::text::TextRenderer;
fn brighten_color(color: u32, factor: f32) -> u32 {
let r = ((color >> 16) & 0xFF) as f32;
let g = ((color >> 8) & 0xFF) as f32;
let b = (color & 0xFF) as f32;
let r = ((r * factor).min(255.0)) as u32;
let g = ((g * factor).min(255.0)) as u32;
let b = ((b * factor).min(255.0)) as u32;
(r << 16) | (g << 8) | b
}
pub struct Canvas {
width: u32,
height: u32,
pixmap: Pixmap,
background_color: u32,
text_renderer: TextRenderer,
}
impl Canvas {
pub fn new(width: u32, height: u32) -> Self {
let pixmap = Pixmap::new(width, height).expect("Failed to create pixmap");
Self {
width,
height,
pixmap,
background_color: 0x000000, text_renderer: TextRenderer::new(),
}
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn resize(&mut self, width: u32, height: u32) {
if self.width != width || self.height != height {
self.width = width;
self.height = height;
self.pixmap = Pixmap::new(width, height).expect("Failed to create pixmap");
}
}
pub fn set_background(&mut self, color: u32) {
self.background_color = color;
}
pub fn clear(&mut self) {
let r = ((self.background_color >> 16) & 0xFF) as f32 / 255.0;
let g = ((self.background_color >> 8) & 0xFF) as f32 / 255.0;
let b = (self.background_color & 0xFF) as f32 / 255.0;
self.pixmap.fill(Color::from_rgba(r, g, b, 1.0).unwrap());
}
pub fn fill_rect(&mut self, x: i32, y: i32, width: u32, height: u32, color: u32) {
debug_assert!(
x >= 0 && y >= 0,
"fill_rect: negative coordinates ({}, {})",
x,
y
);
debug_assert!(
x + width as i32 <= self.width as i32,
"fill_rect: x overflow ({} + {} > {})",
x,
width,
self.width
);
debug_assert!(
y + height as i32 <= self.height as i32,
"fill_rect: y overflow ({} + {} > {})",
y,
height,
self.height
);
let r = ((color >> 16) & 0xFF) as f32 / 255.0;
let g = ((color >> 8) & 0xFF) as f32 / 255.0;
let b = (color & 0xFF) as f32 / 255.0;
let mut paint = Paint::default();
paint.set_color(Color::from_rgba(r, g, b, 1.0).unwrap());
if let Some(rect) = Rect::from_xywh(x as f32, y as f32, width as f32, height as f32) {
self.pixmap
.fill_rect(rect, &paint, Transform::identity(), None);
}
}
pub fn fill_circle(&mut self, cx: i32, cy: i32, radius: u32, color: u32) {
debug_assert!(
cx - radius as i32 >= 0,
"fill_circle: left edge off screen ({} - {} < 0)",
cx,
radius
);
debug_assert!(
cy - radius as i32 >= 0,
"fill_circle: top edge off screen ({} - {} < 0)",
cy,
radius
);
debug_assert!(
cx + radius as i32 <= self.width as i32,
"fill_circle: right edge off screen ({} + {} > {})",
cx,
radius,
self.width
);
debug_assert!(
cy + radius as i32 <= self.height as i32,
"fill_circle: bottom edge off screen ({} + {} > {})",
cy,
radius,
self.height
);
let r = ((color >> 16) & 0xFF) as f32 / 255.0;
let g = ((color >> 8) & 0xFF) as f32 / 255.0;
let b = (color & 0xFF) as f32 / 255.0;
let mut paint = Paint::default();
paint.set_color(Color::from_rgba(r, g, b, 1.0).unwrap());
paint.anti_alias = true;
let mut pb = PathBuilder::new();
pb.push_circle(cx as f32, cy as f32, radius as f32);
if let Some(path) = pb.finish() {
self.pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
Transform::identity(),
None,
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, stroke_width: f32, color: u32) {
let r = ((color >> 16) & 0xFF) as f32 / 255.0;
let g = ((color >> 8) & 0xFF) as f32 / 255.0;
let b = (color & 0xFF) as f32 / 255.0;
let mut paint = Paint::default();
paint.set_color(Color::from_rgba(r, g, b, 1.0).unwrap());
paint.anti_alias = true;
let stroke = Stroke {
width: stroke_width,
line_cap: tiny_skia::LineCap::Round,
..Default::default()
};
let mut pb = PathBuilder::new();
pb.move_to(x1 as f32, y1 as f32);
pb.line_to(x2 as f32, y2 as f32);
if let Some(path) = pb.finish() {
self.pixmap
.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_arc(
&mut self,
cx: i32,
cy: i32,
radius: u32,
start_angle: f32,
end_angle: f32,
stroke_width: f32,
color: u32,
) {
let total_radius = radius as i32 + (stroke_width / 2.0).ceil() as i32;
debug_assert!(
cx - total_radius >= 0,
"draw_arc: left edge off screen ({} - {} < 0)",
cx,
total_radius
);
debug_assert!(
cy - total_radius >= 0,
"draw_arc: top edge off screen ({} - {} < 0)",
cy,
total_radius
);
debug_assert!(
cx + total_radius <= self.width as i32,
"draw_arc: right edge off screen ({} + {} > {})",
cx,
total_radius,
self.width
);
debug_assert!(
cy + total_radius <= self.height as i32,
"draw_arc: bottom edge off screen ({} + {} > {})",
cy,
total_radius,
self.height
);
let r = ((color >> 16) & 0xFF) as f32 / 255.0;
let g = ((color >> 8) & 0xFF) as f32 / 255.0;
let b = (color & 0xFF) as f32 / 255.0;
let mut paint = Paint::default();
paint.set_color(Color::from_rgba(r, g, b, 1.0).unwrap());
paint.anti_alias = true;
let stroke = Stroke {
width: stroke_width,
..Default::default()
};
let mut pb = PathBuilder::new();
let segments = 64;
let angle_span = end_angle - start_angle;
let cx_f = cx as f32;
let cy_f = cy as f32;
let radius_f = radius as f32;
for i in 0..=segments {
let t = i as f32 / segments as f32;
let angle = start_angle + t * angle_span;
let px = cx_f + radius_f * angle.cos();
let py = cy_f + radius_f * angle.sin();
if i == 0 {
pb.move_to(px, py);
} else {
pb.line_to(px, py);
}
}
if let Some(path) = pb.finish() {
self.pixmap
.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
}
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, size: f32, color: u32) {
let text_width = self.text_renderer.text_width(text, size);
let text_height = self.text_renderer.line_height(size);
debug_assert!(
x >= 0 && y >= 0,
"draw_text: negative coordinates ({}, {}) for '{}'",
x,
y,
text
);
debug_assert!(
x + text_width <= self.width as i32,
"draw_text: text extends past right edge ({} + {} > {}) for '{}'",
x,
text_width,
self.width,
text
);
debug_assert!(
y + text_height <= self.height as i32,
"draw_text: text extends past bottom edge ({} + {} > {}) for '{}'",
y,
text_height,
self.height,
text
);
self.text_renderer
.draw_text(&mut self.pixmap, x, y, text, size, color);
}
#[allow(clippy::too_many_arguments)]
pub fn draw_text_scaled(
&mut self,
x: i32,
y: i32,
text: &str,
size: f32,
color: u32,
x_scale: f32,
) {
let text_width = self.text_renderer.text_width_scaled(text, size, x_scale);
let text_height = self.text_renderer.line_height(size);
debug_assert!(
x >= 0 && y >= 0,
"draw_text_scaled: negative coordinates ({}, {}) for '{}'",
x,
y,
text
);
debug_assert!(
x + text_width <= self.width as i32,
"draw_text_scaled: text extends past right edge ({} + {} > {}) for '{}'",
x,
text_width,
self.width,
text
);
debug_assert!(
y + text_height <= self.height as i32,
"draw_text_scaled: text extends past bottom edge ({} + {} > {}) for '{}'",
y,
text_height,
self.height,
text
);
self.text_renderer
.draw_text_scaled(&mut self.pixmap, x, y, text, size, color, x_scale);
}
pub fn text_width(&self, text: &str, size: f32) -> i32 {
self.text_renderer.text_width(text, size)
}
pub fn text_width_scaled(&self, text: &str, size: f32, x_scale: f32) -> i32 {
self.text_renderer.text_width_scaled(text, size, x_scale)
}
pub fn line_height(&self, size: f32) -> i32 {
self.text_renderer.line_height(size)
}
#[allow(clippy::too_many_arguments)]
pub fn draw_graph(
&mut self,
x: i32,
y: i32,
width: u32,
height: u32,
data: &VecDeque<f64>,
max_value: f64,
line_color: u32,
bg_color: u32,
) {
debug_assert!(
x >= 0 && y >= 0,
"draw_graph: negative coordinates ({}, {})",
x,
y
);
debug_assert!(
x + width as i32 <= self.width as i32,
"draw_graph: x overflow ({} + {} > {})",
x,
width,
self.width
);
debug_assert!(
y + height as i32 <= self.height as i32,
"draw_graph: y overflow ({} + {} > {})",
y,
height,
self.height
);
let r = ((bg_color >> 16) & 0xFF) as f32 / 255.0;
let g = ((bg_color >> 8) & 0xFF) as f32 / 255.0;
let b = (bg_color & 0xFF) as f32 / 255.0;
let mut paint = Paint::default();
paint.set_color(Color::from_rgba(r, g, b, 1.0).unwrap());
if let Some(rect) = Rect::from_xywh(x as f32, y as f32, width as f32, height as f32) {
self.pixmap
.fill_rect(rect, &paint, Transform::identity(), None);
}
if data.is_empty() || max_value <= 0.0 {
return;
}
let high_color = brighten_color(line_color, 1.4); let max_color = 0xFFFFFF;
let num_points = data.len();
let bar_width = (width as f64 / num_points as f64).max(1.0);
for (i, &value) in data.iter().enumerate() {
let normalized = (value / max_value).min(1.0);
let bar_height = (normalized * height as f64) as u32;
if bar_height > 0 {
let bar_x = x + (i as f64 * bar_width) as i32;
let bar_y = y + (height - bar_height) as i32;
let color = if normalized >= 1.0 {
max_color
} else if normalized >= 0.95 {
high_color
} else {
line_color
};
self.fill_rect(bar_x, bar_y, bar_width.ceil() as u32, bar_height, color);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_dual_graph(
&mut self,
x: i32,
y: i32,
width: u32,
height: u32,
data1: &VecDeque<f64>,
data2: &VecDeque<f64>,
max_value: f64,
color1: u32,
color2: u32,
bg_color: u32,
) {
debug_assert!(
x >= 0 && y >= 0,
"draw_dual_graph: negative coordinates ({}, {})",
x,
y
);
debug_assert!(
x + width as i32 <= self.width as i32,
"draw_dual_graph: x overflow ({} + {} > {})",
x,
width,
self.width
);
debug_assert!(
y + height as i32 <= self.height as i32,
"draw_dual_graph: y overflow ({} + {} > {})",
y,
height,
self.height
);
let r = ((bg_color >> 16) & 0xFF) as f32 / 255.0;
let g = ((bg_color >> 8) & 0xFF) as f32 / 255.0;
let b = (bg_color & 0xFF) as f32 / 255.0;
let mut paint = Paint::default();
paint.set_color(Color::from_rgba(r, g, b, 1.0).unwrap());
if let Some(rect) = Rect::from_xywh(x as f32, y as f32, width as f32, height as f32) {
self.pixmap
.fill_rect(rect, &paint, Transform::identity(), None);
}
if (data1.is_empty() && data2.is_empty()) || max_value <= 0.0 {
return;
}
let num_points = data1.len().max(data2.len());
if num_points == 0 {
return;
}
let bar_width = (width as f64 / num_points as f64).max(1.0);
let high_color1 = brighten_color(color1, 1.4);
let high_color2 = brighten_color(color2, 1.4);
let max_color = 0xFFFFFF;
for (i, &value) in data1.iter().enumerate() {
let normalized = (value / max_value).min(1.0);
let bar_height = (normalized * height as f64) as u32;
if bar_height > 0 {
let bar_x = x + (i as f64 * bar_width) as i32;
let bar_y = y + (height - bar_height) as i32;
let color = if normalized >= 1.0 {
max_color
} else if normalized >= 0.95 {
high_color1
} else {
color1
};
self.fill_rect(bar_x, bar_y, bar_width.ceil() as u32, bar_height, color);
}
}
for (i, &value) in data2.iter().enumerate() {
let normalized = (value / max_value).min(1.0);
let bar_height = (normalized * height as f64) as u32;
if bar_height > 0 {
let bar_x = x + (i as f64 * bar_width) as i32;
let bar_y = y + (height - bar_height) as i32;
let color = if normalized >= 1.0 {
max_color
} else if normalized >= 0.95 {
high_color2
} else {
color2
};
let overlay_width = (bar_width * 0.6).ceil() as u32;
self.fill_rect(bar_x, bar_y, overlay_width, bar_height, color);
}
}
}
pub fn render_to_framebuffer(&self, fb: &mut Framebuffer) -> Result<()> {
let pixels = self.pixmap.pixels();
let data = fb.data_mut();
for (i, pixel) in pixels.iter().enumerate() {
if i < data.len() {
data[i] = rgb888_to_rgb565(pixel.red(), pixel.green(), pixel.blue());
}
}
Ok(())
}
pub fn pixels(&self) -> &[u8] {
self.pixmap.data()
}
pub fn pixmap_pixels(&self) -> &[tiny_skia::PremultipliedColorU8] {
self.pixmap.pixels()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canvas_creation() {
let canvas = Canvas::new(320, 170);
assert_eq!(canvas.dimensions(), (320, 170));
}
}