1use alpha2_eval::{SampledAnimator, SampledSelector};
10use alpha2_model::{RangeShape, RangeUnits, RunState, TextRun, TextUnit};
11use alpha2_text::{ClusterState, ColorMix, TextLayout};
12use alpha2_time::MediaTime;
13
14#[must_use]
17pub fn selection(
18 selector: &SampledSelector,
19 layout: &TextLayout,
20 runs: &[TextRun],
21 time: MediaTime,
22) -> Vec<f32> {
23 match selector {
24 SampledSelector::Range {
25 based_on,
26 units,
27 start,
28 end,
29 offset,
30 shape,
31 amount,
32 } => {
33 let (unit_of, count) = units_of(layout, *based_on);
34 #[allow(clippy::cast_precision_loss)]
35 let count_f = count as f32;
36 let (mut s, mut e) = match units {
37 RangeUnits::Percent => (
38 (start + offset) / 100.0 * count_f,
39 (end + offset) / 100.0 * count_f,
40 ),
41 RangeUnits::Index => (start + offset, end + offset),
42 };
43 if s > e {
44 std::mem::swap(&mut s, &mut e);
45 }
46 unit_of
47 .iter()
48 .map(|unit| unit.map_or(0.0, |unit| range_value(*shape, unit, s, e) * amount))
49 .collect()
50 }
51 SampledSelector::Timing { state, ramp, sweep } => {
52 timing(layout, runs, time, *state, *ramp, *sweep)
53 }
54 SampledSelector::Class { class } => {
55 let run_of = run_of_clusters(layout, runs);
56 run_of
57 .iter()
58 .map(|run| {
59 let selected =
60 run.is_some_and(|run| runs[run].class.iter().any(|name| name == class));
61 if selected { 1.0 } else { 0.0 }
62 })
63 .collect()
64 }
65 }
66}
67
68fn units_of(layout: &TextLayout, based_on: TextUnit) -> (Vec<Option<u32>>, u32) {
71 let clusters = &layout.clusters;
72 match based_on {
73 TextUnit::Characters => (
74 (0..u32::try_from(clusters.len()).unwrap_or(u32::MAX))
75 .map(Some)
76 .collect(),
77 u32::try_from(clusters.len()).unwrap_or(u32::MAX),
78 ),
79 TextUnit::CharactersExcludingSpaces => {
80 let mut next = 0;
81 let units = clusters
82 .iter()
83 .map(|cluster| {
84 (!cluster.is_space).then(|| {
85 next += 1;
86 next - 1
87 })
88 })
89 .collect();
90 (units, next)
91 }
92 TextUnit::Words => {
93 let count = clusters
94 .iter()
95 .filter_map(|cluster| cluster.word)
96 .max()
97 .map_or(0, |last| last + 1);
98 (clusters.iter().map(|cluster| cluster.word).collect(), count)
99 }
100 TextUnit::Lines => (
101 clusters.iter().map(|cluster| Some(cluster.line)).collect(),
102 u32::try_from(layout.lines.len()).unwrap_or(u32::MAX),
103 ),
104 }
105}
106
107#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
110fn range_value(shape: RangeShape, unit: u32, s: f32, e: f32) -> f32 {
111 #[allow(clippy::float_cmp)]
112 if e == s {
113 return 0.0;
114 }
115 let i = unit as f32;
116 let ramp = || (i + 0.5 - s) / (e - s);
117 let triangle = |u: f32| {
118 if (0.0..=1.0).contains(&u) {
119 1.0 - (2.0 * u - 1.0).abs()
120 } else {
121 0.0
122 }
123 };
124 match shape {
125 RangeShape::Square => (e.min(i + 1.0) - s.max(i)).clamp(0.0, 1.0),
126 RangeShape::RampUp => ramp().clamp(0.0, 1.0),
127 RangeShape::RampDown => 1.0 - ramp().clamp(0.0, 1.0),
128 RangeShape::Triangle => triangle(ramp()),
129 RangeShape::Round => {
130 let u = ramp();
131 if (0.0..=1.0).contains(&u) {
132 let x = 2.0 * u - 1.0;
133 (1.0 - x * x).max(0.0).sqrt()
134 } else {
135 0.0
136 }
137 }
138 RangeShape::Smooth => {
139 let t = triangle(ramp());
140 t * t * (3.0 - 2.0 * t)
141 }
142 }
143}
144
145fn run_of_clusters(layout: &TextLayout, runs: &[TextRun]) -> Vec<Option<usize>> {
147 let mut ends = Vec::with_capacity(runs.len());
148 let mut end = 0;
149 for run in runs {
150 end += run.text.len();
151 ends.push(end);
152 }
153 layout
154 .clusters
155 .iter()
156 .map(|cluster| {
157 let run = ends.partition_point(|&end| end <= cluster.bytes.start);
158 (run < runs.len()).then_some(run)
159 })
160 .collect()
161}
162
163#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
165fn timing(
166 layout: &TextLayout,
167 runs: &[TextRun],
168 time: MediaTime,
169 state: RunState,
170 ramp: MediaTime,
171 sweep: bool,
172) -> Vec<f32> {
173 let run_of = run_of_clusters(layout, runs);
174 let ramp = ramp.ticks();
175 let reached = |at: i64| -> f32 {
177 let past = time.ticks().saturating_sub(at);
178 if ramp <= 0 {
179 if past >= 0 { 1.0 } else { 0.0 }
180 } else {
181 (past as f64 / ramp as f64).clamp(0.0, 1.0) as f32
182 }
183 };
184 let mut place = vec![(0_usize, 0_usize); run_of.len()];
186 let mut counts = vec![0_usize; runs.len()];
187 for (cluster, run) in run_of.iter().enumerate() {
188 if let Some(run) = run {
189 place[cluster].0 = counts[*run];
190 counts[*run] += 1;
191 }
192 }
193 for (cluster, run) in run_of.iter().enumerate() {
194 if let Some(run) = run {
195 place[cluster].1 = counts[*run];
196 }
197 }
198 run_of
199 .iter()
200 .zip(place)
201 .map(|(run, (index, count))| {
202 let Some(span) = run.and_then(|run| runs[run].time) else {
203 return 0.0;
204 };
205 let (start, end) = (span.start.ticks(), span.end.ticks());
206 let (a, b) = if sweep && count > 0 {
207 let length = i128::from(end - start);
208 let share = |at: usize| {
209 i64::try_from(i128::from(start) + length * at as i128 / count as i128)
210 .unwrap_or(end)
211 };
212 (share(index), share(index + 1))
213 } else {
214 (start, end)
215 };
216 let (begun, ended) = (reached(a), reached(b));
217 match state {
218 RunState::Upcoming => 1.0 - begun,
219 RunState::Active => begun * (1.0 - ended),
220 RunState::Spoken => ended,
221 }
222 })
223 .collect()
224}
225
226#[must_use]
230pub fn cluster_states(
231 layout: &TextLayout,
232 runs: &[TextRun],
233 animators: &[SampledAnimator],
234 time: MediaTime,
235 font_size: f32,
236) -> (Vec<ClusterState>, u64) {
237 let mut states = vec![ClusterState::IDENTITY; layout.clusters.len()];
238 let mut degrees = vec![0.0_f32; states.len()];
240 for animator in animators {
241 let weights = selection(&animator.selector, layout, runs, time);
242 for ((state, degrees), weight) in states.iter_mut().zip(&mut degrees).zip(weights) {
243 if weight <= 0.0 {
244 continue;
245 }
246 if let Some([x, y]) = animator.offset {
247 state.offset[0] += weight * x * font_size;
248 state.offset[1] += weight * y * font_size;
249 }
250 if let Some(scale) = animator.scale {
251 for (axis, target) in state.scale.iter_mut().zip(scale) {
252 *axis *= 1.0 + weight * (target - 1.0);
253 }
254 }
255 if let Some(rotation) = animator.rotation_degrees {
256 *degrees += weight * rotation;
257 }
258 if let Some(opacity) = animator.opacity {
259 state.opacity *= 1.0 + weight * (opacity - 1.0);
260 }
261 if let Some(target) = animator.fill {
262 state.fill = Some(mix_toward(state.fill, target, weight));
263 }
264 if let Some(target) = animator.stroke {
265 state.stroke = Some(mix_toward(state.stroke, target, weight));
266 }
267 if let Some(tracking) = animator.tracking {
268 state.tracking += weight * tracking * font_size;
269 }
270 }
271 }
272 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
273 let mut feed = |value: f32| {
274 for byte in value.to_bits().to_le_bytes() {
275 hash = (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3);
276 }
277 };
278 let mut identity = true;
279 for (state, degrees) in states.iter_mut().zip(degrees) {
280 state.rotation = degrees.to_radians();
281 state.opacity = state.opacity.clamp(0.0, 1.0);
282 identity &= state.is_identity();
283 let mixes = [state.fill, state.stroke].map(|mix| {
284 mix.map_or([1.0, 0.0, 0.0, 0.0, 0.0], |mix| {
285 [mix.keep, mix.add[0], mix.add[1], mix.add[2], mix.add[3]]
286 })
287 });
288 for value in state
289 .offset
290 .into_iter()
291 .chain(state.scale)
292 .chain([state.rotation, state.opacity, state.tracking])
293 .chain(mixes.into_iter().flatten())
294 {
295 feed(value);
296 }
297 }
298 (states, if identity { 0 } else { hash.max(1) })
299}
300
301fn mix_toward(mix: Option<ColorMix>, target: [f32; 4], weight: f32) -> ColorMix {
304 let mix = mix.unwrap_or(ColorMix {
305 keep: 1.0,
306 add: [0.0; 4],
307 });
308 let target = premultiplied_linear(target);
309 ColorMix {
310 keep: mix.keep * (1.0 - weight),
311 add: std::array::from_fn(|channel| {
312 mix.add[channel] * (1.0 - weight) + target[channel] * weight
313 }),
314 }
315}
316
317fn premultiplied_linear(color: [f32; 4]) -> [f32; 4] {
319 let alpha = color[3].clamp(0.0, 1.0);
320 let linear = |value: f32| {
321 let value = value.clamp(0.0, 1.0);
322 if value >= 1.0 {
323 1.0
324 } else if value <= 0.040_45 {
325 value / 12.92
326 } else {
327 libm::powf((value + 0.055) / 1.055, 2.4)
328 }
329 };
330 [
331 linear(color[0]) * alpha,
332 linear(color[1]) * alpha,
333 linear(color[2]) * alpha,
334 alpha,
335 ]
336}
337
338#[cfg(test)]
339#[path = "text_animation_tests.rs"]
340mod tests;