motion_content/vector/ink/brush/
advanced.rs1use super::{BrushStroke, BrushStrokeBuilder, Config, invalid};
4use motion_core::{
5 Canvas, Duration, FillRule, NodeModifiers, Point, Rect, Time, Transform, ValidationError,
6 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 pigment::{PigmentMix, PigmentPainting};
19pub use presets::BrushPreset;
20pub use timing::BrushTiming;
21
22use geometry::{outline, samples};
23use material::material;
24
25#[derive(Clone, Copy, Debug)]
27pub enum NibMode {
28 Fixed(f64),
30 FollowPath(f64),
32 Tilt,
34}
35
36#[derive(Clone, Copy, Debug)]
43pub enum BrushCap {
44 Round,
46 Flat,
48 Tapered(f64),
50}
51
52#[derive(Clone, Copy, Debug)]
54pub struct InputSample {
55 pub position: Point,
57 pub pressure: f64,
59 pub tilt: f64,
61 pub time: Time,
63}
64
65#[derive(Clone)]
66struct PressureFunction(Arc<dyn Fn(f64) -> f64 + Send + Sync>);
67
68impl std::fmt::Debug for PressureFunction {
69 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 formatter
71 .debug_struct("PressureFunction")
72 .finish_non_exhaustive()
73 }
74}
75
76#[derive(Clone, Debug)]
77pub(super) struct Options {
78 pressure: Vec<(f64, f64)>,
79 function: Option<PressureFunction>,
80 nib: NibMode,
81 caps: [BrushCap; 2],
82 samples: usize,
83 input: Vec<InputSample>,
84 input_arcs: Vec<f64>,
85 speed_response: f64,
86 wobble: Option<(f64, f64, u32)>,
87 bristles: usize,
88 spread: f64,
89 jitter: f64,
90 seed: u32,
91 tile_size: u32,
92 parallel: bool,
93}
94
95impl Default for Options {
96 fn default() -> Self {
97 Self {
98 pressure: Vec::new(),
99 function: None,
100 nib: NibMode::Fixed(0.0),
101 caps: [BrushCap::Round; 2],
102 samples: 256,
103 input: Vec::new(),
104 input_arcs: Vec::new(),
105 speed_response: 0.0,
106 wobble: None,
107 bristles: 1,
108 spread: 0.0,
109 jitter: 0.0,
110 seed: 0,
111 tile_size: 2048,
112 parallel: false,
113 }
114 }
115}
116
117impl Options {
118 pub(super) fn reset_pressure(&mut self) {
119 self.pressure.clear();
120 self.function = None;
121 }
122
123 pub(super) fn set_fixed_angle(&mut self, angle: f64) {
124 if matches!(self.nib, NibMode::Fixed(_)) {
125 self.nib = NibMode::Fixed(angle);
126 }
127 }
128}
129
130impl BrushStrokeBuilder {
131 pub(super) fn configure(mut self, configure: impl FnOnce(&mut Options)) -> Self {
132 configure(&mut self.config.advanced);
133 self
134 }
135
136 pub fn pressure_curve(self, curve: Vec<(f64, f64)>) -> Self {
138 self.configure(|options| {
139 options.pressure = curve;
140 options.function = None;
141 })
142 }
143
144 pub fn pressure_from(self, function: impl Fn(f64) -> f64 + Send + Sync + 'static) -> Self {
146 self.configure(|options| {
147 options.pressure.clear();
148 options.function = Some(PressureFunction(Arc::new(function)));
149 })
150 }
151
152 pub fn nib_mode(self, mode: NibMode) -> Self {
154 self.configure(|options| options.nib = mode)
155 }
156
157 pub fn caps(self, start: BrushCap, end: BrushCap) -> Self {
159 self.configure(|options| options.caps = [start, end])
160 }
161
162 pub fn sample_limit(self, samples: usize) -> Self {
164 self.configure(|options| options.samples = samples)
165 }
166
167 pub fn speed_response(self, response: f64) -> Self {
169 self.configure(|options| options.speed_response = response)
170 }
171
172 pub fn bristles(self, count: usize, spread: f64, jitter: f64, seed: u32) -> Self {
174 self.configure(|options| {
175 options.bristles = count;
176 options.spread = spread;
177 options.jitter = jitter;
178 options.seed = seed;
179 })
180 }
181
182 pub fn tiled_material(self, tile_size: u32) -> Self {
184 self.configure(|options| options.tile_size = tile_size)
185 }
186
187 pub fn parallel_material(self, enabled: bool) -> Self {
189 self.configure(|options| options.parallel = enabled)
190 }
191
192 pub fn wobble(self, amplitude: f64, frequency: f64, seed: u32) -> Self {
194 self.configure(|options| options.wobble = Some((amplitude, frequency, seed)))
195 }
196
197 pub fn boil(self, amplitude: f64, frequency: f64, seed: u32, frame: u64) -> Self {
199 self.wobble(
200 amplitude,
201 frequency,
202 seed ^ motion_core::noise::hash32((frame / 2) as u32),
203 )
204 }
205
206 pub fn try_wetness(
208 self,
209 age: Duration,
210 radius: f32,
211 drying: Duration,
212 ) -> Result<Self, ValidationError> {
213 if age < Duration::ZERO || drying <= Duration::ZERO || !radius.is_finite() || radius < 0.0 {
214 return Err(invalid(
215 "wetness",
216 "Invalid age, wash radius or drying duration",
217 ));
218 }
219 let extent = f64::from(radius) * (1.0 - libm::exp(-age.as_seconds() / drying.as_seconds()));
220 let ink = self
221 .config
222 .ink
223 .clone()
224 .unwrap_or_default()
225 .bleed(extent as f32);
226 Ok(self.ink(ink))
227 }
228}
229
230impl BrushStroke {
231 pub fn tip(&self) -> Result<(Point, Vector2), ValidationError> {
233 let options = &self.config.advanced;
234 if self.progress > 0.0 {
235 self.validate()?;
236 let groups = samples(&self.config, self.progress)?;
237 let tips: Vec<_> = groups
238 .iter()
239 .rev()
240 .take(options.bristles)
241 .filter_map(|group| group.last())
242 .collect();
243 if let Some(sample) = tips.first() {
244 let mut point = Point::new(0.0, 0.0);
245 for tip in &tips {
246 point.x += tip.point.x / tips.len() as f64;
247 point.y += tip.point.y / tips.len() as f64;
248 }
249 return Ok((point, sample.tangent));
250 }
251 }
252 self.config.path.try_point_at(self.progress)
253 }
254
255 pub fn tip_node(
257 &self,
258 node: impl Into<motion_core::Node>,
259 ) -> Result<motion_core::Node, ValidationError> {
260 let (point, tangent) = self.tip()?;
261 Ok(node
262 .into()
263 .pivot(motion_core::Pivot::Point(Point::new(0.0, 0.0)))
264 .rotate(libm::atan2(tangent.y, tangent.x))
265 .translate(point.x, point.y))
266 }
267
268 pub fn erase(self, content: impl Into<motion_core::Node>) -> motion_core::Node {
270 content.into().mask(
271 motion_core::Node::from(self),
272 motion_core::MaskMode::InvertedAlpha,
273 )
274 }
275
276 fn validate(&self) -> Result<(), ValidationError> {
277 validate(&self.config)?;
278 if !self.progress.is_finite() || !(0.0..=1.0).contains(&self.progress) {
279 return Err(invalid("progress", "Require reveal progress in 0..=1"));
280 }
281 Ok(())
282 }
283}
284
285fn validate_input(samples: &[InputSample]) -> Result<(), ValidationError> {
286 if !(2..=512).contains(&samples.len())
287 || samples
288 .first()
289 .is_some_and(|sample| sample.time != Time::ZERO)
290 || samples.iter().any(|sample| {
291 !sample.position.x.is_finite()
292 || !sample.position.y.is_finite()
293 || !sample.pressure.is_finite()
294 || !(0.0..=4.0).contains(&sample.pressure)
295 || !sample.tilt.is_finite()
296 || sample.time < Time::ZERO
297 || sample.time.as_seconds() > 86400.0
298 })
299 || samples.windows(2).any(|pair| pair[0].time >= pair[1].time)
300 {
301 return Err(invalid(
302 "input",
303 "Require 2..=512 finite pen samples with strictly increasing time starting at zero",
304 ));
305 }
306 Ok(())
307}
308
309pub(super) fn validate(stroke: &Config) -> Result<(), ValidationError> {
310 let options = &stroke.advanced;
311 if !stroke.width.is_finite()
312 || !(0.0..=512.0).contains(&stroke.width)
313 || !stroke.aspect.is_finite()
314 || !(f64::MIN_POSITIVE..=1.0).contains(&stroke.aspect)
315 || !stroke.angle.is_finite()
316 || stroke.material_work_limit == 0
317 || stroke
318 .pressure
319 .iter()
320 .any(|value| !value.is_finite() || !(0.0..=4.0).contains(value))
321 || stroke
322 .load
323 .iter()
324 .any(|value| !value.is_finite() || !(0.0..=1.0).contains(value))
325 || !(2..=512).contains(&options.samples)
326 || !(1..=64).contains(&options.bristles)
327 || !(16..=2048).contains(&options.tile_size)
328 || !options.spread.is_finite()
329 || !(0.0..=512.0).contains(&options.spread)
330 || !options.jitter.is_finite()
331 || !(0.0..=1.0).contains(&options.jitter)
332 || !options.speed_response.is_finite()
333 || !(0.0..=1.0).contains(&options.speed_response)
334 {
335 return Err(invalid(
336 "extended",
337 "Invalid bounded geometry or material configuration",
338 ));
339 }
340 if !options.pressure.is_empty()
341 && (options.pressure.len() > 512
342 || options.pressure.first().map(|key| key.0) != Some(0.0)
343 || options.pressure.last().map(|key| key.0) != Some(1.0)
344 || options.pressure.iter().any(|(time, pressure)| {
345 !time.is_finite()
346 || !(0.0..=1.0).contains(time)
347 || !pressure.is_finite()
348 || !(0.0..=4.0).contains(pressure)
349 })
350 || options
351 .pressure
352 .windows(2)
353 .any(|pair| pair[0].0 >= pair[1].0))
354 {
355 return Err(invalid(
356 "pressure_curve",
357 "Require ordered finite controls spanning 0..=1",
358 ));
359 }
360 if !options.input.is_empty() {
361 validate_input(&options.input)?;
362 }
363 match options.nib {
364 NibMode::Fixed(angle) | NibMode::FollowPath(angle) if !angle.is_finite() => {
365 return Err(invalid("nib", "Nib angle must be finite"));
366 }
367 NibMode::Tilt if options.input.is_empty() => {
368 return Err(invalid("nib", "Tilt requires recorded input"));
369 }
370 _ => {}
371 }
372 if options
373 .caps
374 .iter()
375 .any(|cap| matches!(cap,BrushCap::Tapered(length) if !length.is_finite() || *length<=0.0))
376 {
377 return Err(invalid("caps", "Taper length must be positive and finite"));
378 }
379 if options.wobble.is_some_and(|(amount, frequency, _)| {
380 !amount.is_finite()
381 || !(0.0..=512.0).contains(&amount)
382 || !frequency.is_finite()
383 || !(f64::MIN_POSITIVE..=100.0).contains(&frequency)
384 }) {
385 return Err(invalid("wobble", "Invalid wobble amplitude or frequency"));
386 }
387 stroke.paint.validate()?;
388 if let Some(ink) = &stroke.ink {
389 ink.padding()
390 .map_err(|error| invalid("ink", &error.to_string()))?;
391 }
392 Ok(())
393}
394
395pub(super) fn bounds(stroke: &BrushStroke) -> Result<Rect, ValidationError> {
396 stroke.validate()?;
397 let config = &stroke.config;
398 let options = &config.advanced;
399 let padding = config
400 .ink
401 .as_ref()
402 .map_or(Ok(0.0), |ink| ink.padding().map(f64::from))
403 .map_err(|error| invalid("ink", &error.to_string()))?;
404 Ok(config.path.bounding_box().inset(
405 -(config.width * 2.0
406 + padding
407 + options.spread
408 + options.wobble.map_or(0.0, |value| value.0)
409 + 1.0),
410 ))
411}
412
413pub(super) fn draw(stroke: &BrushStroke, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
414 let bounds = bounds(stroke)?;
415 let config = &stroke.config;
416 let groups = samples(config, stroke.progress)?;
417 let path = Arc::new(outline(config, &groups)?);
418 if stroke.progress == 0.0 || config.width == 0.0 {
419 return Ok(());
420 }
421 if config.ink.is_none() {
422 return canvas.fill_path(
423 path,
424 Transform::IDENTITY,
425 config.paint.clone(),
426 FillRule::NonZero,
427 );
428 }
429 let full = stroke
430 .full_material
431 .get_or_init(|| material(config, bounds))
432 .as_ref()
433 .map_err(Clone::clone)?;
434 for tile in full {
435 canvas.fill_path(
436 path.clone(),
437 Transform::IDENTITY,
438 tile.paint.clone(),
439 FillRule::NonZero,
440 )?;
441 }
442 Ok(())
443}