Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draw a fuller arrow #143

Merged
merged 2 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,38 @@ pub struct Vec2D {
pub y: f32,
}

#[derive(Default, Debug, Copy, Clone, PartialEq)]
pub struct Angle {
pub radians: f32,
}
impl Angle {
pub fn from_radians(radians: f32) -> Self {
Self { radians }
}

pub fn from_degrees(degrees: f32) -> Self {
Self {
radians: degrees * PI / 180.0,
}
}

pub fn cos(&self) -> f32 {
self.radians.cos()
}

pub fn sin(&self) -> f32 {
self.radians.sin()
}
}

impl Mul<f32> for Angle {
type Output = Angle;

fn mul(self, rhs: f32) -> Self::Output {
Angle::from_radians(self.radians * rhs)
}
}

impl Vec2D {
pub fn zero() -> Self {
Self { x: 0.0, y: 0.0 }
Expand All @@ -27,6 +59,24 @@ impl Vec2D {
self.x * self.x + self.y * self.y
}

/**
* Get the angle of the vector.
* Angle of 0 is the positive x-axis.
* Angle of PI/2 is the positive y-axis.
*/
pub fn angle(&self) -> Angle {
Angle::from_radians(self.y.atan2(self.x))
}

/**
* Create a vector from an angle.
* Angle of 0 is the positive x-axis.
* Angle of PI/2 is the positive y-axis.
*/
pub fn from_angle(angle: Angle) -> Vec2D {
Vec2D::new(angle.cos(), angle.sin())
}

pub fn snapped_vector_15deg(&self) -> Vec2D {
let current_angle = (self.y / self.x).atan();
let current_norm2 = self.norm2();
Expand Down
18 changes: 18 additions & 0 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,24 @@ impl Size {
}
}

pub fn to_arrow_tail_width(self) -> f32 {
let size_factor = APP_CONFIG.read().annotation_size_factor();
match self {
Size::Small => 3.0 * size_factor,
Size::Medium => 10.0 * size_factor,
Size::Large => 25.0 * size_factor,
}
}

pub fn to_arrow_head_length(self) -> f32 {
let size_factor = APP_CONFIG.read().annotation_size_factor();
match self {
Size::Small => 15.0 * size_factor,
Size::Medium => 30.0 * size_factor,
Size::Large => 60.0 * size_factor,
}
}

pub fn to_blur_factor(self) -> f32 {
let size_factor = APP_CONFIG.read().annotation_size_factor();
match self {
Expand Down
120 changes: 85 additions & 35 deletions src/tools/arrow.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use std::f32::consts::PI;

use anyhow::Result;
use femtovg::{FontId, Path};
use relm4::gtk::gdk::{Key, ModifierType};

use crate::{
math::Vec2D,
math::{Angle, Vec2D},
sketch_board::{MouseEventMsg, MouseEventType},
style::Style,
};
Expand Down Expand Up @@ -101,30 +99,6 @@ impl Tool for ArrowTool {
}
}

impl Arrow {
fn get_arrow_head_points(&self) -> (Vec2D, Vec2D) {
let end = match self.end {
Some(e) => e,
None => return (Vec2D::zero(), Vec2D::zero()), // exit if no end
};

// borrowed from: https://math.stackexchange.com/questions/1314006/drawing-an-arrow
let delta = self.start - end;
let l1 = delta.norm();
const L2: f32 = 30.0;
const PHI: f32 = PI / 6.0;
let (sin_phi, cos_phi) = PHI.sin_cos();

let x3 = end.x + L2 / l1 * (delta.x * cos_phi + delta.y * sin_phi);
let y3 = end.y + L2 / l1 * (delta.y * cos_phi - delta.x * sin_phi);

let x4 = end.x + L2 / l1 * (delta.x * cos_phi - delta.y * sin_phi);
let y4 = end.y + L2 / l1 * (delta.y * cos_phi + delta.x * sin_phi);

(Vec2D::new(x3, y3), Vec2D::new(x4, y4))
}
}

impl Drawable for Arrow {
fn draw(
&self,
Expand All @@ -135,19 +109,95 @@ impl Drawable for Arrow {
Some(e) => e,
None => return Ok(()), // exit if no end
};
let (p1, p2) = self.get_arrow_head_points();

// Fat arrow:
// C
// E #
// ######G###
// A ######D##### B
// ##########
// F #
//
//
// Thin arrow:
// C
// \
// A -------- B
// /
//
// A: start
// B: end
// C: head side
// D: midpoint
// E: tail side
// F: tail side
// G: the cross-section of C - D on the tail side.
// Head: the point of the head at the end of the arrow (2, 3, 4).
// Tail: the line from the start to the midpoint (1 - 4).
// Side: the sloped side of the arrow head (3 - 2).
// Midpoint: where the tail ends and the head begins (4).
// Arrow length: the distance from the start to the end (1 - 2).
// Head angle: the angle of the head point at end (2).
// Tail width: the distance from tail side to tail side (5 - 6).

let arrow_offset = end - self.start;
let arrow_length = arrow_offset.norm();
let arrow_direction = arrow_offset * (1.0 / arrow_length);

// We rotate the canvas so that we can draw the arrow on the x-axis.
// start will be at (0,0)
// end will be at (length, 0)
canvas.save();
canvas.translate(self.start.x, self.start.y);
canvas.rotate(arrow_direction.angle().radians);

// The width of the tail (double distance from start to head side)
let tail_width = self.style.size.to_arrow_tail_width();
// The length of the (sloped) side of the arrow head (distance from end to head side).
let head_side_length = self.style.size.to_arrow_head_length();
// The offset of the midpoint is the distance the midpoint moves toward the end of the arrow.
// A offset of 0 will place the midpoint right below the head side.
// A negative value will result in a diamond head.
// A positive value will result in a sharper head.
let midpoint_offset = head_side_length * 0.1;

let head_angle = Angle::from_degrees(60.0); // The angle of the point of the arrow head.

let tail_half_width = tail_width / 2.0;
let head_half_angle = head_angle * 0.5;
let head_left =
Vec2D::new(arrow_length, 0.0) - Vec2D::from_angle(head_half_angle) * head_side_length;
let midpoint_x = head_left.x + midpoint_offset;

if self.style.fill {
// Draw a 'fat' arrow.
let mut path = Path::new();
path.move_to(midpoint_x, tail_half_width); // G
path.line_to(head_left.x, -head_left.y); // C
path.line_to(arrow_length, 0.0); // B
path.line_to(head_left.x, head_left.y); // C (mirrored)
path.line_to(midpoint_x, -tail_half_width); // G (mirrored)
if midpoint_x > 0.0 {
// If the midpoint is placed _before_ the start, there is only a head and no tail.
// We can skip the beginning of the tail.
path.line_to(0.0, -tail_half_width); // F
path.line_to(0.0, tail_half_width); // E
}
path.close();

let mut path = Path::new();
path.move_to(self.start.x, self.start.y);
path.line_to(end.x, end.y);
canvas.fill_path(&path, &self.style.into());
} else {
// Draw a 'thin' arrow head.
let mut path = Path::new();
path.move_to(head_left.x, -head_left.y); // C
path.line_to(arrow_length, 0.0); // B
path.line_to(head_left.x, head_left.y); // C (mirrored)

path.move_to(p1.x, p1.y);
path.line_to(end.x, end.y);
path.line_to(p2.x, p2.y);
path.move_to(0.0, 0.0); // A
path.line_to(arrow_length, 0.0); // B

canvas.stroke_path(&path, &self.style.into());
canvas.stroke_path(&path, &self.style.into());
}

canvas.restore();
Ok(())
Expand Down
Loading