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