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