Skip to main content

alpha2_eval/
shapes.rs

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