File size: 3,540 Bytes
2409829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use dyn_any::DynAny;
use glam::DVec2;
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::math::bbox::AxisAlignedBbox;
use std::hash::{Hash, Hasher};

/// The style of a brush.
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushStyle {
	pub color: Color,
	pub diameter: f64,
	pub hardness: f64,
	pub flow: f64,
	pub spacing: f64, // Spacing as a fraction of the diameter.
	pub blend_mode: BlendMode,
}

impl Default for BrushStyle {
	fn default() -> Self {
		Self {
			color: Color::BLACK,
			diameter: 40.,
			hardness: 50.,
			flow: 100.,
			spacing: 50., // Percentage of diameter.
			blend_mode: BlendMode::Normal,
		}
	}
}

impl Hash for BrushStyle {
	fn hash<H: Hasher>(&self, state: &mut H) {
		self.color.hash(state);
		self.diameter.to_bits().hash(state);
		self.hardness.to_bits().hash(state);
		self.flow.to_bits().hash(state);
		self.spacing.to_bits().hash(state);
		self.blend_mode.hash(state);
	}
}

impl Eq for BrushStyle {}

impl PartialEq for BrushStyle {
	fn eq(&self, other: &Self) -> bool {
		self.color == other.color
			&& self.diameter.to_bits() == other.diameter.to_bits()
			&& self.hardness.to_bits() == other.hardness.to_bits()
			&& self.flow.to_bits() == other.flow.to_bits()
			&& self.spacing.to_bits() == other.spacing.to_bits()
			&& self.blend_mode == other.blend_mode
	}
}

/// A single sample of brush parameters across the brush stroke.
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushInputSample {
	// The position of the sample in layer space, in pixels.
	// The origin of layer space is not specified.
	pub position: DVec2,
	// Future work: pressure, stylus angle, etc.
}

impl Hash for BrushInputSample {
	fn hash<H: Hasher>(&self, state: &mut H) {
		self.position.x.to_bits().hash(state);
		self.position.y.to_bits().hash(state);
	}
}

/// The parameters for a single stroke brush.
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushStroke {
	pub style: BrushStyle,
	pub trace: Vec<BrushInputSample>,
}

impl BrushStroke {
	pub fn bounding_box(&self) -> AxisAlignedBbox {
		let radius = self.style.diameter / 2.;
		self.compute_blit_points()
			.iter()
			.map(|pos| AxisAlignedBbox {
				start: *pos + DVec2::new(-radius, -radius),
				end: *pos + DVec2::new(radius, radius),
			})
			.reduce(|a, b| a.union(&b))
			.unwrap_or(AxisAlignedBbox::ZERO)
	}

	pub fn compute_blit_points(&self) -> Vec<DVec2> {
		// We always travel in a straight line towards the next user input,
		// placing a blit point every time we travelled our spacing distance.
		let spacing_dist = self.style.spacing / 100. * self.style.diameter;

		let Some(first_sample) = self.trace.first() else {
			return Vec::new();
		};

		let mut cur_pos = first_sample.position;
		let mut result = vec![cur_pos];
		let mut dist_until_next_blit = spacing_dist;
		for sample in &self.trace[1..] {
			// Travel to the next sample.
			let delta = sample.position - cur_pos;
			let mut dist_left = delta.length();
			let unit_step = delta / dist_left;

			while dist_left >= dist_until_next_blit {
				// Take a step to the next blit point.
				cur_pos += dist_until_next_blit * unit_step;
				dist_left -= dist_until_next_blit;

				// Blit.
				result.push(cur_pos);
				dist_until_next_blit = spacing_dist;
			}

			// Take the partial step to land at the sample.
			dist_until_next_blit -= dist_left;
			cur_pos = sample.position;
		}

		result
	}
}