1use 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#[derive(Clone, Debug, PartialEq)]
18pub struct ShapesPlan {
19 items: Vec<Planned>,
20 fixed: Option<Arc<[EvaluatedItem]>>,
22}
23
24#[derive(Clone, Debug, PartialEq)]
25enum Planned {
26 Fixed(EvaluatedItem),
27 Group {
29 items: Vec<Planned>,
30 fixed: Option<Arc<[EvaluatedItem]>>,
31 transform: ShapeTransform,
32 },
33 Live(ShapeItem),
34}
35
36impl ShapesPlan {
37 #[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 #[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 #[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#[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#[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#[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;