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
	}
}
 | 
