use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Rect, Stroke, Transform};
use crate::{color, drawable::Drawable, text_render::TextRender};
pub struct Axis {
x: f32,
y: f32,
viewport: Rect,
color_index: usize,
drawables: Vec<Box<dyn Drawable>>,
config: Config,
}
impl Axis {
pub(crate) fn new(x: f32, y: f32, size: (f32, f32)) -> Self {
Self {
x,
y,
viewport: Rect::from_xywh(0., 0., size.0, size.1).unwrap(),
drawables: Vec::new(),
color_index: 0,
config: Config::default(),
}
}
pub(crate) fn change_veiwport(&mut self, axis: (f32, f32), size: (f32, f32)) {
self.x = axis.0;
self.y = axis.1;
self.viewport = Rect::from_xywh(0., 0., size.0, size.1).unwrap();
}
fn render_axis(&self, pixmap: &mut Pixmap, ui_ts: &Transform, tr: &TextRender) {
let width = self.viewport.width();
let height = self.viewport.height();
let margin = (width * 0.1).min(50.0);
let (x_min, x_max) = self.config.x_limit.unwrap_or((-1.0, 1.0));
let (y_min, y_max) = self.config.y_limit.unwrap_or((-1.0, 1.0));
let x_range = (x_max - x_min).max(1e-6);
let y_range = (y_max - y_min).max(1e-6);
let plot_w = width - 2.0 * margin;
let plot_h = height - 2.0 * margin;
let (scale_x, scale_y) = match self.config.strategy {
ScaleStrategy::Stretch => (plot_w / x_range, plot_h / y_range),
ScaleStrategy::Fit => {
let s = (plot_w / x_range).min(plot_h / y_range);
(s, s)
}
};
let actual_w = x_range * scale_x;
let actual_h = y_range * scale_y;
let origin_x = if x_min >= 0.0 {
0.0
} else if x_max <= 0.0 {
actual_w
} else {
(-x_min / x_range) * actual_w
};
let origin_y = if y_min >= 0.0 {
0.0
} else if y_max <= 0.0 {
-actual_h
} else {
(-y_min / y_range) * -actual_h
};
let (x_interval, xn) = Self::calculate_tick_interval(x_range);
let (y_interval, yn) = Self::calculate_tick_interval(y_range);
self.draw_grid(pixmap, ui_ts, x_interval, y_interval, actual_w, actual_h);
self.draw_ticks(
pixmap,
ui_ts,
tr,
(x_interval, xn),
(y_interval, yn),
actual_w,
actual_h,
);
if x_min <= 0.0 && x_max >= 0.0 {
self.draw_axis_y(pixmap, ui_ts, actual_h, origin_x);
}
if y_min <= 0.0 && y_max >= 0.0 {
self.draw_axis_x(pixmap, ui_ts, actual_w, origin_y);
}
}
fn draw_grid(&self, pixmap: &mut Pixmap, ts: &Transform, x_int: f32, y_int: f32, w: f32, h: f32) {
let mut pb = PathBuilder::new();
let (x_min, x_max) = self.config.x_limit.unwrap_or((0.0, 10.0));
let (y_min, y_max) = self.config.y_limit.unwrap_or((0.0, 10.0));
let x_range = (x_max - x_min).max(1e-6);
let y_range = (y_max - y_min).max(1e-6);
let mut x_val = (x_min / x_int).floor() * x_int;
while x_val < x_max - (x_int * 0.01) {
if x_val >= x_min {
let px = (x_val - x_min) / x_range * w;
pb.move_to(px, 0.0);
pb.line_to(px, -h);
}
x_val += x_int;
}
let mut y_val = (y_min / y_int).floor() * y_int;
while y_val < y_max - (y_int * 0.01) {
if y_val >= y_min {
let py = -((y_val - y_min) / y_range * h);
pb.move_to(0.0, py);
pb.line_to(w, py);
}
y_val += y_int;
}
self.stroke_path(pixmap, pb, ts, 1., color::get_gray());
}
fn draw_ticks(
&self, pixmap: &mut Pixmap, ui_ts: &Transform, tr: &TextRender, x_info: (f32, usize),
y_info: (f32, usize), w: f32, h: f32,
) {
let (x_min, x_max) = self.config.x_limit.unwrap_or((0.0, 1.0));
let (y_min, y_max) = self.config.y_limit.unwrap_or((0.0, 1.0));
let x_range = (x_max - x_min).max(1e-6);
let y_range = (y_max - y_min).max(1e-6);
let (x_int, x_count) = x_info;
let (y_int, y_count) = y_info;
let font_size = 12.0; let [r, g, b, a] = color::get_fg();
let text_color = Color::from_rgba8(r, g, b, a);
let x_start_val = (x_min / x_int).floor() * x_int;
for i in 0..x_count {
let x_val = x_start_val + (i as f32 * x_int);
if x_val >= x_min && x_val <= x_max + 1e-6 {
let px = (x_val - x_min) / x_range * w;
let label = format!("{:.1}", x_val);
let text_w = label.len() as f32 * (font_size * 0.5);
tr.draw(
pixmap,
&label,
ui_ts.tx + px - (text_w / 2.0), ui_ts.ty + 10.0, font_size, text_color,
);
}
}
let y_start_val = (y_min / y_int).floor() * y_int;
for i in 0..y_count {
let y_val = y_start_val + (i as f32 * y_int);
if y_val >= y_min && y_val <= y_max + 1e-6 {
let py = -((y_val - y_min) / y_range * h); let label = format!("{:.1}", y_val);
let text_w = label.len() as f32 * (font_size * 0.5);
tr.draw(
pixmap,
&label,
ui_ts.tx - text_w - 8.0, ui_ts.ty + py - (font_size / 2.0), font_size, text_color,
);
}
}
}
fn draw_axis_x(&self, pixmap: &mut Pixmap, ts: &Transform, w: f32, origin_y: f32) {
let mut pb = PathBuilder::new();
let arrow_len = 10.0;
pb.move_to(0.0, origin_y);
pb.line_to(w, origin_y);
pb.move_to(w, origin_y);
pb.line_to(w - arrow_len, origin_y - arrow_len * 0.5);
pb.move_to(w, origin_y);
pb.line_to(w - arrow_len, origin_y + arrow_len * 0.5);
self.stroke_path(pixmap, pb, ts, 1.5, color::get_fg());
}
fn draw_axis_y(&self, pixmap: &mut Pixmap, ts: &Transform, h: f32, origin_x: f32) {
let mut pb = PathBuilder::new();
let arrow_len = 10.0;
pb.move_to(origin_x, 0.0);
pb.line_to(origin_x, -h);
pb.move_to(origin_x, -h);
pb.line_to(origin_x - arrow_len * 0.5, -h + arrow_len);
pb.move_to(origin_x, -h);
pb.line_to(origin_x + arrow_len * 0.5, -h + arrow_len);
self.stroke_path(pixmap, pb, ts, 1.5, color::get_fg());
}
fn stroke_path(
&self, pixmap: &mut Pixmap, pb: PathBuilder, ts: &Transform, width: f32, color: [u8; 4],
) {
if let Some(path) = pb.finish() {
let mut paint = Paint::default();
paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
paint.anti_alias = true;
pixmap.stroke_path(
&path,
&paint,
&Stroke {
width,
..Default::default()
},
*ts,
None,
);
}
}
fn calculate_tick_interval(range: f32) -> (f32, usize) {
if range <= 0.0 {
return (1.0, 1);
}
let raw_interval = range / 8.0;
let magnitude = 10_f32.powf(raw_interval.log10().floor());
let normalized = raw_interval / magnitude;
let nice_interval = if normalized < 1.5 {
1.0 * magnitude
} else if normalized < 3.0 {
2.0 * magnitude
} else if normalized < 7.0 {
5.0 * magnitude
} else {
10.0 * magnitude
};
let num_ticks = (range / nice_interval).ceil() as usize + 1;
(nice_interval, num_ticks)
}
fn auto_limit(&mut self) {
if self.config.x_limit.is_some() && self.config.y_limit.is_some() {
return;
}
println!("into autolimit");
let mut total_bounds = None;
for drawable in &self.drawables {
if let Some(b) = drawable.bound() {
match total_bounds {
None => total_bounds = Some(b),
Some(ref mut tb) => {
tb.x_min = tb.x_min.min(b.x_min);
tb.x_max = tb.x_max.max(b.x_max);
tb.y_min = tb.y_min.min(b.y_min);
tb.y_max = tb.y_max.max(b.y_max);
}
}
}
}
if let Some(b) = total_bounds {
let padding = 0.1;
let dx = if b.x_max <= b.x_min {
1.0
} else {
(b.x_max - b.x_min) * padding
};
let dy = if b.y_max <= b.y_min {
1.0
} else {
(b.y_max - b.y_min) * padding
};
if self.config.x_limit.is_none() {
self.set_x_limit(Some((b.x_min, b.x_max + dx)));
}
if self.config.y_limit.is_none() {
self.set_y_limit(Some((b.y_min, b.y_max + dy)));
}
}
}
pub(crate) fn render(&mut self, pixmap: &mut Pixmap, tr: &TextRender) {
self.auto_limit();
let width = self.viewport.width();
let height = self.viewport.height();
let margin: f32 = (width * 0.1).min(50.);
let (x_min, x_max) = self.config.x_limit.unwrap_or((0.0, 1.0));
let (y_min, y_max) = self.config.y_limit.unwrap_or((0.0, 1.0));
let x_range = x_max - x_min;
let y_range = y_max - y_min;
if x_range == 0.0 || y_range == 0.0 {
return;
}
let plot_w = width - 2. * margin;
let plot_h = height - 2. * margin;
let (scale_x, scale_y, offset_x, offset_y) = match self.config.strategy {
ScaleStrategy::Stretch => {
(plot_w / x_range, plot_h / y_range, 0.0, 0.0)
}
ScaleStrategy::Fit => {
let s = (plot_w / x_range).min(plot_h / y_range);
let ox = (plot_w - (x_range * s)) / 2.0;
let oy = (plot_h - (y_range * s)) / 2.0;
(s, s, ox, oy)
}
};
let base_ts = Transform::from_translate(self.x, self.y);
let ui_ts = base_ts.pre_translate(margin + offset_x, height - (margin + offset_y));
let data_ts = ui_ts
.pre_scale(scale_x, -scale_y)
.pre_translate(-x_min, -y_min);
self.render_axis(pixmap, &ui_ts, tr);
for drawable in &mut self.drawables {
if drawable.get_color() == [0, 0, 0, 0] {
let color = color::get_color(self.color_index & 7);
self.color_index += 1;
drawable.set_color(color);
}
drawable.draw(pixmap, &data_ts);
}
}
pub fn add(&mut self, drawable: Box<dyn Drawable>) {
self.drawables.push(drawable);
}
pub fn set_x_limit(&mut self, limit: Option<(f32, f32)>) {
self.config.x_limit = limit;
}
pub fn set_y_limit(&mut self, limit: Option<(f32, f32)>) {
self.config.y_limit = limit;
}
pub fn set_strategy(&mut self, strategy: ScaleStrategy) {
self.config.strategy = strategy;
}
}
#[derive(Default)]
pub struct Config {
x_limit: Option<(f32, f32)>,
y_limit: Option<(f32, f32)>,
strategy: ScaleStrategy,
}
#[derive(Default)]
pub enum ScaleStrategy {
#[default]
Fit,
Stretch,
}