1use crate::animation::{opaque, unit_scale, valid_keyframes, visit_keyed, visit_keyed_point};
10use crate::checks::{
11 ValueIssue, ValueProblem, any, non_negative, point, scalar, unit, validate_paint, vec2,
12};
13use crate::{Animated, AnimatedPoint, Paint, Vec2};
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17pub const MAX_SHAPE_ITEMS: usize = 1_024;
19pub const MAX_SHAPE_DEPTH: usize = 32;
21pub const MAX_PATH_VERTICES: usize = 4_096;
23pub const MAX_PATH_SUBPATHS: usize = 256;
25pub const MAX_DASH_ENTRIES: usize = 16;
27pub const MAX_POLYSTAR_POINTS: f32 = 100.0;
30pub const MIN_POLYSTAR_POINTS: f32 = 3.0;
32
33#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
42pub enum ShapeItem {
43 Group {
46 items: Vec<ShapeItem>,
48 #[serde(default)]
50 transform: ShapeTransform,
51 },
52 Rectangle(RectangleGeometry),
54 Ellipse(EllipseGeometry),
56 Polystar(PolystarGeometry),
58 Path(PathGeometry),
60 Fill {
63 paint: Animated<Paint>,
65 #[serde(default = "opaque")]
67 opacity: Animated<f32>,
68 #[serde(default)]
70 rule: FillRule,
71 },
72 Stroke(StrokeItem),
74 Trim(TrimItem),
76}
77
78fn zero() -> Animated<f32> {
79 Animated::Constant { value: 0.0 }
80}
81
82#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
84#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
85#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
86pub enum ShapeGeometry {
87 Rectangle(RectangleGeometry),
89 Ellipse(EllipseGeometry),
91 Polystar(PolystarGeometry),
93 Path(PathGeometry),
95}
96
97#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101#[serde(deny_unknown_fields)]
102pub struct RectangleGeometry {
103 pub center: AnimatedPoint,
105 pub size: Animated<Vec2>,
108 #[serde(default = "zero")]
111 pub corner_radius: Animated<f32>,
112 #[serde(default = "zero")]
114 pub rotation_degrees: Animated<f32>,
115}
116
117#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
120#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
121#[serde(deny_unknown_fields)]
122pub struct EllipseGeometry {
123 pub center: AnimatedPoint,
125 pub size: Animated<Vec2>,
128 #[serde(default = "zero")]
130 pub rotation_degrees: Animated<f32>,
131}
132
133#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
136#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
137#[serde(deny_unknown_fields)]
138pub struct PolystarGeometry {
139 pub kind: PolystarKind,
141 pub center: AnimatedPoint,
143 pub points: Animated<f32>,
147 pub outer_radius: Animated<f32>,
149 #[serde(default = "zero")]
151 pub inner_radius: Animated<f32>,
152 #[serde(default = "zero")]
154 pub outer_roundness: Animated<f32>,
155 #[serde(default = "zero")]
157 pub inner_roundness: Animated<f32>,
158 #[serde(default = "zero")]
160 pub rotation_degrees: Animated<f32>,
161}
162
163#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
166#[serde(rename_all = "snake_case")]
167pub enum PolystarKind {
168 #[default]
170 Star,
171 Polygon,
173}
174
175#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
181#[serde(deny_unknown_fields)]
182pub struct PathGeometry {
183 pub path: Animated<Path>,
185}
186
187#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
189#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
190#[serde(deny_unknown_fields)]
191pub struct Path {
192 pub subpaths: Vec<Subpath>,
195}
196
197#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
199#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
200#[serde(deny_unknown_fields)]
201pub struct Subpath {
202 #[serde(default)]
204 pub closed: bool,
205 pub vertices: Vec<PathVertex>,
207}
208
209#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
213#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
214#[serde(deny_unknown_fields)]
215pub struct PathVertex {
216 pub point: Vec2,
218 #[serde(default)]
220 pub in_tangent: Vec2,
221 #[serde(default)]
223 pub out_tangent: Vec2,
224}
225
226#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
230#[serde(deny_unknown_fields)]
231pub struct ShapeTransform {
232 #[serde(default = "origin")]
234 pub anchor: Animated<Vec2>,
235 #[serde(default)]
237 pub position: AnimatedPoint,
238 #[serde(default = "unit_scale")]
240 pub scale: Animated<Vec2>,
241 #[serde(default = "zero")]
243 pub rotation_degrees: Animated<f32>,
244 #[serde(default = "opaque")]
247 pub opacity: Animated<f32>,
248}
249
250fn origin() -> Animated<Vec2> {
251 Animated::Constant {
252 value: Vec2::new(0.0, 0.0),
253 }
254}
255
256impl Default for ShapeTransform {
257 fn default() -> Self {
258 Self {
259 anchor: origin(),
260 position: AnimatedPoint::default(),
261 scale: unit_scale(),
262 rotation_degrees: zero(),
263 opacity: opaque(),
264 }
265 }
266}
267
268#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
270#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
271#[serde(deny_unknown_fields)]
272pub struct StrokeItem {
273 pub paint: Animated<Paint>,
275 pub width: Animated<f32>,
277 #[serde(default = "opaque")]
279 pub opacity: Animated<f32>,
280 #[serde(default)]
282 pub join: LineJoin,
283 #[serde(default = "four")]
286 pub miter_limit: f32,
287 #[serde(default)]
289 pub cap: LineCap,
290 #[serde(default)]
292 pub dash: Option<Dash>,
293}
294
295const fn four() -> f32 {
296 4.0
297}
298
299#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302#[serde(rename_all = "snake_case")]
303pub enum LineJoin {
304 #[default]
306 Miter,
307 Round,
309 Bevel,
311}
312
313#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
315#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
316#[serde(rename_all = "snake_case")]
317pub enum LineCap {
318 #[default]
320 Butt,
321 Round,
323 Square,
325}
326
327#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
330#[serde(deny_unknown_fields)]
331pub struct Dash {
332 pub pattern: Vec<Animated<f32>>,
336 #[serde(default = "zero")]
338 pub offset: Animated<f32>,
339}
340
341#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
343#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
344#[serde(deny_unknown_fields)]
345pub struct TrimItem {
346 pub start: Animated<f32>,
348 pub end: Animated<f32>,
350 #[serde(default = "zero")]
352 pub offset: Animated<f32>,
353 #[serde(default)]
355 pub mode: TrimMode,
356}
357
358#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
361#[serde(rename_all = "snake_case")]
362pub enum TrimMode {
363 #[default]
365 Together,
366 Individually,
368}
369
370#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
372#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
373#[serde(rename_all = "snake_case")]
374pub enum FillRule {
375 #[default]
377 Nonzero,
378 Evenodd,
380}
381
382#[derive(Copy, Clone, Debug, Eq, PartialEq)]
384#[non_exhaustive]
385pub enum ShapeProblem {
386 TooManyItems,
388 TooDeep,
390 PathTooLarge,
392 InvalidDash,
394 InvalidKeyframes,
396 OutOfRange,
398 PathKeyframesDiffer,
400 PaintKeyframesDiffer,
402}
403
404impl fmt::Display for ShapeProblem {
405 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
406 formatter.write_str(match self {
407 Self::TooManyItems => "has more items than a shapes generator may hold",
408 Self::TooDeep => "nests groups too deeply",
409 Self::PathTooLarge => "has more subpaths or vertices than a path may hold",
410 Self::InvalidDash => "has an invalid dash pattern",
411 Self::InvalidKeyframes => "has invalid keyframes",
412 Self::OutOfRange => "has a value out of range",
413 Self::PathKeyframesDiffer => "has path keyframes that differ in structure",
414 Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
415 })
416 }
417}
418
419#[derive(Clone, Debug, Eq, PartialEq)]
421pub struct ShapeIssue {
422 pub at: String,
425 pub problem: ShapeProblem,
427}
428
429impl From<ValueIssue> for ShapeIssue {
430 fn from(issue: ValueIssue) -> Self {
431 Self::new(
432 issue.at,
433 match issue.problem {
434 ValueProblem::InvalidKeyframes => ShapeProblem::InvalidKeyframes,
435 ValueProblem::OutOfRange => ShapeProblem::OutOfRange,
436 ValueProblem::PaintKeyframesDiffer => ShapeProblem::PaintKeyframesDiffer,
437 },
438 )
439 }
440}
441
442impl ShapeIssue {
443 fn new(at: impl Into<String>, problem: ShapeProblem) -> Self {
444 Self {
445 at: at.into(),
446 problem,
447 }
448 }
449}
450
451type Checked = Result<(), ShapeIssue>;
452
453pub fn validate_shape_items(items: &[ShapeItem]) -> Result<(), ShapeIssue> {
459 let mut count = 0;
460 validate_items(items, "items", 1, &mut count)
461}
462
463pub(crate) fn validate_items(
464 items: &[ShapeItem],
465 at: &str,
466 depth: usize,
467 count: &mut usize,
468) -> Checked {
469 if depth > MAX_SHAPE_DEPTH {
470 return Err(ShapeIssue::new(at, ShapeProblem::TooDeep));
471 }
472 for (index, item) in items.iter().enumerate() {
473 let at = format!("{at}[{index}]");
474 *count += 1;
475 if *count > MAX_SHAPE_ITEMS {
476 return Err(ShapeIssue::new(at, ShapeProblem::TooManyItems));
477 }
478 match item {
479 ShapeItem::Group { items, transform } => {
480 validate_transform(transform, &format!("{at}.transform"))?;
481 validate_items(items, &format!("{at}.items"), depth + 1, count)?;
482 }
483 ShapeItem::Rectangle(geometry) => validate_rectangle(geometry, &at)?,
484 ShapeItem::Ellipse(geometry) => validate_ellipse(geometry, &at)?,
485 ShapeItem::Polystar(geometry) => validate_polystar(geometry, &at)?,
486 ShapeItem::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path"))?,
487 ShapeItem::Fill { paint, opacity, .. } => {
488 validate_paint(paint, &format!("{at}.paint"))?;
489 scalar(opacity, &format!("{at}.opacity"), unit)?;
490 }
491 ShapeItem::Stroke(stroke) => validate_stroke(stroke, &at)?,
492 ShapeItem::Trim(trim) => {
493 scalar(&trim.start, &format!("{at}.start"), unit)?;
494 scalar(&trim.end, &format!("{at}.end"), unit)?;
495 scalar(&trim.offset, &format!("{at}.offset"), any)?;
496 }
497 }
498 }
499 Ok(())
500}
501
502impl ShapeGeometry {
503 pub fn validate(&self, at: &str) -> Result<(), ShapeIssue> {
508 match self {
509 Self::Rectangle(geometry) => validate_rectangle(geometry, at),
510 Self::Ellipse(geometry) => validate_ellipse(geometry, at),
511 Self::Polystar(geometry) => validate_polystar(geometry, at),
512 Self::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path")),
513 }
514 }
515}
516
517fn sizes(value: Vec2) -> bool {
518 value.x >= 0.0 && value.y >= 0.0
519}
520
521fn validate_rectangle(geometry: &RectangleGeometry, at: &str) -> Checked {
522 point(&geometry.center, &format!("{at}.center"))?;
523 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
524 scalar(
525 &geometry.corner_radius,
526 &format!("{at}.corner_radius"),
527 non_negative,
528 )?;
529 scalar(
530 &geometry.rotation_degrees,
531 &format!("{at}.rotation_degrees"),
532 any,
533 )?;
534 Ok(())
535}
536
537fn validate_ellipse(geometry: &EllipseGeometry, at: &str) -> Checked {
538 point(&geometry.center, &format!("{at}.center"))?;
539 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
540 scalar(
541 &geometry.rotation_degrees,
542 &format!("{at}.rotation_degrees"),
543 any,
544 )?;
545 Ok(())
546}
547
548fn validate_polystar(geometry: &PolystarGeometry, at: &str) -> Checked {
549 point(&geometry.center, &format!("{at}.center"))?;
550 scalar(&geometry.points, &format!("{at}.points"), |points| {
551 (MIN_POLYSTAR_POINTS..=MAX_POLYSTAR_POINTS).contains(&points)
552 })?;
553 scalar(
554 &geometry.outer_radius,
555 &format!("{at}.outer_radius"),
556 non_negative,
557 )?;
558 scalar(
559 &geometry.inner_radius,
560 &format!("{at}.inner_radius"),
561 non_negative,
562 )?;
563 scalar(
564 &geometry.outer_roundness,
565 &format!("{at}.outer_roundness"),
566 unit,
567 )?;
568 scalar(
569 &geometry.inner_roundness,
570 &format!("{at}.inner_roundness"),
571 unit,
572 )?;
573 scalar(
574 &geometry.rotation_degrees,
575 &format!("{at}.rotation_degrees"),
576 any,
577 )?;
578 Ok(())
579}
580
581fn validate_transform(transform: &ShapeTransform, at: &str) -> Checked {
582 vec2(&transform.anchor, &format!("{at}.anchor"), |_| true)?;
583 point(&transform.position, &format!("{at}.position"))?;
584 vec2(&transform.scale, &format!("{at}.scale"), |_| true)?;
585 scalar(
586 &transform.rotation_degrees,
587 &format!("{at}.rotation_degrees"),
588 any,
589 )?;
590 scalar(&transform.opacity, &format!("{at}.opacity"), unit)?;
591 Ok(())
592}
593
594fn validate_stroke(stroke: &StrokeItem, at: &str) -> Checked {
595 validate_paint(&stroke.paint, &format!("{at}.paint"))?;
596 scalar(&stroke.width, &format!("{at}.width"), non_negative)?;
597 scalar(&stroke.opacity, &format!("{at}.opacity"), unit)?;
598 if !stroke.miter_limit.is_finite() || stroke.miter_limit < 1.0 {
599 return Err(ShapeIssue::new(
600 format!("{at}.miter_limit"),
601 ShapeProblem::OutOfRange,
602 ));
603 }
604 if let Some(dash) = &stroke.dash {
605 let at = format!("{at}.dash");
606 if dash.pattern.is_empty() || dash.pattern.len() > MAX_DASH_ENTRIES {
607 return Err(ShapeIssue::new(
608 format!("{at}.pattern"),
609 ShapeProblem::InvalidDash,
610 ));
611 }
612 for (index, entry) in dash.pattern.iter().enumerate() {
613 scalar(entry, &format!("{at}.pattern[{index}]"), non_negative)?;
614 }
615 let positive = dash.pattern.iter().any(|entry| match entry {
618 Animated::Constant { value } => *value > 0.0,
619 Animated::Keyframes { keyframes } => keyframes.iter().any(|key| key.value > 0.0),
620 });
621 if !positive {
622 return Err(ShapeIssue::new(
623 format!("{at}.pattern"),
624 ShapeProblem::InvalidDash,
625 ));
626 }
627 scalar(&dash.offset, &format!("{at}.offset"), any)?;
628 }
629 Ok(())
630}
631
632fn validate_path(path: &Animated<Path>, at: &str) -> Checked {
633 let check_one = |path: &Path| -> Checked {
634 let vertices: usize = path
635 .subpaths
636 .iter()
637 .map(|subpath| subpath.vertices.len())
638 .sum();
639 if path.subpaths.len() > MAX_PATH_SUBPATHS || vertices > MAX_PATH_VERTICES {
640 return Err(ShapeIssue::new(at, ShapeProblem::PathTooLarge));
641 }
642 let finite = |value: Vec2| value.x.is_finite() && value.y.is_finite();
643 let all_finite = path.subpaths.iter().all(|subpath| {
644 subpath.vertices.iter().all(|vertex| {
645 finite(vertex.point) && finite(vertex.in_tangent) && finite(vertex.out_tangent)
646 })
647 });
648 all_finite
649 .then_some(())
650 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
651 };
652 match path {
653 Animated::Constant { value } => check_one(value),
654 Animated::Keyframes { keyframes } => {
655 if !valid_keyframes(keyframes) {
656 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
657 }
658 for keyframe in keyframes {
659 check_one(&keyframe.value)?;
660 }
661 let structure = |path: &Path| {
662 path.subpaths
663 .iter()
664 .map(|subpath| (subpath.closed, subpath.vertices.len()))
665 .collect::<Vec<_>>()
666 };
667 let first = structure(&keyframes[0].value);
668 if keyframes[1..]
669 .iter()
670 .any(|keyframe| structure(&keyframe.value) != first)
671 {
672 return Err(ShapeIssue::new(at, ShapeProblem::PathKeyframesDiffer));
673 }
674 Ok(())
675 }
676 }
677}
678
679impl Path {
680 #[must_use]
682 pub fn vertex_count(&self) -> usize {
683 self.subpaths
684 .iter()
685 .map(|subpath| subpath.vertices.len())
686 .sum()
687 }
688}
689
690impl ShapeItem {
691 #[must_use]
693 pub fn is_animated(&self) -> bool {
694 match self {
695 Self::Group { items, transform } => {
696 transform.is_animated() || items.iter().any(Self::is_animated)
697 }
698 Self::Rectangle(geometry) => geometry.is_animated(),
699 Self::Ellipse(geometry) => geometry.is_animated(),
700 Self::Polystar(geometry) => geometry.is_animated(),
701 Self::Path(geometry) => keyed(&geometry.path),
702 Self::Fill { paint, opacity, .. } => keyed(paint) || keyed(opacity),
703 Self::Stroke(stroke) => {
704 keyed(&stroke.paint)
705 || keyed(&stroke.width)
706 || keyed(&stroke.opacity)
707 || stroke
708 .dash
709 .as_ref()
710 .is_some_and(|dash| dash.pattern.iter().any(keyed) || keyed(&dash.offset))
711 }
712 Self::Trim(trim) => keyed(&trim.start) || keyed(&trim.end) || keyed(&trim.offset),
713 }
714 }
715}
716
717pub(crate) fn for_each_keyed_item(
720 items: &[ShapeItem],
721 at: &str,
722 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
723) {
724 for (index, item) in items.iter().enumerate() {
725 item.for_each_keyed(&format!("{at}[{index}]"), visit);
726 }
727}
728
729impl ShapeItem {
730 pub(crate) fn for_each_keyed(
732 &self,
733 at: &str,
734 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
735 ) {
736 match self {
737 Self::Group { items, transform } => {
738 let transform_at = format!("{at}.transform");
739 visit_keyed(&format!("{transform_at}.anchor"), &transform.anchor, visit);
740 visit_keyed_point(
741 &format!("{transform_at}.position"),
742 &transform.position,
743 visit,
744 );
745 visit_keyed(&format!("{transform_at}.scale"), &transform.scale, visit);
746 visit_keyed(
747 &format!("{transform_at}.rotation_degrees"),
748 &transform.rotation_degrees,
749 visit,
750 );
751 visit_keyed(
752 &format!("{transform_at}.opacity"),
753 &transform.opacity,
754 visit,
755 );
756 for_each_keyed_item(items, &format!("{at}.items"), visit);
757 }
758 Self::Rectangle(geometry) => geometry.for_each_keyed(at, visit),
759 Self::Ellipse(geometry) => geometry.for_each_keyed(at, visit),
760 Self::Polystar(geometry) => geometry.for_each_keyed(at, visit),
761 Self::Path(geometry) => visit_keyed(&format!("{at}.path"), &geometry.path, visit),
762 Self::Fill { paint, opacity, .. } => {
763 visit_keyed(&format!("{at}.paint"), paint, visit);
764 visit_keyed(&format!("{at}.opacity"), opacity, visit);
765 }
766 Self::Stroke(stroke) => {
767 visit_keyed(&format!("{at}.paint"), &stroke.paint, visit);
768 visit_keyed(&format!("{at}.width"), &stroke.width, visit);
769 visit_keyed(&format!("{at}.opacity"), &stroke.opacity, visit);
770 if let Some(dash) = &stroke.dash {
771 for (index, entry) in dash.pattern.iter().enumerate() {
772 visit_keyed(&format!("{at}.dash.pattern[{index}]"), entry, visit);
773 }
774 visit_keyed(&format!("{at}.dash.offset"), &dash.offset, visit);
775 }
776 }
777 Self::Trim(trim) => {
778 visit_keyed(&format!("{at}.start"), &trim.start, visit);
779 visit_keyed(&format!("{at}.end"), &trim.end, visit);
780 visit_keyed(&format!("{at}.offset"), &trim.offset, visit);
781 }
782 }
783 }
784}
785
786impl ShapeGeometry {
787 pub(crate) fn for_each_keyed(
789 &self,
790 at: &str,
791 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
792 ) {
793 match self {
794 Self::Rectangle(geometry) => geometry.for_each_keyed(at, visit),
795 Self::Ellipse(geometry) => geometry.for_each_keyed(at, visit),
796 Self::Polystar(geometry) => geometry.for_each_keyed(at, visit),
797 Self::Path(geometry) => visit_keyed(&format!("{at}.path"), &geometry.path, visit),
798 }
799 }
800
801 #[must_use]
803 pub fn is_animated(&self) -> bool {
804 match self {
805 Self::Rectangle(geometry) => geometry.is_animated(),
806 Self::Ellipse(geometry) => geometry.is_animated(),
807 Self::Polystar(geometry) => geometry.is_animated(),
808 Self::Path(geometry) => keyed(&geometry.path),
809 }
810 }
811}
812
813impl RectangleGeometry {
814 fn is_animated(&self) -> bool {
815 self.center.is_animated()
816 || keyed(&self.size)
817 || keyed(&self.corner_radius)
818 || keyed(&self.rotation_degrees)
819 }
820}
821
822impl EllipseGeometry {
823 fn is_animated(&self) -> bool {
824 self.center.is_animated() || keyed(&self.size) || keyed(&self.rotation_degrees)
825 }
826}
827
828impl RectangleGeometry {
829 fn for_each_keyed(&self, at: &str, visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime])) {
830 visit_keyed_point(&format!("{at}.center"), &self.center, visit);
831 visit_keyed(&format!("{at}.size"), &self.size, visit);
832 visit_keyed(&format!("{at}.corner_radius"), &self.corner_radius, visit);
833 visit_keyed(
834 &format!("{at}.rotation_degrees"),
835 &self.rotation_degrees,
836 visit,
837 );
838 }
839}
840
841impl EllipseGeometry {
842 fn for_each_keyed(&self, at: &str, visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime])) {
843 visit_keyed_point(&format!("{at}.center"), &self.center, visit);
844 visit_keyed(&format!("{at}.size"), &self.size, visit);
845 visit_keyed(
846 &format!("{at}.rotation_degrees"),
847 &self.rotation_degrees,
848 visit,
849 );
850 }
851}
852
853impl PolystarGeometry {
854 fn for_each_keyed(&self, at: &str, visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime])) {
855 visit_keyed_point(&format!("{at}.center"), &self.center, visit);
856 visit_keyed(&format!("{at}.points"), &self.points, visit);
857 visit_keyed(&format!("{at}.outer_radius"), &self.outer_radius, visit);
858 visit_keyed(&format!("{at}.inner_radius"), &self.inner_radius, visit);
859 visit_keyed(
860 &format!("{at}.outer_roundness"),
861 &self.outer_roundness,
862 visit,
863 );
864 visit_keyed(
865 &format!("{at}.inner_roundness"),
866 &self.inner_roundness,
867 visit,
868 );
869 visit_keyed(
870 &format!("{at}.rotation_degrees"),
871 &self.rotation_degrees,
872 visit,
873 );
874 }
875
876 fn is_animated(&self) -> bool {
877 self.center.is_animated()
878 || keyed(&self.points)
879 || keyed(&self.outer_radius)
880 || keyed(&self.inner_radius)
881 || keyed(&self.outer_roundness)
882 || keyed(&self.inner_roundness)
883 || keyed(&self.rotation_degrees)
884 }
885}
886
887impl ShapeTransform {
888 fn is_animated(&self) -> bool {
889 keyed(&self.anchor)
890 || self.position.is_animated()
891 || keyed(&self.scale)
892 || keyed(&self.rotation_degrees)
893 || keyed(&self.opacity)
894 }
895}
896
897const fn keyed<T>(animation: &Animated<T>) -> bool {
898 matches!(animation, Animated::Keyframes { .. })
899}