use std::time::Instant;
use femtovg::{FontId, LineCap, LineJoin, Paint, Path};
use crate::{
configuration::APP_CONFIG,
math::Vec2D,
sketch_board::{MouseButton, MouseEventMsg, MouseEventType, SketchBoardInput},
style::Style,
};
use super::{Drawable, DrawableClone, Tool, ToolUpdateResult, Tools};
use relm4::Sender;
#[derive(Default)]
pub struct BrushTool {
drawable: Option<BrushDrawable>,
style: Style,
input_enabled: bool,
sender: Option<Sender<SketchBoardInput>>,
}
#[derive(Debug, Clone)]
pub struct BrushDrawable {
start_point: Option<Vec2D>,
points: Vec<Vec2D>,
smoother: Smoother,
style: Style,
}
impl BrushDrawable {
fn add_point(&mut self, point: Vec2D) {
self.points.push(self.smoother.update(point));
}
}
impl Drawable for BrushDrawable {
fn draw(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
_font: FontId,
_bounds: (Vec2D, Vec2D),
) -> anyhow::Result<()> {
if self.points.is_empty() {
return Ok(());
}
let Some(start_point) = self.start_point else {
return Ok(());
};
canvas.save();
let mut path = Path::new();
path.move_to(start_point.x, start_point.y);
for p in self.points.iter().skip(1) {
path.line_to(start_point.x + p.x, start_point.y + p.y);
}
canvas.stroke_path(
&path,
&Paint::from(self.style)
.with_line_cap(LineCap::Round)
.with_line_join(LineJoin::Round),
);
canvas.restore();
Ok(())
}
}
impl Tool for BrushTool {
fn input_enabled(&self) -> bool {
self.input_enabled
}
fn set_input_enabled(&mut self, value: bool) {
self.input_enabled = value;
}
fn get_tool_type(&self) -> super::Tools {
Tools::Brush
}
fn handle_mouse_event(&mut self, event: MouseEventMsg) -> ToolUpdateResult {
match event.type_ {
MouseEventType::BeginDrag => {
if event.button == MouseButton::Middle {
return ToolUpdateResult::Unmodified;
}
let Some(brush) = &mut self.drawable else {
return ToolUpdateResult::Unmodified;
};
brush.start_point = Some(event.pos);
ToolUpdateResult::Redraw
}
MouseEventType::EndDrag => {
if event.button == MouseButton::Middle {
return ToolUpdateResult::Unmodified;
}
let Some(brush) = &mut self.drawable else {
return ToolUpdateResult::Unmodified;
};
brush.add_point(event.pos);
let result = brush.clone_box();
self.drawable = None;
ToolUpdateResult::Commit(result)
}
MouseEventType::UpdateDrag => {
if event.button == MouseButton::Middle {
return ToolUpdateResult::Unmodified;
}
let Some(brush) = &mut self.drawable else {
return ToolUpdateResult::Unmodified;
};
brush.add_point(event.pos);
ToolUpdateResult::Redraw
}
MouseEventType::Click => {
if event.button != MouseButton::Primary {
return ToolUpdateResult::Unmodified;
}
self.drawable = Some(BrushDrawable {
start_point: None,
smoother: Smoother::new(APP_CONFIG.read().brush_smooth_history_size()),
points: vec![event.pos],
style: self.style,
});
ToolUpdateResult::Unmodified
}
_ => ToolUpdateResult::Unmodified,
}
}
fn get_drawable(&self) -> Option<&dyn Drawable> {
match &self.drawable {
Some(d) => Some(d),
None => None,
}
}
fn handle_style_event(&mut self, style: Style) -> ToolUpdateResult {
self.style = style;
ToolUpdateResult::Unmodified
}
fn active(&self) -> bool {
self.drawable.is_some()
}
fn set_sender(&mut self, sender: Sender<SketchBoardInput>) {
self.sender = Some(sender);
}
}
#[derive(Debug, Clone)]
pub struct Smoother {
history: Vec<Vec2D>, smoothed_point: Option<Vec2D>,
max_history: usize,
last_update: Option<Instant>,
}
impl Smoother {
pub fn new(max_history: usize) -> Self {
Self {
history: Vec::with_capacity(max_history + 1),
smoothed_point: None,
max_history,
last_update: None,
}
}
pub fn update(&mut self, raw: Vec2D) -> Vec2D {
if self.max_history == 0 {
return raw;
}
if self.history.len() >= self.max_history {
self.history.remove(0);
}
self.history.push(raw);
let n = self.history.len() as f32;
let sum = self
.history
.iter()
.fold(Vec2D { x: 0.0, y: 0.0 }, |acc, p| Vec2D {
x: acc.x + p.x,
y: acc.y + p.y,
});
let averaged_raw = Vec2D {
x: sum.x / n,
y: sum.y / n,
};
let dt = if let Some(last_update) = self.last_update {
let now = Instant::now();
let dt = now.duration_since(last_update).as_secs_f32();
self.last_update = Some(now);
dt
} else {
self.last_update = Some(Instant::now());
0.0
};
let last = *self.history.last().unwrap_or(&raw);
let first = self.history.first().unwrap_or(&raw);
let distance = last.distance_to(first);
let total_dt = dt * self.history.len() as f32;
let speed = distance / total_dt.clamp(0.001, 1.0);
let alpha = Self::compute_alpha(speed);
let smoothed = if let Some(prev) = self.smoothed_point {
Vec2D {
x: alpha * averaged_raw.x + (1.0 - alpha) * prev.x,
y: alpha * averaged_raw.y + (1.0 - alpha) * prev.y,
}
} else {
averaged_raw
};
self.smoothed_point = Some(smoothed);
smoothed
}
fn compute_alpha(speed: f32) -> f32 {
let min_alpha = 0.05;
let max_alpha = 0.5;
let clamped_speed = speed.clamp(0.01, 500.0);
let norm = (clamped_speed / 500.0).sqrt();
min_alpha + (max_alpha - min_alpha) * norm
}
}