Skip to main content

alpha2_eval/
shapes.rs

1//! A shapes generator's content tree evaluated at a time.
2//!
3//! A [`ShapesPlan`] is built with the index and pre-evaluates every subtree without keyframes,
4//! so each frame evaluates only what animates.
5
6use crate::animation::{sample_paint, sample_path, sample_point, sample_scalar, sample_vec2};
7use alpha2_geometry::kurbo::{Point, Vec2};
8use alpha2_geometry::{EvaluatedGeometry, EvaluatedItem, EvaluatedTransform};
9use alpha2_model::{
10    Animated, AnimatedPoint, EllipseGeometry, PathGeometry, PolystarGeometry, RectangleGeometry,
11    ShapeGeometry, ShapeItem, ShapeTransform,
12};
13use alpha2_time::MediaTime;
14use std::sync::Arc;
15
16/// A content tree with its static parts evaluated once.
17#[derive(Clone, Debug, PartialEq)]
18pub struct ShapesPlan {
19    items: Vec<Planned>,
20    /// The whole tree, when nothing in it animates.
21    fixed: Option<Arc<[EvaluatedItem]>>,
22}
23
24#[derive(Clone, Debug, PartialEq)]
25enum Planned {
26    Fixed(EvaluatedItem),
27    /// A group with keyframes somewhere inside.
28    Group {
29        items: Vec<Planned>,
30        fixed: Option<Arc<[EvaluatedItem]>>,
31        transform: ShapeTransform,
32    },
33    Live(ShapeItem),
34}
35
36impl ShapesPlan {
37    /// Plans `items`, evaluating every subtree that does not animate.
38    #[must_use]
39    pub fn new(items: &[ShapeItem]) -> Self {
40        let items: Vec<Planned> = items.iter().map(plan).collect();
41        let fixed = all_fixed(&items);
42        Self { items, fixed }
43    }
44
45    /// The tree at `time`. `aspect` (width per unit height) shapes motion along spatial curves.
46    #[must_use]
47    pub fn evaluate(&self, time: MediaTime, aspect: f64) -> Arc<[EvaluatedItem]> {
48        if let Some(fixed) = &self.fixed {
49            return Arc::clone(fixed);
50        }
51        evaluate_planned(&self.items, time, aspect)
52    }
53
54    /// Whether anything in the tree animates.
55    #[must_use]
56    pub fn is_animated(&self) -> bool {
57        self.fixed.is_none()
58    }
59}
60
61fn plan(item: &ShapeItem) -> Planned {
62    if !item.is_animated() {
63        return Planned::Fixed(evaluate_item(item, MediaTime::ZERO, 1.0));
64    }
65    match item {
66        ShapeItem::Group { items, transform } => {
67            let items: Vec<Planned> = items.iter().map(plan).collect();
68            let fixed = all_fixed(&items);
69            Planned::Group {
70                items: if fixed.is_some() { Vec::new() } else { items },
71                fixed,
72                transform: transform.clone(),
73            }
74        }
75        _ => Planned::Live(item.clone()),
76    }
77}
78
79fn all_fixed(items: &[Planned]) -> Option<Arc<[EvaluatedItem]>> {
80    items
81        .iter()
82        .map(|item| match item {
83            Planned::Fixed(item) => Some(item.clone()),
84            _ => None,
85        })
86        .collect::<Option<Vec<_>>>()
87        .map(Arc::from)
88}
89
90fn evaluate_planned(items: &[Planned], time: MediaTime, aspect: f64) -> Arc<[EvaluatedItem]> {
91    items
92        .iter()
93        .map(|item| match item {
94            Planned::Fixed(item) => item.clone(),
95            Planned::Group {
96                items,
97                fixed,
98                transform,
99            } => EvaluatedItem::Group {
100                items: fixed
101                    .clone()
102                    .unwrap_or_else(|| evaluate_planned(items, time, aspect)),
103                transform: evaluate_transform(transform, time, aspect),
104            },
105            Planned::Live(item) => evaluate_item(item, time, aspect),
106        })
107        .collect()
108}
109
110/// A whole content tree at `time`, without a plan.
111#[must_use]
112pub fn evaluate_items(items: &[ShapeItem], time: MediaTime, aspect: f64) -> Arc<[EvaluatedItem]> {
113    items
114        .iter()
115        .map(|item| evaluate_item(item, time, aspect))
116        .collect()
117}
118
119fn scalar(animation: &Animated<f32>, time: MediaTime) -> f64 {
120    f64::from(sample_scalar(animation, time))
121}
122
123fn point(animation: &AnimatedPoint, time: MediaTime, aspect: f64) -> Point {
124    let value = sample_point(animation, time, aspect);
125    Point::new(f64::from(value.x), f64::from(value.y))
126}
127
128fn vector(animation: &Animated<alpha2_model::Vec2>, time: MediaTime) -> Vec2 {
129    let value = sample_vec2(animation, time);
130    Vec2::new(f64::from(value.x), f64::from(value.y))
131}
132
133/// One item at `time`. Opacities, trims and roundness clamp to `0..=1` and lengths to `>= 0`,
134/// since an overshooting curve can carry them past.
135#[must_use]
136pub fn evaluate_item(item: &ShapeItem, time: MediaTime, aspect: f64) -> EvaluatedItem {
137    let unit = |value: f64| value.clamp(0.0, 1.0);
138    match item {
139        ShapeItem::Group { items, transform } => EvaluatedItem::Group {
140            items: evaluate_items(items, time, aspect),
141            transform: evaluate_transform(transform, time, aspect),
142        },
143        ShapeItem::Rectangle(geometry) => EvaluatedItem::Geometry(evaluate_geometry(
144            &ShapeGeometry::Rectangle(geometry.clone()),
145            time,
146            aspect,
147        )),
148        ShapeItem::Ellipse(geometry) => EvaluatedItem::Geometry(ellipse(geometry, time, aspect)),
149        ShapeItem::Polystar(geometry) => EvaluatedItem::Geometry(polystar(geometry, time, aspect)),
150        ShapeItem::Path(geometry) => EvaluatedItem::Geometry(path(geometry, time)),
151        ShapeItem::Fill {
152            paint,
153            opacity,
154            rule,
155        } => EvaluatedItem::Fill {
156            paint: sample_paint(paint, time),
157            opacity: unit(scalar(opacity, time)),
158            rule: *rule,
159        },
160        ShapeItem::Stroke(stroke) => EvaluatedItem::Stroke {
161            paint: sample_paint(&stroke.paint, time),
162            width: scalar(&stroke.width, time).max(0.0),
163            opacity: unit(scalar(&stroke.opacity, time)),
164            join: stroke.join,
165            miter_limit: f64::from(stroke.miter_limit),
166            cap: stroke.cap,
167            dash: stroke.dash.as_ref().map_or_else(Vec::new, |dash| {
168                dash.pattern
169                    .iter()
170                    .map(|entry| scalar(entry, time).max(0.0))
171                    .collect()
172            }),
173            dash_offset: stroke
174                .dash
175                .as_ref()
176                .map_or(0.0, |dash| scalar(&dash.offset, time)),
177        },
178        ShapeItem::Trim(trim) => EvaluatedItem::Trim {
179            start: unit(scalar(&trim.start, time)),
180            end: unit(scalar(&trim.end, time)),
181            offset: scalar(&trim.offset, time),
182            mode: trim.mode,
183        },
184    }
185}
186
187/// A geometry at `time`: a mask's outline, or a content tree's.
188#[must_use]
189pub fn evaluate_geometry(
190    geometry: &ShapeGeometry,
191    time: MediaTime,
192    aspect: f64,
193) -> EvaluatedGeometry {
194    match geometry {
195        ShapeGeometry::Rectangle(RectangleGeometry {
196            center,
197            size,
198            corner_radius,
199            rotation_degrees,
200        }) => EvaluatedGeometry::Rectangle {
201            center: point(center, time, aspect),
202            size: vector(size, time),
203            corner_radius: scalar(corner_radius, time).max(0.0),
204            rotation_degrees: scalar(rotation_degrees, time),
205        },
206        ShapeGeometry::Ellipse(geometry) => ellipse(geometry, time, aspect),
207        ShapeGeometry::Polystar(geometry) => polystar(geometry, time, aspect),
208        ShapeGeometry::Path(geometry) => path(geometry, time),
209    }
210}
211
212fn ellipse(geometry: &EllipseGeometry, time: MediaTime, aspect: f64) -> EvaluatedGeometry {
213    EvaluatedGeometry::Ellipse {
214        center: point(&geometry.center, time, aspect),
215        size: vector(&geometry.size, time),
216        rotation_degrees: scalar(&geometry.rotation_degrees, time),
217    }
218}
219
220fn polystar(geometry: &PolystarGeometry, time: MediaTime, aspect: f64) -> EvaluatedGeometry {
221    EvaluatedGeometry::Polystar {
222        kind: geometry.kind,
223        center: point(&geometry.center, time, aspect),
224        points: scalar(&geometry.points, time).clamp(
225            f64::from(alpha2_model::MIN_POLYSTAR_POINTS),
226            f64::from(alpha2_model::MAX_POLYSTAR_POINTS),
227        ),
228        outer_radius: scalar(&geometry.outer_radius, time).max(0.0),
229        inner_radius: scalar(&geometry.inner_radius, time).max(0.0),
230        outer_roundness: scalar(&geometry.outer_roundness, time).clamp(0.0, 1.0),
231        inner_roundness: scalar(&geometry.inner_roundness, time).clamp(0.0, 1.0),
232        rotation_degrees: scalar(&geometry.rotation_degrees, time),
233    }
234}
235
236fn path(geometry: &PathGeometry, time: MediaTime) -> EvaluatedGeometry {
237    EvaluatedGeometry::Path(Arc::new(sample_path(&geometry.path, time)))
238}
239
240fn evaluate_transform(
241    transform: &ShapeTransform,
242    time: MediaTime,
243    aspect: f64,
244) -> EvaluatedTransform {
245    let anchor = vector(&transform.anchor, time);
246    EvaluatedTransform {
247        anchor: Point::new(anchor.x, anchor.y),
248        position: point(&transform.position, time, aspect),
249        scale: vector(&transform.scale, time),
250        rotation_degrees: scalar(&transform.rotation_degrees, time),
251        opacity: scalar(&transform.opacity, time).clamp(0.0, 1.0),
252    }
253}
254
255#[cfg(test)]
256#[path = "shapes_tests.rs"]
257mod tests;