1use super::{BrushStroke, invalid};
4use motion_core::{
5 Canvas, Duration, FillRule, NodeModifiers, Paint, Point, Rect, Shape, Time, Transform,
6 ValidationError, Vector2,
7};
8use std::sync::Arc;
9
10mod geometry;
11mod material;
12mod pigment;
13mod presets;
14#[cfg(test)]
15mod tests;
16mod timing;
17
18pub use material::Paper;
19pub use pigment::{PigmentMix, PigmentPainting};
20pub use presets::BrushPreset;
21pub use timing::BrushTiming;
22
23use geometry::{outline, samples};
24use material::material;
25
26#[derive(Clone, Copy, Debug)]
28pub enum NibMode {
29 Fixed(f64),
31 FollowPath(f64),
33 Tilt,
35}
36
37#[derive(Clone, Copy, Debug)]
39pub enum BrushCap {
40 Round,
42 Flat,
44 Tapered(f64),
46}
47
48#[derive(Clone, Copy, Debug)]
50pub struct InputSample {
51 pub position: Point,
53 pub pressure: f64,
55 pub tilt: f64,
57 pub time: Time,
59}
60
61#[derive(Clone)]
62struct PressureFunction(Arc<dyn Fn(f64) -> f64 + Send + Sync>);
63
64impl std::fmt::Debug for PressureFunction {
65 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 formatter
67 .debug_struct("PressureFunction")
68 .finish_non_exhaustive()
69 }
70}
71
72#[derive(Clone, Debug)]
73pub(super) struct Options {
74 pressure: Vec<(f64, f64)>,
75 function: Option<PressureFunction>,
76 nib: NibMode,
77 caps: [BrushCap; 2],
78 samples: usize,
79 input: Vec<InputSample>,
80 input_arcs: Vec<f64>,
81 speed_response: f64,
82 wobble: Option<(f64, f64, u32)>,
83 paint: Option<Paint>,
84 bristles: usize,
85 spread: f64,
86 jitter: f64,
87 seed: u32,
88 tile_size: u32,
89 parallel: bool,
90 paper: Option<Paper>,
91 build_up: f64,
92 edge_darkening: f64,
93 anisotropy: f64,
94 height: f64,
95 paper_transform: Transform,
96}
97
98impl Default for Options {
99 fn default() -> Self {
100 Self {
101 pressure: Vec::new(),
102 function: None,
103 nib: NibMode::Fixed(0.0),
104 caps: [BrushCap::Round; 2],
105 samples: 256,
106 input: Vec::new(),
107 input_arcs: Vec::new(),
108 speed_response: 0.0,
109 wobble: None,
110 paint: None,
111 bristles: 1,
112 spread: 0.0,
113 jitter: 0.0,
114 seed: 0,
115 tile_size: 2048,
116 parallel: false,
117 paper: None,
118 build_up: 0.0,
119 edge_darkening: 0.0,
120 anisotropy: 1.0,
121 height: 0.0,
122 paper_transform: Transform::IDENTITY,
123 }
124 }
125}
126
127impl Options {
128 pub(super) fn reset_pressure(&mut self) {
129 self.pressure.clear();
130 self.function = None;
131 }
132
133 pub(super) fn set_fixed_angle(&mut self, angle: f64) {
134 if matches!(self.nib, NibMode::Fixed(_)) {
135 self.nib = NibMode::Fixed(angle);
136 }
137 }
138}
139
140impl BrushStroke {
141 fn configure(mut self, configure: impl FnOnce(&mut Options)) -> Self {
142 configure(&mut self.advanced);
143 self.reset();
144 self
145 }
146
147 pub fn pressure_curve(self, curve: Vec<(f64, f64)>) -> Self {
149 self.configure(|options| {
150 options.pressure = curve;
151 options.function = None;
152 })
153 }
154
155 pub fn pressure_from(self, function: impl Fn(f64) -> f64 + Send + Sync + 'static) -> Self {
157 self.configure(|options| {
158 options.pressure.clear();
159 options.function = Some(PressureFunction(Arc::new(function)));
160 })
161 }
162
163 pub fn nib_mode(self, mode: NibMode) -> Self {
165 self.configure(|options| options.nib = mode)
166 }
167
168 pub fn caps(self, start: BrushCap, end: BrushCap) -> Self {
170 self.configure(|options| options.caps = [start, end])
171 }
172
173 pub fn sample_limit(self, samples: usize) -> Self {
175 self.configure(|options| options.samples = samples)
176 }
177
178 pub fn speed_response(self, response: f64) -> Self {
180 self.configure(|options| options.speed_response = response)
181 }
182
183 pub fn paint(self, paint: impl Into<Paint>) -> Self {
185 self.configure(|options| options.paint = Some(paint.into()))
186 }
187
188 pub fn bristles(self, count: usize, spread: f64, jitter: f64, seed: u32) -> Self {
190 self.configure(|options| {
191 options.bristles = count;
192 options.spread = spread;
193 options.jitter = jitter;
194 options.seed = seed;
195 })
196 }
197
198 pub fn tiled_material(self, tile_size: u32) -> Self {
200 self.configure(|options| options.tile_size = tile_size)
201 }
202
203 pub fn parallel_material(self, enabled: bool) -> Self {
205 self.configure(|options| options.parallel = enabled)
206 }
207
208 pub fn paper_texture(self, paper: Paper) -> Self {
210 self.configure(|options| options.paper = Some(paper))
211 }
212
213 pub fn paper_transform(self, transform: Transform) -> Self {
216 self.configure(|options| options.paper_transform = transform)
217 }
218
219 pub fn build_up(self, strength: f64) -> Self {
221 self.configure(|options| options.build_up = strength)
222 }
223
224 pub fn edge_darkening(self, strength: f64) -> Self {
226 self.configure(|options| options.edge_darkening = strength)
227 }
228
229 pub fn edge_anisotropy(self, ratio: f64) -> Self {
231 self.configure(|options| options.anisotropy = ratio)
232 }
233
234 pub fn height_map(self, strength: f64) -> Self {
236 self.configure(|options| options.height = strength)
237 }
238
239 pub fn wobble(self, amplitude: f64, frequency: f64, seed: u32) -> Self {
241 self.configure(|options| options.wobble = Some((amplitude, frequency, seed)))
242 }
243
244 pub fn tip(&self) -> Result<(Point, Vector2), ValidationError> {
246 let options = &self.advanced;
247 if self.progress > 0.0 {
248 validate(self)?;
249 let groups = samples(self, self.progress)?;
250 let tips: Vec<_> = groups
251 .iter()
252 .rev()
253 .take(options.bristles)
254 .filter_map(|group| group.last())
255 .collect();
256 if let Some(sample) = tips.first() {
257 let mut point = Point::new(0.0, 0.0);
258 for tip in &tips {
259 point.x += tip.point.x / tips.len() as f64;
260 point.y += tip.point.y / tips.len() as f64;
261 }
262 return Ok((point, sample.tangent));
263 }
264 }
265 self.path.try_point_at(self.progress)
266 }
267
268 pub fn tip_node(
270 &self,
271 node: impl Into<motion_core::Node>,
272 ) -> Result<motion_core::Node, ValidationError> {
273 let (point, tangent) = self.tip()?;
274 Ok(node
275 .into()
276 .pivot(motion_core::Pivot::Point(Point::new(0.0, 0.0)))
277 .rotate(libm::atan2(tangent.y, tangent.x))
278 .translate(point.x, point.y))
279 }
280
281 pub fn erase(self, content: impl Into<motion_core::Node>) -> motion_core::Node {
283 content.into().mask(
284 motion_core::Node::from(self),
285 motion_core::MaskMode::InvertedAlpha,
286 )
287 }
288
289 pub fn boil(self, amplitude: f64, frequency: f64, seed: u32, frame: u64) -> Self {
291 self.wobble(
292 amplitude,
293 frequency,
294 seed ^ motion_core::noise::hash32((frame / 2) as u32),
295 )
296 }
297
298 pub fn wetness(
300 self,
301 age: Duration,
302 radius: f32,
303 drying: Duration,
304 ) -> Result<Self, ValidationError> {
305 if age < Time::ZERO || drying <= Time::ZERO || !radius.is_finite() || radius < 0.0 {
306 return Err(invalid(
307 "wetness",
308 "Invalid age, wash radius or drying duration",
309 ));
310 }
311 let extent = f64::from(radius) * (1.0 - libm::exp(-age.as_seconds() / drying.as_seconds()));
312 let ink = self.ink.unwrap_or_default().bleed(extent as f32);
313 Ok(self.ink(ink))
314 }
315}
316
317fn validate_input(samples: &[InputSample]) -> Result<(), ValidationError> {
318 if !(2..=512).contains(&samples.len())
319 || samples
320 .first()
321 .is_some_and(|sample| sample.time != Time::ZERO)
322 || samples.iter().any(|sample| {
323 !sample.position.x.is_finite()
324 || !sample.position.y.is_finite()
325 || !sample.pressure.is_finite()
326 || !(0.0..=4.0).contains(&sample.pressure)
327 || !sample.tilt.is_finite()
328 || sample.time < Time::ZERO
329 || sample.time.as_seconds() > 86400.0
330 })
331 || samples.windows(2).any(|pair| pair[0].time >= pair[1].time)
332 {
333 return Err(invalid(
334 "input",
335 "Require 2..=512 finite pen samples with strictly increasing time starting at zero",
336 ));
337 }
338 Ok(())
339}
340
341fn validate(stroke: &BrushStroke) -> Result<(), ValidationError> {
342 let options = &stroke.advanced;
343 if !options.height.is_finite() || !(0.0..=16.0).contains(&options.height) {
344 return Err(invalid("height", "Require finite height in 0..=16"));
345 }
346 if !options.paper_transform.is_finite() {
347 return Err(invalid(
348 "paper_transform",
349 "Paper coordinates must be finite",
350 ));
351 }
352 stroke.path.validate()?;
353 if !stroke.width.is_finite()
354 || !(0.0..=512.0).contains(&stroke.width)
355 || !stroke.aspect.is_finite()
356 || !(f64::MIN_POSITIVE..=1.0).contains(&stroke.aspect)
357 || !stroke.angle.is_finite()
358 || !stroke.progress.is_finite()
359 || !(0.0..=1.0).contains(&stroke.progress)
360 || stroke.material_work_limit == 0
361 || stroke
362 .pressure
363 .iter()
364 .any(|value| !value.is_finite() || !(0.0..=4.0).contains(value))
365 || stroke
366 .load
367 .iter()
368 .any(|value| !value.is_finite() || !(0.0..=1.0).contains(value))
369 || !(2..=512).contains(&options.samples)
370 || !(1..=64).contains(&options.bristles)
371 || !(16..=2048).contains(&options.tile_size)
372 || !options.spread.is_finite()
373 || !(0.0..=512.0).contains(&options.spread)
374 || !options.jitter.is_finite()
375 || !(0.0..=1.0).contains(&options.jitter)
376 || !options.speed_response.is_finite()
377 || !(0.0..=1.0).contains(&options.speed_response)
378 || !options.build_up.is_finite()
379 || !(0.0..=1.0).contains(&options.build_up)
380 || !options.edge_darkening.is_finite()
381 || !(0.0..=1.0).contains(&options.edge_darkening)
382 || !options.anisotropy.is_finite()
383 || !(0.1..=10.0).contains(&options.anisotropy)
384 {
385 return Err(invalid(
386 "extended",
387 "Invalid bounded geometry or material configuration",
388 ));
389 }
390 if !options.pressure.is_empty()
391 && (options.pressure.len() > 512
392 || options.pressure.first().unwrap().0 != 0.0
393 || options.pressure.last().unwrap().0 != 1.0
394 || options.pressure.iter().any(|(time, pressure)| {
395 !time.is_finite()
396 || !(0.0..=1.0).contains(time)
397 || !pressure.is_finite()
398 || !(0.0..=4.0).contains(pressure)
399 })
400 || options
401 .pressure
402 .windows(2)
403 .any(|pair| pair[0].0 >= pair[1].0))
404 {
405 return Err(invalid(
406 "pressure_curve",
407 "Require ordered finite controls spanning 0..=1",
408 ));
409 }
410 if !options.input.is_empty() {
411 validate_input(&options.input)?;
412 }
413 match options.nib {
414 NibMode::Fixed(angle) | NibMode::FollowPath(angle) if !angle.is_finite() => {
415 return Err(invalid("nib", "Nib angle must be finite"));
416 }
417 NibMode::Tilt if options.input.is_empty() => {
418 return Err(invalid("nib", "Tilt requires recorded input"));
419 }
420 _ => {}
421 }
422 if options
423 .caps
424 .iter()
425 .any(|cap| matches!(cap,BrushCap::Tapered(length) if !length.is_finite() || *length<=0.0))
426 {
427 return Err(invalid("caps", "Taper length must be positive and finite"));
428 }
429 if options.wobble.is_some_and(|(amount, frequency, _)| {
430 !amount.is_finite()
431 || !(0.0..=512.0).contains(&amount)
432 || !frequency.is_finite()
433 || !(f64::MIN_POSITIVE..=100.0).contains(&frequency)
434 }) {
435 return Err(invalid("wobble", "Invalid wobble amplitude or frequency"));
436 }
437 if options.paper.as_ref().is_some_and(|paper| {
438 !paper.grain_scale.is_finite()
439 || paper.grain_scale <= 0.0
440 || !paper.absorbency.is_finite()
441 || !(0.0..=1.0).contains(&paper.absorbency)
442 }) {
443 return Err(invalid("paper", "Invalid paper scale or absorbency"));
444 }
445 if let Some(paint) = &options.paint {
446 paint.validate()?;
447 }
448 if let Some(ink) = stroke.ink {
449 ink.padding()
450 .map_err(|error| invalid("ink", &error.to_string()))?;
451 }
452 Ok(())
453}
454
455pub(super) fn bounds(stroke: &BrushStroke) -> Result<Rect, ValidationError> {
456 validate(stroke)?;
457 let options = &stroke.advanced;
458 let padding = stroke
459 .ink
460 .map_or(Ok(0.0), |ink| ink.padding().map(f64::from))
461 .map_err(|error| invalid("ink", &error.to_string()))?;
462 Ok(stroke.path.bounding_box().inset(
463 -(stroke.width * 2.0
464 + padding
465 + options.spread
466 + options.wobble.map_or(0.0, |value| value.0)
467 + 1.0),
468 ))
469}
470
471pub(super) fn draw(stroke: &BrushStroke, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
472 let bounds = bounds(stroke)?;
473 let groups = samples(stroke, stroke.progress)?;
474 let path = Arc::new(outline(stroke, &groups)?);
475 if stroke.progress == 0.0 || stroke.width == 0.0 {
476 return Ok(());
477 }
478 let options = &stroke.advanced;
479 let paint = options.paint.clone().unwrap_or_else(|| stroke.color.into());
480 if stroke.ink.is_none()
481 && options.paper.is_none()
482 && options.build_up == 0.0
483 && options.edge_darkening == 0.0
484 && options.height == 0.0
485 {
486 return canvas.fill_path(path, Transform::IDENTITY, paint, FillRule::NonZero);
487 }
488 let full = stroke
489 .full_material
490 .get_or_init(|| material(stroke, bounds))
491 .as_ref()
492 .map_err(Clone::clone)?;
493 for tile in full {
494 canvas.fill_path(
495 path.clone(),
496 Transform::IDENTITY,
497 tile.paint.clone(),
498 FillRule::NonZero,
499 )?;
500 }
501 Ok(())
502}