motion_content/vector/ink/brush/advanced/
timing.rs1use super::super::{BrushStroke, BrushStrokeBuilder, invalid};
4use super::geometry::subpaths;
5use super::{InputSample, validate_input};
6use motion_core::{Duration, Paint, Path2D, Time, ValidationError};
7
8#[derive(Clone, Debug)]
10pub struct BrushTiming {
11 pub duration: Duration,
13 pub curve: Vec<(f64, f64)>,
15 pub lift_pause: Duration,
17}
18
19impl BrushTiming {
20 pub fn new(duration: Duration) -> Self {
22 Self {
23 duration,
24 curve: vec![(0.0, 0.0), (1.0, 1.0)],
25 lift_pause: Duration::ZERO,
26 }
27 }
28}
29
30impl BrushStrokeBuilder {
31 pub fn from_samples(
33 samples: &[InputSample],
34 width: f64,
35 paint: impl Into<Paint>,
36 ) -> Result<Self, ValidationError> {
37 validate_input(samples)?;
38 let mut path = Path2D::new();
39 path.try_move_to(samples[0].position)?;
40 let mut cumulative = vec![0.0];
41 for index in 0..samples.len() - 1 {
42 let previous = samples[index.saturating_sub(1)].position;
43 let first = samples[index].position;
44 let second = samples[index + 1].position;
45 let next = samples[(index + 2).min(samples.len() - 1)].position;
46 if first == second {
47 cumulative.push(cumulative[index]);
48 continue;
49 }
50 let control_first = first + (second - previous) * (1.0 / 6.0);
51 let control_second = second - (next - first) * (1.0 / 6.0);
52 let mut segment = Path2D::new();
53 segment.try_move_to(first)?;
54 segment.try_cubic_to(control_first, control_second, second)?;
55 cumulative.push(cumulative.last().copied().unwrap_or(0.0) + segment.try_length()?);
56 path.try_cubic_to(control_first, control_second, second)?;
57 }
58 let total = cumulative.last().copied().unwrap_or(0.0);
59 if total <= 0.0 {
60 return Err(invalid("input", "Recording must contain nonzero travel"));
61 }
62 let mut curve: Vec<(f64, f64)> = Vec::new();
63 for (length, sample) in cumulative.iter().zip(samples) {
64 let amount = *length / total;
65 if let Some(control) = curve.last_mut().filter(|control| control.0 == amount) {
66 control.1 = sample.pressure;
67 } else {
68 curve.push((amount, sample.pressure));
69 }
70 }
71 Ok(
72 BrushStroke::builder(path, width, paint).configure(|options| {
73 options.input = samples.to_vec();
74 options.input_arcs = cumulative.iter().map(|arc| arc / total).collect();
75 options.pressure = curve;
76 }),
77 )
78 }
79
80 pub fn try_smooth(mut self, strength: f64) -> Result<Self, ValidationError> {
82 if !strength.is_finite() || !(0.0..=1.0).contains(&strength) {
83 return Err(invalid("smooth", "Require strength in 0..=1"));
84 }
85 let paths = subpaths(&self.config.path)?;
86 let mut output = Path2D::new();
87 for path in paths {
88 let points = (0..=64)
89 .map(|index| path.try_point_at(index as f64 / 64.0).map(|value| value.0))
90 .collect::<Result<Vec<_>, _>>()?;
91 for (index, point) in points.iter().enumerate() {
92 let point = if index == 0 || index == 64 {
93 *point
94 } else {
95 *point
96 + (points[index - 1] + (points[index + 1] - points[index - 1]) * 0.5
97 - *point)
98 * (strength * 0.5)
99 };
100 if index == 0 {
101 output.try_move_to(point)?;
102 } else {
103 output.try_line_to(point)?;
104 }
105 }
106 }
107 self.config.path = output;
108 Ok(self)
109 }
110}
111
112impl BrushStroke {
113 pub fn write_with(self, time: Time) -> Result<Self, ValidationError> {
115 let options = &self.config.advanced;
116 if options.input.is_empty() {
117 return Err(invalid("timing", "Recorded timing requires from_samples"));
118 }
119 validate_input(&options.input)?;
120 if time < Time::ZERO {
121 return Err(invalid("timing", "Reveal time must be nonnegative"));
122 }
123 let input = &options.input;
124 let index = input
125 .partition_point(|sample| sample.time <= time)
126 .saturating_sub(1);
127 let amount = if index + 1 >= input.len() {
128 1.0
129 } else {
130 options.input_arcs[index]
131 + (options.input_arcs[index + 1] - options.input_arcs[index])
132 * (time - input[index].time).as_seconds()
133 / (input[index + 1].time - input[index].time).as_seconds()
134 };
135 Ok(self.write(amount.clamp(0.0, 1.0)))
136 }
137
138 pub fn write_timed(self, time: Time, timing: &BrushTiming) -> Result<Self, ValidationError> {
140 let stroke = self;
141 if time < Time::ZERO
142 || timing.duration <= Duration::ZERO
143 || timing.duration.as_seconds() > 86400.0
144 || timing.lift_pause.as_seconds() > 86400.0
145 || !(2..=512).contains(&timing.curve.len())
146 || timing.curve.first() != Some(&(0.0, 0.0))
147 || timing.curve.last() != Some(&(1.0, 1.0))
148 || timing.curve.iter().any(|(time, arc)| {
149 !time.is_finite()
150 || !arc.is_finite()
151 || !(0.0..=1.0).contains(time)
152 || !(0.0..=1.0).contains(arc)
153 })
154 || timing
155 .curve
156 .windows(2)
157 .any(|pair| pair[0].0 >= pair[1].0 || pair[0].1 > pair[1].1)
158 {
159 return Err(invalid(
160 "timing",
161 "Invalid duration, pause or monotone curve",
162 ));
163 }
164 let paths = subpaths(&stroke.config.path)?;
165 let lengths = paths
166 .iter()
167 .map(Path2D::try_length)
168 .collect::<Result<Vec<_>, _>>()?;
169 let total: f64 = lengths.iter().sum();
170 if total <= 0.0 {
171 return Err(invalid("timing", "Writing requires nonzero travel"));
172 }
173 let mut elapsed = 0.0;
174 let mut arc = 0.0;
175 let seconds = time.as_seconds();
176 for length in lengths {
177 let duration = timing.duration.as_seconds() * length / total;
178 if seconds <= elapsed {
179 return Ok(stroke.write(arc / total));
180 }
181 if seconds < elapsed + duration {
182 let amount = (seconds - elapsed) / duration;
183 let index = timing
184 .curve
185 .partition_point(|control| control.0 <= amount)
186 .saturating_sub(1)
187 .min(timing.curve.len() - 2);
188 let first = timing.curve[index];
189 let second = timing.curve[index + 1];
190 let progress =
191 first.1 + (second.1 - first.1) * (amount - first.0) / (second.0 - first.0);
192 return Ok(stroke.write((arc + length * progress) / total));
193 }
194 arc += length;
195 elapsed += duration + timing.lift_pause.as_seconds();
196 }
197 Ok(stroke.write(1.0))
198 }
199}