1use std::sync::Arc;
9
10use ffcine_core::anim::{Clock, Track, seconds};
11use ffcine_render::{Mask, Motion, Overlay, OverlayPart};
12use serde_json::{Map, Value};
13
14use crate::layout::{Glyph, Rendered, Role};
15
16pub use ffcine_core::anim::Ease;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Kind {
22 Fade,
23 Slide,
25 Pop,
27 Typewriter,
29 Wipe,
31 None,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36pub enum By {
37 Char,
38 Word,
39 Line,
40 Whole,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Dir {
45 Up,
46 Down,
47 Left,
48 Right,
49 Center,
51}
52
53impl Dir {
54 fn vector(self) -> [f32; 2] {
55 match self {
56 Self::Up => [0.0, -1.0],
57 Self::Down => [0.0, 1.0],
58 Self::Left => [-1.0, 0.0],
59 Self::Right => [1.0, 0.0],
60 Self::Center => [0.0, 0.0],
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
67pub struct Anim {
68 pub kind: Kind,
69 pub duration: f64,
71 pub by: By,
72 pub stagger: Option<f64>,
74 pub ease: Ease,
75 pub direction: Dir,
77 pub distance: f32,
79 pub scale: f32,
81}
82
83impl Anim {
84 pub fn fade(duration: f64) -> Self {
86 Self {
87 kind: Kind::Fade,
88 duration,
89 by: By::Whole,
90 stagger: None,
91 ease: Ease::Linear,
92 direction: Dir::Up,
93 distance: 0.04,
94 scale: 0.6,
95 }
96 }
97
98 pub fn parse(v: &Map<String, Value>) -> Self {
101 let kind = match v.get("kind").and_then(Value::as_str).unwrap_or("fade") {
102 "slide" | "slide_up" | "rise" => Kind::Slide,
103 "pop" | "scale" | "zoom" => Kind::Pop,
104 "typewriter" | "type" => Kind::Typewriter,
105 "wipe" | "reveal" => Kind::Wipe,
106 "none" | "cut" => Kind::None,
107 _ => Kind::Fade,
108 };
109 let by = match v.get("by").and_then(Value::as_str) {
110 Some("char" | "character" | "letter") => By::Char,
111 Some("word") => By::Word,
112 Some("line") => By::Line,
113 None if kind == Kind::Typewriter => By::Char,
114 _ => By::Whole,
115 };
116 let ease = v.get("ease").and_then(Ease::parse).unwrap_or(match kind {
117 Kind::Slide => Ease::Out,
118 Kind::Pop => Ease::Back,
119 Kind::Wipe => Ease::InOut,
120 _ => Ease::Linear,
121 });
122 let direction = match v.get("direction").and_then(Value::as_str) {
123 Some("down") => Dir::Down,
124 Some("left") => Dir::Left,
125 Some("right") => Dir::Right,
126 Some("center" | "centre") => Dir::Center,
127 Some("up") => Dir::Up,
128 _ if kind == Kind::Wipe => Dir::Right,
129 _ => Dir::Up,
130 };
131 #[allow(clippy::cast_possible_truncation)]
132 let num = |k: &str| v.get(k).and_then(Value::as_f64).map(|x| x as f32);
133 Self {
134 kind,
135 duration: v
136 .get("duration")
137 .and_then(Value::as_f64)
138 .unwrap_or(0.5)
139 .max(0.0),
140 by,
141 stagger: v.get("stagger").and_then(Value::as_f64),
142 ease,
143 direction,
144 distance: num("distance").unwrap_or(0.04),
145 scale: num("scale").unwrap_or(0.6),
146 }
147 }
148
149 fn stagger(&self, n: usize) -> f64 {
150 #[allow(clippy::cast_precision_loss)]
151 match (self.kind, self.stagger) {
152 (_, Some(s)) => s,
153 (Kind::Typewriter, None) => self.duration / n.max(1) as f64,
154 (_, None) => match self.by {
155 By::Char => 0.03,
156 By::Word => 0.1,
157 By::Line => 0.2,
158 By::Whole => 0.0,
159 },
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Default)]
169pub struct Keys<const N: usize> {
170 track: Option<Track<[f64; N]>>,
171}
172
173impl<const N: usize> Keys<N> {
174 #[allow(clippy::many_single_char_names)]
178 fn parse(v: Option<&Value>) -> Self {
179 let Some(v) = v else {
180 return Self::default();
181 };
182 let value = |v: &Value| -> Option<Value> {
184 Some(match v {
185 Value::Number(n) => Value::Array(vec![Value::from(n.as_f64()?); N]),
186 Value::Array(a) => {
187 let c: Vec<f64> = a.iter().filter_map(Value::as_f64).collect();
188 match (N, c.len()) {
189 (4, 3) => Value::from([c[0], c[1], c[2], 1.0].to_vec()),
190 (n, l) if n == l => Value::from(c),
191 _ => return None,
192 }
193 }
194 _ => return None,
195 })
196 };
197 let whole = |v: &Value| -> Value {
199 match v {
200 Value::Number(_) => value(v).unwrap_or_else(|| v.clone()),
201 Value::Array(a) if N == 4 && a.len() == 3 => {
202 let mut a = a.clone();
203 a.push(1.0.into());
204 Value::Array(a)
205 }
206 _ => v.clone(),
207 }
208 };
209 let keys = |a: &[Value]| -> Value {
210 a.iter()
211 .filter_map(|k| {
212 let t = k.get("t").and_then(seconds)?;
213 let v = value(k.get("v").or_else(|| k.get("value"))?)?;
214 let mut o = Map::new();
215 o.insert("t".into(), t.into());
216 o.insert("v".into(), v);
217 if let Some(e) = k.get("interp").or_else(|| k.get("ease"))
219 && Ease::parse(e).is_some()
220 {
221 o.insert("interp".into(), e.clone());
222 }
223 Some(Value::Object(o))
224 })
225 .collect()
226 };
227 let norm = match v {
229 Value::Array(a) if a.iter().any(Value::is_object) => keys(a),
230 Value::Object(o) if o.contains_key("expr") => {
231 let mut o = o.clone();
232 if let Some(Value::Array(a)) = o.get("keys") {
233 let k = keys(a);
234 o.insert("keys".into(), k);
235 }
236 Value::Object(o)
237 }
238 _ => whole(v),
239 };
240 if norm.as_array().is_some_and(Vec::is_empty) {
241 return Self::default();
242 }
243 match Track::parse(&norm, Clock::Clip, "text keyframes") {
244 Ok(t) => Self { track: Some(t) },
245 Err(e) => {
246 eprintln!("warning: {e}");
247 Self::default()
248 }
249 }
250 }
251
252 pub fn is_empty(&self) -> bool {
253 self.track.is_none()
254 }
255
256 pub fn at(&self, t: f64, len: f64) -> Option<[f64; N]> {
258 self.track.as_ref()?.at(Clock::Clip, t, len)
259 }
260
261 fn max(&self, len: f64, fps: f64) -> Option<f64> {
264 let t = self.track.as_ref()?;
265 match t {
266 Track::Const(c) => c.iter().copied().reduce(f64::max),
267 Track::Keys(k) => k.iter().flat_map(|k| k.v.iter().copied()).reduce(f64::max),
268 _ => {
269 #[allow(
270 clippy::cast_possible_truncation,
271 clippy::cast_sign_loss,
272 clippy::cast_precision_loss
273 )]
274 let n = ((len.max(0.0) * fps.max(1.0)).ceil() as usize).clamp(1, MAX_SAMPLES);
275 #[allow(clippy::cast_precision_loss)]
276 (0..=n)
277 .filter_map(|i| self.at(len * i as f64 / n as f64, len))
278 .flatten()
279 .reduce(f64::max)
280 }
281 }
282 }
283}
284
285const MAX_SAMPLES: usize = 100_000;
287
288#[derive(Debug, Clone, PartialEq, Default)]
290pub struct Animation {
291 pub in_: Option<Anim>,
292 pub out: Option<Anim>,
293 pub position: Keys<2>,
295 pub scale: Keys<2>,
297 pub opacity: Keys<1>,
299 pub colour: Keys<4>,
301}
302
303impl Animation {
304 pub fn parse(m: &Map<String, Value>) -> Self {
307 let side = |dir: &str, flat: &str| {
308 match m.get("animation").and_then(|a| a.get(dir)) {
309 Some(Value::Object(o)) => Some(Anim::parse(o)),
310 _ => m.get(flat).and_then(Value::as_f64).map(Anim::fade),
311 }
312 .filter(|a| a.kind != Kind::None && a.duration > 0.0)
313 };
314 let k = m.get("keyframes");
315 Self {
316 in_: side("in", "fade_in"),
317 out: side("out", "fade_out"),
318 position: Keys::parse(k.and_then(|k| k.get("position"))),
319 scale: Keys::parse(k.and_then(|k| k.get("scale"))),
320 opacity: Keys::parse(k.and_then(|k| k.get("opacity"))),
321 colour: Keys::parse(k.and_then(|k| k.get("colour").or_else(|| k.get("color")))),
322 }
323 }
324
325 pub fn is_plain_fade(&self) -> bool {
327 let plain = |a: &Option<Anim>| {
328 a.is_none_or(|a| a.kind == Kind::Fade && a.by == By::Whole && a.ease == Ease::Linear)
329 };
330 plain(&self.in_)
331 && plain(&self.out)
332 && self.position.is_empty()
333 && self.scale.is_empty()
334 && self.opacity.is_empty()
335 && self.colour.is_empty()
336 }
337
338 pub fn raster_scale(&self) -> f32 {
342 self.raster_scale_over(0.0, 1.0)
343 }
344
345 #[allow(clippy::cast_possible_truncation)]
348 pub fn raster_scale_over(&self, len: f64, fps: f64) -> f32 {
349 self.scale
350 .max(len, fps)
351 .map_or(1.0, |s| s.clamp(1.0, 4.0) as f32)
352 }
353}
354
355#[derive(Debug, Clone, Copy)]
357struct Unit {
358 crop: [f32; 4],
359 line: usize,
360 group: [usize; 2],
361}
362
363fn units(r: &Rendered, mask: [f32; 2], by: By) -> Vec<(Unit, usize)> {
366 let pad = r.pad;
367 let n_lines = r.lines.len().saturating_sub(1);
368 if by == By::Whole || r.glyphs.is_empty() || n_lines == 0 {
369 return vec![(
370 Unit {
371 crop: [0.0, 0.0, mask[0], mask[1]],
372 line: 0,
373 group: [0; 2],
374 },
375 0,
376 )];
377 }
378 let band = |l: usize| {
379 let y0 = if l == 0 { 0.0 } else { pad + r.lines[l] };
380 let y1 = if l + 1 >= n_lines {
381 mask[1]
382 } else {
383 pad + r.lines[l + 1]
384 };
385 (y0, y1)
386 };
387 if by == By::Line {
388 return (0..n_lines)
389 .map(|l| {
390 let (y0, y1) = band(l);
391 (
392 Unit {
393 crop: [0.0, y0, mask[0], y1],
394 line: l,
395 group: [0; 2],
396 },
397 l,
398 )
399 })
400 .collect();
401 }
402 let mut pieces: Vec<(usize, f32)> = Vec::new();
404 let mut prev: Option<&Glyph> = None;
405 for g in &r.glyphs {
406 let starts = match prev {
407 None => true,
408 Some(p) if p.line != g.line => true,
409 Some(p) => by == By::Char || (p.space && !g.space),
410 };
411 if starts {
412 pieces.push((g.line, pad + g.x[0]));
413 }
414 prev = Some(g);
415 }
416 (0..pieces.len())
417 .map(|i| {
418 let (line, x) = pieces[i];
419 let first = i == 0 || pieces[i - 1].0 != line;
420 let next = pieces.get(i + 1).filter(|p| p.0 == line);
421 let (y0, y1) = band(line);
422 (
423 Unit {
424 crop: [
425 if first { 0.0 } else { x },
426 y0,
427 next.map_or(mask[0], |p| p.1),
428 y1,
429 ],
430 line,
431 group: [0; 2],
432 },
433 i,
434 )
435 })
436 .collect()
437}
438
439fn partition(r: &Rendered, mask: [f32; 2], by: [By; 2]) -> (Vec<Unit>, [usize; 2]) {
441 let fine = by[0].min(by[1]);
442 let base = units(r, mask, fine);
443 let mut counts = [1usize; 2];
444 let mut out: Vec<Unit> = base.iter().map(|(u, _)| *u).collect();
445 for side in 0..2 {
446 if by[side] == fine {
447 for (o, (_, i)) in out.iter_mut().zip(&base) {
448 o.group[side] = *i;
449 }
450 counts[side] = base.len();
451 continue;
452 }
453 let coarse = units(r, mask, by[side]);
455 for o in &mut out {
456 let cx = f32::midpoint(o.crop[0], o.crop[2]);
457 o.group[side] = coarse
458 .iter()
459 .find(|(c, _)| c.line == o.line && cx >= c.crop[0] && cx <= c.crop[2])
460 .or_else(|| {
461 coarse
462 .iter()
463 .find(|(c, _)| by[side] == By::Whole || c.line == o.line)
464 })
465 .map_or(0, |(_, i)| *i);
466 }
467 counts[side] = coarse.len();
468 }
469 (out, counts)
470}
471
472#[derive(Debug, Clone, Copy, PartialEq)]
475struct Look {
476 k: f32,
477 d: [f32; 2],
478 alpha: f32,
479 wipe: Option<([f32; 2], bool)>,
481}
482
483const REST: Look = Look {
484 k: 1.0,
485 d: [0.0, 0.0],
486 alpha: 1.0,
487 wipe: None,
488};
489
490#[allow(
492 clippy::cast_possible_truncation,
493 clippy::cast_precision_loss,
494 clippy::many_single_char_names
495)]
496fn look(anim: &Animation, g: [usize; 2], n: [usize; 2], t: f64, len: f64, fh: f32) -> Look {
497 let mut l = REST;
498 for (side, a) in [(0, &anim.in_), (1, &anim.out)] {
499 let Some(a) = a else { continue };
500 let s = a.stagger(n[side]);
501 let (gi, ni) = (g[side] as f64, n[side] as f64);
502 let raw = if a.kind == Kind::Typewriter {
503 let shown = if side == 0 {
504 t >= gi * s
505 } else {
506 t < len - a.duration + (ni - 1.0 - gi) * s
508 };
509 if shown { 1.0 } else { 0.0 }
510 } else {
511 let rel = if side == 0 {
512 t - gi * s
513 } else {
514 len - t - (ni - 1.0 - gi) * s
515 };
516 if a.duration > 0.0 {
517 (rel / a.duration).clamp(0.0, 1.0)
518 } else {
519 1.0
520 }
521 };
522 if raw >= 1.0 {
523 continue;
524 }
525 let p = a.ease.at(raw) as f32;
526 let raw = raw as f32;
527 match a.kind {
528 Kind::Fade => l.alpha *= p.clamp(0.0, 1.0),
529 Kind::Typewriter => l.alpha *= raw,
530 Kind::Slide => {
531 let v = a.direction.vector();
532 let sign = if side == 0 { -1.0 } else { 1.0 };
533 let dist = sign * (1.0 - p) * a.distance * fh;
534 l.d = [l.d[0] + v[0] * dist, l.d[1] + v[1] * dist];
535 l.alpha *= raw;
536 }
537 Kind::Pop => {
538 l.k *= a.scale + (1.0 - a.scale) * p;
539 l.alpha *= raw;
540 }
541 Kind::Wipe => {
542 let p = p.clamp(0.0, 1.0);
543 let horizontal = matches!(a.direction, Dir::Left | Dir::Right | Dir::Center);
544 let span = match (a.direction, side) {
546 (Dir::Center, _) => [0.5 - p / 2.0, 0.5 + p / 2.0],
547 (Dir::Right | Dir::Down, 0) | (Dir::Left | Dir::Up, 1) => [0.0, p],
548 _ => [1.0 - p, 1.0],
549 };
550 let prev = l.wipe.map_or([0.0, 1.0], |w| w.0);
551 l.wipe = Some(([prev[0].max(span[0]), prev[1].min(span[1])], horizontal));
552 }
553 Kind::None => {}
554 }
555 }
556 l
557}
558
559fn wiped(
561 src: [f32; 4],
562 dst: [f32; 4],
563 w: Option<([f32; 2], bool)>,
564) -> Option<([f32; 4], [f32; 4])> {
565 let Some(([a, b], horizontal)) = w else {
566 return Some((src, dst));
567 };
568 if b <= a {
569 return None;
570 }
571 let cut = |r: [f32; 4], i: usize| {
572 let len = r[i + 2] - r[i];
573 let mut o = r;
574 o[i] = r[i] + a * len;
575 o[i + 2] = r[i] + b * len;
576 o
577 };
578 let i = usize::from(!horizontal);
579 Some((cut(src, i), cut(dst, i)))
580}
581
582fn transform(r: [f32; 4], k: [f32; 2], c: [f32; 2], d: [f32; 2]) -> [f32; 4] {
584 [
585 c[0] + (r[0] - c[0]) * k[0] + d[0],
586 c[1] + (r[1] - c[1]) * k[1] + d[1],
587 c[0] + (r[2] - c[0]) * k[0] + d[0],
588 c[1] + (r[3] - c[1]) * k[1] + d[1],
589 ]
590}
591
592#[derive(Debug, Clone, Copy)]
594pub struct Span {
595 pub from: i64,
597 pub to: i64,
598 pub frames: i64,
600 pub rate: f64,
601 pub delay: f64,
603}
604
605#[allow(
609 clippy::cast_possible_truncation,
610 clippy::cast_precision_loss,
611 clippy::too_many_lines,
612 clippy::many_single_char_names
613)]
614pub fn animate(
615 r: &Rendered,
616 anim: &Animation,
617 raster: f32,
618 a: [f32; 2],
619 base: [f32; 2],
620 frame: [f32; 2],
621 span: Span,
622) -> Vec<Overlay> {
623 let [fw, fh] = frame;
624 let Some(fill) = r
625 .layers
626 .iter()
627 .find(|l| l.role != Role::Box)
628 .or_else(|| r.layers.first())
629 else {
630 return Vec::new();
631 };
632 let at = fill.parts.first().map_or(r.origin, |p| p.pos);
633 let mask = [fill.mask.width as f32, fill.mask.height as f32];
634 let by = [
635 anim.in_.map_or(By::Whole, |a| a.by),
636 anim.out.map_or(By::Whole, |a| a.by),
637 ];
638 let solid = r.glyphs.is_empty();
639 let (units, counts) = partition(r, mask, by);
640 let centre = |side: usize, g: usize| {
642 let mut b = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
643 for u in units.iter().filter(|u| u.group[side] == g) {
644 b = [
645 b[0].min(u.crop[0]),
646 b[1].min(u.crop[1]),
647 b[2].max(u.crop[2]),
648 b[3].max(u.crop[3]),
649 ];
650 }
651 [
652 at[0] + f32::midpoint(b[0], b[2]),
653 at[1] + f32::midpoint(b[1], b[3]),
654 ]
655 };
656 let side = usize::from(
658 anim.in_.is_none_or(|a| a.kind != Kind::Pop)
659 && anim.out.is_some_and(|a| a.kind == Kind::Pop),
660 );
661 let centres: Vec<[f32; 2]> = units.iter().map(|u| centre(side, u.group[side])).collect();
662 let box_centre = [r.origin[0] + r.size[0] / 2.0, r.origin[1] + r.size[1] / 2.0];
663 let len = (span.to - span.from) as f64 / span.rate;
664 let lo = span.from.max(0);
665 let hi = span.to.min(span.frames).max(lo + 1);
666 let mut frames: Vec<Vec<Vec<OverlayPart>>> = vec![Vec::new(); r.layers.len()];
667 for f in lo..hi {
668 let t = (f - span.from) as f64 / span.rate + 0.5 / span.rate;
669 let kt = t + span.delay;
670 let klen = len + span.delay;
671 let g = anim
672 .scale
673 .at(kt, klen)
674 .map_or([1.0, 1.0], |s| s.map(|v| v as f32));
675 let g = [g[0] / raster, g[1] / raster];
676 let move_ = anim.position.at(kt, klen).map_or([0.0, 0.0], |p| {
677 [(p[0] as f32 - base[0]) * fw, (p[1] as f32 - base[1]) * fh]
678 });
679 let opacity = anim.opacity.at(kt, klen).map_or(1.0, |o| o[0] as f32);
680 let colour = anim.colour.at(kt, klen).map(|c| c.map(|v| v as f32));
681 let looks: Vec<Look> = units
682 .iter()
683 .map(|u| look(anim, u.group, counts, t, len, fh * raster))
684 .collect();
685 let whole = by == [By::Whole; 2];
686 for (li, layer) in r.layers.iter().enumerate() {
687 let tint = |p: &OverlayPart, alpha: f32| {
688 let mut c = p.colour;
689 if let Some(k) = colour
690 .filter(|_| layer.role == Role::Fill || (solid && layer.role == Role::Box))
691 {
692 c = k;
693 }
694 c[3] *= alpha * opacity;
695 c
696 };
697 let mut parts: Vec<OverlayPart> = Vec::new();
698 for p in &layer.parts {
699 if layer.role == Role::Box {
700 let (src, dst) = p.rects(&layer.mask);
703 let l = if whole || solid {
704 looks[0]
705 } else {
706 Look {
707 alpha: looks.iter().map(|l| l.alpha).fold(0.0, f32::max),
708 ..REST
709 }
710 };
711 let Some((src, dst)) = wiped(src, dst, l.wipe) else {
712 continue;
713 };
714 let dst = transform(dst, [l.k, l.k], box_centre, l.d);
715 let dst = transform(dst, g, a, move_);
716 let colour = tint(p, l.alpha);
717 if colour[3] > 1.0 / 512.0 {
718 parts.push(OverlayPart {
719 pos: [dst[0], dst[1]],
720 size: Some([dst[2] - dst[0], dst[3] - dst[1]]),
721 src: p.src.map(|_| src),
723 colour,
724 });
725 }
726 continue;
727 }
728 let mut run: Option<(Look, usize, [f32; 4])> = None;
730 let flush = |run: Option<(Look, usize, [f32; 4])>, parts: &mut Vec<OverlayPart>| {
731 let Some((l, ui, crop)) = run else { return };
732 let dst = [
733 p.pos[0] + crop[0],
734 p.pos[1] + crop[1],
735 p.pos[0] + crop[2],
736 p.pos[1] + crop[3],
737 ];
738 let Some((src, dst)) = wiped(crop, dst, l.wipe) else {
739 return;
740 };
741 let dst = transform(dst, [l.k, l.k], centres[ui], l.d);
742 let dst = transform(dst, g, a, move_);
743 let colour = tint(p, l.alpha);
744 if colour[3] > 1.0 / 512.0 {
745 parts.push(OverlayPart {
746 pos: [dst[0], dst[1]],
747 size: Some([dst[2] - dst[0], dst[3] - dst[1]]),
748 src: Some(src),
749 colour,
750 });
751 }
752 };
753 for (ui, (u, l)) in units.iter().zip(&looks).enumerate() {
754 match &mut run {
755 Some((rl, rui, crop))
756 if rl == l
757 && (l.k - 1.0).abs() < f32::EPSILON
758 && l.wipe.is_none()
759 && units[*rui].line == u.line
760 && (crop[2] - u.crop[0]).abs() < 1e-3 =>
761 {
762 crop[2] = u.crop[2];
763 }
764 _ => {
765 flush(run.take(), &mut parts);
766 run = Some((*l, ui, u.crop));
767 }
768 }
769 }
770 flush(run, &mut parts);
771 }
772 frames[li].push(parts);
773 }
774 }
775 r.layers
776 .iter()
777 .zip(frames)
778 .map(|(l, frames)| Overlay {
779 mask: Arc::clone(&l.mask),
780 parts: frames.last().cloned().unwrap_or_default(),
781 from: span.from,
782 to: span.to,
783 fade_in: 0.0,
784 fade_out: 0.0,
785 motion: Some(Arc::new(Motion { first: lo, frames })),
786 under: None,
787 })
788 .collect()
789}
790
791pub fn rect(
793 size: [f32; 2],
794 base: [f32; 2],
795 anchor: [f32; 2],
796 frame: [f32; 2],
797 colour: [f32; 4],
798) -> Rendered {
799 let a = [(base[0] + 0.5) * frame[0], (base[1] + 0.5) * frame[1]];
800 let origin = [a[0] - anchor[0] * size[0], a[1] - anchor[1] * size[1]];
801 Rendered {
802 layers: vec![crate::layout::Layer {
803 mask: Arc::new(Mask::solid()),
804 parts: vec![OverlayPart {
805 pos: origin,
806 size: Some(size),
807 src: None,
808 colour,
809 }],
810 role: Role::Box,
811 }],
812 origin,
813 size,
814 pad: 0.0,
815 lines: vec![0.0, size[1]],
816 glyphs: Vec::new(),
817 }
818}
819
820#[cfg(test)]
821#[allow(
822 clippy::cast_precision_loss,
823 clippy::cast_possible_truncation,
824 clippy::cast_sign_loss,
825 clippy::many_single_char_names
826)]
827mod tests {
828 use super::*;
829 use crate::layout::Layer;
830 use serde_json::json;
831
832 #[allow(clippy::needless_pass_by_value)]
833 fn obj(v: Value) -> Map<String, Value> {
834 v.as_object().cloned().unwrap_or_default()
835 }
836
837 fn text_box() -> Rendered {
839 let mask = Mask {
840 width: 100,
841 height: 20,
842 data: vec![255; 2000],
843 };
844 let glyphs = (0..5)
845 .map(|i| Glyph {
846 line: 0,
847 x: [i as f32 * 20.0, i as f32 * 20.0 + 20.0],
848 space: i == 2,
849 })
850 .collect();
851 Rendered {
852 layers: vec![Layer {
853 mask: Arc::new(mask),
854 parts: vec![OverlayPart {
855 pos: [50.0, 40.0],
856 colour: [1.0; 4],
857 ..OverlayPart::default()
858 }],
859 role: Role::Fill,
860 }],
861 origin: [50.0, 40.0],
862 size: [100.0, 20.0],
863 pad: 0.0,
864 lines: vec![0.0, 20.0],
865 glyphs,
866 }
867 }
868
869 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
872 fn coverage(ov: &[Overlay], frame: i64) -> Vec<f32> {
873 let (w, h) = (200usize, 100usize);
874 let mut out = vec![0.0f32; w * h];
875 for o in ov {
876 let fade = o.opacity(frame);
877 for p in o.parts_at(frame) {
878 let (s, d) = p.rects(&o.mask);
879 for y in 0..h {
880 for x in 0..w {
881 let (cx, cy) = (x as f32 + 0.5, y as f32 + 0.5);
882 if cx < d[0] || cx >= d[2] || cy < d[1] || cy >= d[3] {
883 continue;
884 }
885 let u = s[0] + (cx - d[0]) / (d[2] - d[0]) * (s[2] - s[0]);
886 let v = s[1] + (cy - d[1]) / (d[3] - d[1]) * (s[3] - s[1]);
887 let m = o.mask.data[(v as usize).min(o.mask.height as usize - 1)
888 * o.mask.width as usize
889 + (u as usize).min(o.mask.width as usize - 1)];
890 let a = f32::from(m) / 255.0 * p.colour[3] * fade;
891 let px = &mut out[y * w + x];
892 *px = *px + a * (1.0 - *px);
893 }
894 }
895 }
896 }
897 out
898 }
899
900 fn span(frames: i64) -> Span {
901 Span {
902 from: 0,
903 to: frames,
904 frames,
905 rate: 10.0,
906 delay: 0.0,
907 }
908 }
909
910 fn run(params: Value, frames: i64) -> Vec<Overlay> {
911 let a = Animation::parse(&obj(params));
912 animate(
913 &text_box(),
914 &a,
915 1.0,
916 [100.0, 50.0],
917 [0.0, 0.0],
918 [200.0, 100.0],
919 span(frames),
920 )
921 }
922
923 #[test]
924 fn eases() {
925 for e in [
926 Ease::Linear,
927 Ease::In,
928 Ease::Out,
929 Ease::InOut,
930 Ease::Ease,
931 Ease::Back,
932 ] {
933 assert!(
934 e.at(0.0).abs() < 1e-9 && (e.at(1.0) - 1.0).abs() < 1e-9,
935 "{e:?}"
936 );
937 }
938 assert!(Ease::Back.at(0.7) > 1.0, "overshoots");
939 assert!(Ease::Out.at(0.5) > 0.5 && Ease::In.at(0.5) < 0.5);
940 assert!((Ease::Bezier([0.0, 0.0, 1.0, 1.0]).at(0.3) - 0.3).abs() < 1e-4);
941 assert_eq!(
942 Ease::parse(&json!([0.4, 0, 0.2, 1])),
943 Some(Ease::Bezier([0.4, 0.0, 0.2, 1.0]))
944 );
945 assert!(Ease::Hold.at(0.99).abs() < 1e-9);
946 }
947
948 #[test]
949 fn keyframes_interpolate_and_count_from_the_end() {
950 let k: Keys<2> = Keys::parse(Some(&json!([
951 {"t": 0, "v": [0.0, 0.0], "interp": "linear"},
952 {"t": {"value": 24, "rate": 24}, "v": [1.0, 2.0]},
953 {"t": -1, "v": [3.0, 3.0]},
954 ])));
955 assert_eq!(k.at(-1.0, 10.0), Some([0.0, 0.0]));
956 assert_eq!(k.at(0.5, 10.0), Some([0.5, 1.0]));
957 assert_eq!(k.at(5.0, 10.0), Some([2.0, 2.5]));
958 assert_eq!(k.at(20.0, 10.0), Some([3.0, 3.0]));
959 let s: Keys<2> = Keys::parse(Some(&json!([{"t": 0, "v": 2}])));
960 assert_eq!(s.at(0.0, 1.0), Some([2.0, 2.0]));
961 }
962
963 #[test]
964 fn parses_and_tells_plain_fades() {
965 let a = Animation::parse(&obj(json!({"fade_in": 0.5})));
966 assert!(a.is_plain_fade());
967 let a = Animation::parse(&obj(
968 json!({"animation": {"in": {"kind": "typewriter", "duration": 1}}}),
969 ));
970 assert!(!a.is_plain_fade());
971 let t = a.in_.unwrap();
972 assert_eq!((t.kind, t.by), (Kind::Typewriter, By::Char));
973 let a = Animation::parse(&obj(json!({"animation": {"in": {"kind": "rotate3d"}}})));
974 assert_eq!(a.in_.map(|a| a.kind), Some(Kind::Fade));
975 let a = Animation::parse(&obj(
976 json!({"keyframes": {"scale": [{"t": 0, "v": 1}, {"t": 1, "v": 2.5}]}}),
977 ));
978 assert!((a.raster_scale() - 2.5).abs() < 1e-6);
979 }
980
981 #[test]
982 fn typewriter_types_left_to_right_and_merges() {
983 let ov = run(
985 json!({"animation": {"in": {"kind": "typewriter", "duration": 1}}}),
986 30,
987 );
988 let c = coverage(&ov, 4);
989 let row = |c: &[f32], x: usize| c[50 * 200 + x];
990 assert!(row(&c, 55) > 0.99 && row(&c, 105) > 0.99);
992 assert!(row(&c, 115) < 0.01);
993 assert_eq!(ov[0].parts_at(4).len(), 1);
995 assert!(row(&coverage(&ov, 20), 145) > 0.99);
996 }
997
998 #[test]
999 fn slide_by_word_staggers() {
1000 let ov = run(
1001 json!({"animation": {"in": {"kind": "slide", "by": "word", "duration": 1, "stagger": 0.5}}}),
1002 30,
1003 );
1004 assert_eq!(ov[0].parts_at(2).len(), 1, "the second word not yet");
1005 let p = ov[0].parts_at(7);
1006 assert_eq!(p.len(), 2, "two words");
1007 assert!(p[0].pos[1] < p[1].pos[1]);
1009 assert!(p[0].colour[3] > p[1].colour[3]);
1010 assert!(p[0].pos[1] > 40.0, "still below its place");
1011 let p = ov[0].parts_at(20);
1013 assert_eq!(p.len(), 1);
1014 assert_eq!(p[0].pos, [50.0, 40.0]);
1015 }
1016
1017 #[test]
1018 fn pop_scales_about_the_centre_and_overshoots() {
1019 let ov = run(
1020 json!({"animation": {"in": {"kind": "pop", "duration": 1}}}),
1021 30,
1022 );
1023 let size = |f: i64| ov[0].parts_at(f)[0].size.unwrap();
1024 let centre = |f: i64| {
1025 let p = ov[0].parts_at(f)[0];
1026 p.pos[0] + p.size.unwrap()[0] / 2.0
1027 };
1028 assert!(size(1)[0] < 100.0);
1029 assert!(size(7)[0] > 100.0, "overshoot");
1030 assert!((centre(1) - 100.0).abs() < 1e-3);
1031 assert!((size(15)[0] - 100.0).abs() < 1e-3);
1032 }
1033
1034 #[test]
1035 fn wipe_and_exit() {
1036 let ov = run(
1037 json!({"animation": {"in": {"kind": "wipe", "duration": 1, "ease": "linear"},
1038 "out": {"kind": "slide", "direction": "down", "duration": 1}}}),
1039 30,
1040 );
1041 let p = ov[0].parts_at(4)[0];
1042 assert_eq!(p.src.map(|s| s[2]), Some(45.0));
1043 assert_eq!(p.size.map(|s| s[0]), Some(45.0));
1044 let p = ov[0].parts_at(25)[0];
1045 assert!(p.pos[1] > 40.0 && p.colour[3] < 1.0, "leaving downwards");
1046 }
1047
1048 #[test]
1049 fn keyframes_move_scale_and_tint() {
1050 let a = Animation::parse(&obj(json!({"keyframes": {
1051 "position": [{"t": 0, "v": [0.0, 0.0]}, {"t": 1, "v": [0.25, 0.0]}],
1052 "scale": [{"t": 0, "v": 1}, {"t": 1, "v": 2}],
1053 "opacity": [{"t": 0, "v": 0.5}],
1054 "colour": [{"t": 0, "v": [1, 0, 0]}],
1055 }})));
1056 let r = text_box();
1057 let mut big = r.clone();
1059 big.layers[0].parts[0].pos = [0.0, 30.0];
1060 big.layers[0].parts[0].src = None;
1061 big.layers[0].mask = Arc::new(Mask {
1062 width: 200,
1063 height: 40,
1064 data: vec![255; 8000],
1065 });
1066 let ov = animate(
1067 &big,
1068 &a,
1069 2.0,
1070 [100.0, 50.0],
1071 [0.0, 0.0],
1072 [200.0, 100.0],
1073 span(20),
1074 );
1075 let p = ov[0].parts_at(0)[0];
1076 assert!((p.size.unwrap()[0] - 105.0).abs() < 1e-3, "{p:?}");
1077 let p = ov[0].parts_at(15)[0];
1078 assert!((p.size.unwrap()[0] - 200.0).abs() < 1e-3);
1080 assert!((p.pos[0] - 50.0).abs() < 1e-3);
1081 assert_eq!(p.colour, [1.0, 0.0, 0.0, 0.5]);
1082 let _ = r;
1083 }
1084
1085 #[test]
1086 fn rect_wipes_from_the_centre() {
1087 let r = rect(
1088 [100.0, 4.0],
1089 [0.0, 0.0],
1090 [0.5, 0.5],
1091 [200.0, 100.0],
1092 [1.0; 4],
1093 );
1094 let a = Animation::parse(&obj(
1095 json!({"animation": {"in": {"kind": "wipe", "direction": "center", "duration": 1, "ease": "linear"}}}),
1096 ));
1097 let ov = animate(
1098 &r,
1099 &a,
1100 1.0,
1101 [100.0, 50.0],
1102 [0.0, 0.0],
1103 [200.0, 100.0],
1104 span(20),
1105 );
1106 let p = ov[0].parts_at(4)[0];
1107 assert_eq!(p.pos, [77.5, 48.0]);
1108 assert_eq!(p.size, Some([45.0, 4.0]));
1109 assert_eq!(p.src, None);
1110 }
1111
1112 #[test]
1113 fn keyframes_take_expressions() {
1114 let o: Keys<1> = Keys::parse(Some(&json!("lt / dur")));
1115 assert_eq!(o.at(2.5, 10.0), Some([0.25]));
1116 let p: Keys<2> = Keys::parse(Some(&json!([0.1, "0.3 - 0.05 * lt"])));
1117 let v = p.at(2.0, 10.0).unwrap();
1118 assert!((v[0] - 0.1).abs() < 1e-12 && (v[1] - 0.2).abs() < 1e-12);
1119 let k: Keys<1> = Keys::parse(Some(&json!({
1120 "keys": [{"t": 0, "v": 0}, {"t": -1, "v": 1}], "expr": "value * 0.5"
1121 })));
1122 assert_eq!(k.at(4.5, 10.0), Some([0.25]));
1123 assert!(Keys::<1>::parse(Some(&json!("st * 2"))).is_empty());
1125 }
1126
1127 #[test]
1128 fn keyframes_stay_lenient() {
1129 let o: Keys<1> = Keys::parse(Some(&json!([{"t": 0, "value": 0}, {"t": 2, "value": 1}])));
1131 assert_eq!(o.at(1.0, 10.0), Some([0.5]));
1132 let o: Keys<1> = Keys::parse(Some(&json!([
1134 {"t": "soon", "v": 5}, {"t": 0, "v": 0}, {"t": 2, "v": 1}
1135 ])));
1136 assert_eq!(o.at(1.0, 10.0), Some([0.5]));
1137 let c: Keys<4> = Keys::parse(Some(&json!(["lt / dur", 0.5, 0.5, 1])));
1139 assert_eq!(c.at(2.5, 10.0), Some([0.25, 0.5, 0.5, 1.0]));
1140 let c: Keys<4> = Keys::parse(Some(&json!(["lt / dur", 0.5, 0.5])));
1141 assert_eq!(c.at(2.5, 10.0), Some([0.25, 0.5, 0.5, 1.0]));
1142 }
1143
1144 #[test]
1145 fn expression_scale_rasterises_at_its_peak() {
1146 let a = Animation::parse(&obj(json!({"keyframes": {"scale": ["1 + lt / dur", 1]}})));
1147 assert!((a.raster_scale_over(4.0, 30.0) - 2.0).abs() < 1e-6);
1148 let a = Animation::parse(&obj(json!({"keyframes": {"scale": [
1149 "1 + 2 * between(lt, 1, 1.1)", 1
1150 ]}})));
1151 assert!((a.raster_scale_over(4.0, 30.0) - 3.0).abs() < 1e-6);
1152 let a = Animation::parse(&obj(json!({"keyframes": {"scale": {
1153 "keys": [{"t": 0, "v": 1}, {"t": -1, "v": 2}], "expr": "value * 1.5"
1154 }}})));
1155 assert!((a.raster_scale_over(4.0, 30.0) - 3.0).abs() < 1e-6);
1156 let a = Animation::parse(&obj(json!({"keyframes": {"scale": 10}})));
1158 assert!((a.raster_scale_over(4.0, 30.0) - 4.0).abs() < 1e-6);
1159 }
1160}