1use super::{TextMatch, TextOptions, TextTarget};
2use crate::{AnimationState, MotionValue, Playback, PresenceMode, stagger};
3
4use std::{
5 collections::{HashMap, VecDeque},
6 sync::Arc,
7 time::Duration,
8};
9
10#[derive(Clone, Debug)]
12pub struct TextLayoutUnit {
13 pub text: Arc<str>,
15 pub slot: usize,
17 pub origin: [f64; 2],
19 pub size: [f64; 2],
21 pub em: f64,
23 pub enter: Option<TextTarget>,
25}
26
27impl TextLayoutUnit {
28 pub fn new(
30 text: impl Into<Arc<str>>,
31 slot: usize,
32 origin: [f64; 2],
33 size: [f64; 2],
34 em: f64,
35 ) -> Self {
36 assert!(origin.iter().all(|value| value.is_finite()));
37 assert!(size.iter().all(|value| value.is_finite() && *value >= 0.));
38 assert!(em.is_finite() && em > 0.);
39
40 Self {
41 text: text.into(),
42 slot,
43 origin,
44 size,
45 em,
46 enter: None,
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug)]
54pub struct TextPresentation {
55 pub id: u64,
57 pub origin: [f64; 2],
59 pub clip_origin: [f64; 2],
61 pub size: [f64; 2],
63 pub opacity: f64,
65 pub reveal: f64,
67 pub substitution: Option<usize>,
69 pub exiting: bool,
71 pub visible: bool,
73}
74
75struct Unit {
76 layout: TextLayoutUnit,
77 id: u64,
78 position: [MotionValue; 2],
79 pose: [MotionValue; 4],
80 last_pose: [f64; 4],
81 enter: TextTarget,
82 delay: Duration,
83 sequence_index: usize,
84 born: Duration,
85 exiting: bool,
86 waiting: bool,
87 finished: bool,
88 animation: AnimationState,
89 declared_clip: Option<crate::Animation>,
90 clip_resume: Option<(Duration, Duration)>,
91 frame: TextPresentation,
92 scramble: Option<super::Scramble>,
93}
94
95impl Unit {
96 fn reset_pose(&mut self, now: Duration, options: &TextOptions, count: usize, animate: bool) {
97 self.delay = stagger(
98 options.stagger,
99 self.sequence_index,
100 count,
101 options.stagger_from,
102 )
103 .unwrap_or(Duration::ZERO);
104 self.born = now;
105 self.finished = false;
106 let from = if animate {
107 self.enter
108 } else {
109 TextTarget::default()
110 };
111
112 let values = from.resolved_values(self.layout.em, self.layout.size[1]);
113
114 for (value, initial) in self.pose.iter_mut().zip(values) {
115 value.jump(initial, now);
116 }
117
118 self.animation = AnimationState::default();
119 self.declared_clip = None;
120 self.clip_resume = None;
121 self.last_pose = values;
122 self.target_pose(now, options, self.delay);
123 }
124
125 fn entrance_progress(&self, progress: f64, total: f64, options: &TextOptions) -> f64 {
126 let duration = options.entrance_duration(1).as_secs_f64();
127 let start = self.delay.as_secs_f64();
128
129 if duration == 0. {
130 if progress * total >= start { 1. } else { 0. }
131 } else {
132 ((progress * total - start) / duration).clamp(0., 1.)
133 }
134 }
135
136 fn target_pose(&mut self, now: Duration, options: &TextOptions, delay: Duration) {
137 let target = if self.exiting {
138 options.exit
139 } else {
140 TextTarget::default()
141 };
142
143 let values = target.resolved_values(self.layout.em, self.layout.size[1]);
144
145 for (index, (value, target)) in self.pose.iter_mut().zip(values).enumerate() {
146 value.animate_delayed(target, options.pose_transition(index), delay, now);
147 }
148 }
149
150 fn resume_pose(&mut self, now: Duration, options: &TextOptions) {
151 for (value, held) in self.pose.iter_mut().zip(self.last_pose) {
152 value.jump(held, now);
153 }
154
155 self.target_pose(now, options, Duration::ZERO);
156 }
157
158 fn sample(
159 &mut self,
160 now: Duration,
161 options: &TextOptions,
162 reduced: bool,
163 progress: Option<f64>,
164 ) -> bool {
165 if self.waiting {
166 self.frame.visible = false;
167
168 return false;
169 }
170
171 let mut active = false;
172 let mut origin = [0.; 2];
173
174 for (axis, value) in self.position.iter_mut().enumerate() {
175 let frame = value.sample(now, reduced);
176
177 origin[axis] = frame.value();
178 active |= frame.is_animating();
179 }
180
181 let mut pose = [0.; 4];
182
183 if let Some(progress) = progress.filter(|_| !self.exiting && !reduced) {
184 let elapsed = options.entrance_duration(1).mul_f64(progress);
185
186 for (index, ((output, initial), target)) in pose
187 .iter_mut()
188 .zip(
189 self.enter
190 .resolved_values(self.layout.em, self.layout.size[1]),
191 )
192 .zip(TextTarget::default().values())
193 .enumerate()
194 {
195 let mut value = MotionValue::new(initial);
196
197 value.animate(target, options.pose_transition(index), Duration::ZERO);
198 *output = if progress >= 1. {
199 target
200 } else if progress <= 0. {
201 initial
202 } else {
203 value.sample(elapsed, false).value()
204 };
205 }
206 } else {
207 for (output, value) in pose.iter_mut().zip(&mut self.pose) {
208 let frame = value.sample(now, reduced);
209
210 *output = frame.value();
211 active |= frame.is_animating();
212 }
213 }
214
215 let mut clip = [0., 0., 1.];
216
217 if let Some(animation) = &options.animation {
218 if self.declared_clip.as_ref() != Some(animation) {
219 if self.declared_clip.is_some() {
220 self.clip_resume = Some((now, Duration::ZERO));
221 }
222
223 self.animation.sample(animation, now, reduced);
224 self.declared_clip = Some(animation.clone());
225 }
226
227 if let Some(progress) = progress.filter(|_| !self.exiting)
228 && let Some(duration) = animation.duration()
229 {
230 self.clip_resume = Some((now, duration.mul_f64(progress)));
231 }
232
233 let elapsed = self.clip_resume.map_or_else(
234 || now.saturating_sub(self.born).saturating_sub(self.delay),
235 |(anchor, cursor)| cursor.saturating_add(now.saturating_sub(anchor)),
236 );
237
238 let held = self.clip_resume.is_none() && now < self.born.saturating_add(self.delay);
239
240 self.animation.seek(elapsed, now);
241 let frame = self.animation.sample(animation, now, reduced);
242
243 clip = [
244 frame.target().x_px().into(),
245 frame.target().y_px().into(),
246 frame.target().alpha().into(),
247 ];
248 active |= !self.exiting && progress.is_none() && (held || frame.is_animating());
249 }
250
251 let mut substitution = if self.exiting {
252 self.frame.substitution
253 } else {
254 None
255 };
256
257 if let Some(scramble) = &self.scramble {
258 let elapsed = now.saturating_sub(self.born).saturating_sub(self.delay);
259 let in_scramble = progress.map_or(elapsed < scramble.duration, |value| value < 1.);
260
261 if in_scramble && !self.exiting && !reduced && !scramble.duration.is_zero() {
262 let tick = progress.map_or(elapsed.as_millis() as u64 / 35, |value| {
263 (value * 30.) as u64
264 });
265
266 let mut bits = scramble.seed
267 ^ self.id.wrapping_mul(0x9e3779b97f4a7c15)
268 ^ tick.wrapping_mul(0xbf58476d1ce4e5b9);
269
270 bits = (bits ^ (bits >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
271 bits = (bits ^ (bits >> 27)).wrapping_mul(0x94d049bb133111eb);
272 substitution = Some((bits ^ (bits >> 31)) as usize % scramble.alphabet.len());
273 active |= progress.is_none();
274 }
275 }
276
277 self.last_pose = pose;
278 self.frame = TextPresentation {
279 id: self.id,
280 clip_origin: origin,
281 origin: [
282 origin[0] + pose[0] * self.layout.em + clip[0],
283 origin[1] + pose[1] * self.layout.em + clip[1],
284 ],
285 size: self.layout.size,
286 opacity: (pose[2] * clip[2]).clamp(0., 1.),
287 reveal: pose[3].clamp(0., 1.),
288 substitution,
289 exiting: self.exiting,
290 visible: !self.waiting,
291 };
292 self.finished = self.exiting && !active;
293
294 active
295 }
296}
297
298#[derive(Default)]
302pub struct TextPlaybackState {
303 units: Vec<Unit>,
304 content: String,
305 options: TextOptions,
306 next_id: u64,
307 clock: Playback,
308 manual_progress: Option<f64>,
309 last_progress: Option<f64>,
310 initialized: bool,
311 active: bool,
312 count: usize,
313}
314
315impl TextPlaybackState {
316 pub fn reconcile(
319 &mut self,
320 text: &str,
321 layout: &[TextLayoutUnit],
322 options: &TextOptions,
323 now: Duration,
324 reduced: bool,
325 ) -> Vec<u64> {
326 self.sample(now, reduced, self.last_progress);
327 let time = self.clock.time(now);
328
329 let transition_changed = self.options.transition != options.transition
330 || self.options.opacity_transition != options.opacity_transition;
331
332 if self.last_progress.is_some() {
333 for unit in &mut self.units {
334 unit.resume_pose(time, &self.options);
335 }
336 }
337
338 let changed = self.content != text;
339 let first = !self.initialized;
340
341 self.initialized = true;
342 let mut candidates: HashMap<(Arc<str>, Option<usize>), VecDeque<usize>> = HashMap::new();
343
344 for (index, unit) in self.units.iter().enumerate() {
345 if !unit.waiting && (!changed || options.matching != TextMatch::Replace) {
346 let slot = if !changed || options.matching == TextMatch::Position {
347 Some(unit.layout.slot)
348 } else {
349 None
350 };
351
352 candidates
353 .entry((unit.layout.text.clone(), slot))
354 .or_default()
355 .push_back(index);
356 }
357 }
358
359 let mut used = vec![false; self.units.len()];
360 let mut assigned = Vec::with_capacity(layout.len());
361
362 for unit_layout in layout {
363 let slot = if !changed || options.matching == TextMatch::Position {
364 Some(unit_layout.slot)
365 } else {
366 None
367 };
368
369 let existing = candidates
370 .get_mut(&(unit_layout.text.clone(), slot))
371 .and_then(VecDeque::pop_front);
372
373 assigned.push(existing);
374
375 if let Some(index) = existing {
376 used[index] = true;
377 }
378 }
379
380 self.options = options.clone();
381
382 if options
383 .animation
384 .as_ref()
385 .is_some_and(|clip| clip.duration().is_none())
386 {
387 self.manual_progress = None;
388 self.last_progress = None;
389 }
390
391 self.count = layout.len();
392
393 for (index, unit) in self.units.iter_mut().enumerate() {
394 if !used[index] {
395 if unit.waiting {
396 unit.finished = true;
397 continue;
398 }
399
400 if !unit.exiting {
401 unit.exiting = true;
402 unit.target_pose(time, options, Duration::ZERO);
403 }
404 }
405 }
406
407 let waiting = options.presence_mode == PresenceMode::Wait
408 && self.units.iter().any(|unit| unit.exiting && !unit.finished);
409
410 let mut ids = Vec::with_capacity(layout.len());
411
412 for (index, (layout, existing)) in layout.iter().zip(assigned).enumerate() {
413 if let Some(existing) = existing {
414 let unit = &mut self.units[existing];
415 let returning = unit.exiting;
416
417 unit.layout = layout.clone();
418 unit.sequence_index = index;
419 unit.enter = layout.enter.unwrap_or(options.enter);
420 unit.scramble = options.scramble.clone();
421 unit.exiting = false;
422 unit.waiting = false;
423 unit.finished = false;
424
425 for (value, target) in unit.position.iter_mut().zip(layout.origin) {
426 if let Some(transition) = options.reflow {
427 value.animate(target, transition, time);
428 } else {
429 value.jump(target, time);
430 }
431 }
432
433 if returning || transition_changed {
434 unit.target_pose(time, options, Duration::ZERO);
435 }
436
437 ids.push(unit.id);
438 } else {
439 let id = self.next_id;
440
441 self.next_id = self
442 .next_id
443 .checked_add(1)
444 .expect("text identity exhausted");
445 let mut unit = Unit {
446 layout: layout.clone(),
447 id,
448 position: layout.origin.map(MotionValue::new),
449 pose: TextTarget::default().values().map(MotionValue::new),
450 last_pose: TextTarget::default().values(),
451 enter: layout.enter.unwrap_or(options.enter),
452 delay: Duration::ZERO,
453 sequence_index: index,
454 born: time,
455 exiting: false,
456 waiting,
457 finished: false,
458 animation: AnimationState::default(),
459 declared_clip: None,
460 clip_resume: None,
461 frame: TextPresentation {
462 id,
463 origin: layout.origin,
464 clip_origin: layout.origin,
465 size: layout.size,
466 opacity: 0.,
467 reveal: 1.,
468 substitution: None,
469 exiting: false,
470 visible: false,
471 },
472 scramble: options.scramble.clone(),
473 };
474
475 unit.reset_pose(time, options, self.count, !first || options.animate_initial);
476 self.units.push(unit);
477 ids.push(id);
478 }
479 }
480
481 self.content.clear();
482 self.content.push_str(text);
483 self.sample(now, reduced, self.last_progress);
484
485 ids
486 }
487
488 pub fn sample(&mut self, now: Duration, reduced: bool, progress: Option<f64>) -> bool {
492 let progress = progress.or(self.manual_progress);
493
494 if let Some(progress) = progress {
495 assert!((0. ..=1.).contains(&progress));
496 }
497
498 if progress.is_some() {
499 assert!(
500 self.options
501 .animation
502 .as_ref()
503 .is_none_or(|animation| animation.duration().is_some()),
504 "cannot scrub an infinite text clip"
505 );
506 }
507
508 let time = self.clock.time(now);
509
510 if progress.is_none() && self.last_progress.is_some() {
511 for unit in &mut self.units {
512 unit.resume_pose(time, &self.options);
513 }
514 }
515
516 self.last_progress = progress;
517 self.active = false;
518 let total = self.options.entrance_duration(self.count).as_secs_f64();
519
520 for unit in &mut self.units {
521 let unit_progress =
522 progress.map(|progress| unit.entrance_progress(progress, total, &self.options));
523
524 self.active |= unit.sample(time, &self.options, reduced, unit_progress);
525 }
526
527 self.units.retain(|unit| !unit.finished);
528
529 if !self.units.iter().any(|unit| unit.exiting) {
530 for unit in &mut self.units {
531 if unit.waiting {
532 unit.waiting = false;
533 unit.reset_pose(time, &self.options, self.count, !reduced);
534 let unit_progress = progress
535 .map(|progress| unit.entrance_progress(progress, total, &self.options));
536
537 self.active |= unit.sample(time, &self.options, reduced, unit_progress);
538 }
539 }
540 }
541
542 self.active &= self.clock.is_playing() && !reduced;
543
544 self.active
545 }
546
547 pub fn translate(&mut self, offset: [f64; 2]) {
551 assert!(offset.iter().all(|value| value.is_finite()));
552
553 for unit in &mut self.units {
554 for (axis, delta) in offset.into_iter().enumerate() {
555 unit.position[axis].translate(delta);
556 unit.layout.origin[axis] += delta;
557 unit.frame.origin[axis] += delta;
558 unit.frame.clip_origin[axis] += delta;
559 }
560 }
561 }
562
563 pub fn presentation(&self) -> impl Iterator<Item = &TextPresentation> {
565 self.units.iter().map(|unit| &unit.frame)
566 }
567
568 pub fn text(&self) -> &str {
570 &self.content
571 }
572
573 pub fn is_animating(&self) -> bool {
575 self.active
576 }
577
578 pub fn pause(&mut self, now: Duration) {
580 self.sample(now, false, self.last_progress);
581 self.clock.pause(now);
582 self.active = false;
583 }
584
585 pub fn play(&mut self, now: Duration) {
587 let time = self.clock.time(now);
588
589 if self.manual_progress.take().is_some() || self.last_progress.is_some() {
590 for unit in &mut self.units {
591 unit.resume_pose(time, &self.options);
592 }
593 }
594
595 self.last_progress = None;
596
597 self.clock.play(now);
598 }
599
600 pub fn seek(&mut self, progress: f64, now: Duration) -> bool {
603 assert!((0. ..=1.).contains(&progress));
604
605 if self
606 .options
607 .animation
608 .as_ref()
609 .is_some_and(|animation| animation.duration().is_none())
610 {
611 return false;
612 }
613
614 self.manual_progress = Some(progress);
615 self.clock.pause(now);
616 self.sample(now, false, None);
617
618 true
619 }
620
621 pub fn restart(&mut self, now: Duration) {
623 self.manual_progress = None;
624 self.last_progress = None;
625 self.clock.play(now);
626 let time = self.clock.time(now);
627
628 for unit in &mut self.units {
629 if !unit.exiting {
630 unit.reset_pose(time, &self.options, self.count, true);
631 }
632 }
633 }
634}