1use crate::animation::{opaque, unit_scale, valid_keyframes, visit_keyed, visit_keyed_point};
11use crate::checks::{
12 ValueIssue, ValueProblem, any, non_negative, point, scalar, unit, validate_paint, vec2,
13};
14use crate::{Animated, AnimatedPoint, Paint, Vec2};
15use serde::{Deserialize, Serialize};
16use std::fmt;
17
18pub const MAX_SHAPE_ITEMS: usize = 1_024;
21pub const MAX_SHAPE_DEPTH: usize = 32;
23pub const MAX_PATH_VERTICES: usize = 4_096;
25pub const MAX_PATH_SUBPATHS: usize = 256;
27pub const MAX_DASH_ENTRIES: usize = 16;
29pub const MAX_POLYSTAR_POINTS: f32 = 100.0;
32pub const MIN_POLYSTAR_POINTS: f32 = 3.0;
34
35#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
45pub enum ShapeItem {
46 Group {
50 items: Vec<ShapeItem>,
52 #[serde(default)]
54 transform: ShapeTransform,
55 },
56 Rectangle(RectangleGeometry),
58 Ellipse(EllipseGeometry),
60 Polystar(PolystarGeometry),
62 Path(PathGeometry),
64 Fill {
68 paint: Animated<Paint>,
70 #[serde(default = "opaque")]
72 opacity: Animated<f32>,
73 #[serde(default)]
75 rule: FillRule,
76 },
77 Stroke(StrokeItem),
79 Trim(TrimItem),
81}
82
83fn zero() -> Animated<f32> {
84 Animated::Constant { value: 0.0 }
85}
86
87#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
91pub enum ShapeGeometry {
92 Rectangle(RectangleGeometry),
94 Ellipse(EllipseGeometry),
96 Polystar(PolystarGeometry),
98 Path(PathGeometry),
100}
101
102#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(deny_unknown_fields)]
107pub struct RectangleGeometry {
108 pub center: AnimatedPoint,
110 pub size: Animated<Vec2>,
113 #[serde(default = "zero")]
116 pub corner_radius: Animated<f32>,
117 #[serde(default = "zero")]
119 pub rotation_degrees: Animated<f32>,
120}
121
122#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
126#[serde(deny_unknown_fields)]
127pub struct EllipseGeometry {
128 pub center: AnimatedPoint,
130 pub size: Animated<Vec2>,
133 #[serde(default = "zero")]
135 pub rotation_degrees: Animated<f32>,
136}
137
138#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
142#[serde(deny_unknown_fields)]
143pub struct PolystarGeometry {
144 pub kind: PolystarKind,
146 pub center: AnimatedPoint,
148 pub points: Animated<f32>,
153 pub outer_radius: Animated<f32>,
156 #[serde(default = "zero")]
158 pub inner_radius: Animated<f32>,
159 #[serde(default = "zero")]
162 pub outer_roundness: Animated<f32>,
163 #[serde(default = "zero")]
165 pub inner_roundness: Animated<f32>,
166 #[serde(default = "zero")]
168 pub rotation_degrees: Animated<f32>,
169}
170
171#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
174#[serde(rename_all = "snake_case")]
175pub enum PolystarKind {
176 #[default]
178 Star,
179 Polygon,
181}
182
183#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
189#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
190#[serde(deny_unknown_fields)]
191pub struct PathGeometry {
192 pub path: Animated<Path>,
194}
195
196#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
199#[serde(deny_unknown_fields)]
200pub struct Path {
201 pub subpaths: Vec<Subpath>,
204}
205
206#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
208#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209#[serde(deny_unknown_fields)]
210pub struct Subpath {
211 #[serde(default)]
213 pub closed: bool,
214 pub vertices: Vec<PathVertex>,
216}
217
218#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
223#[serde(deny_unknown_fields)]
224pub struct PathVertex {
225 pub point: Vec2,
227 #[serde(default)]
229 pub in_tangent: Vec2,
230 #[serde(default)]
232 pub out_tangent: Vec2,
233}
234
235#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
240#[serde(deny_unknown_fields)]
241pub struct ShapeTransform {
242 #[serde(default = "origin")]
244 pub anchor: Animated<Vec2>,
245 #[serde(default)]
247 pub position: AnimatedPoint,
248 #[serde(default = "unit_scale")]
251 pub scale: Animated<Vec2>,
252 #[serde(default = "zero")]
254 pub rotation_degrees: Animated<f32>,
255 #[serde(default = "opaque")]
258 pub opacity: Animated<f32>,
259}
260
261fn origin() -> Animated<Vec2> {
262 Animated::Constant {
263 value: Vec2::new(0.0, 0.0),
264 }
265}
266
267impl Default for ShapeTransform {
268 fn default() -> Self {
269 Self {
270 anchor: origin(),
271 position: AnimatedPoint::default(),
272 scale: unit_scale(),
273 rotation_degrees: zero(),
274 opacity: opaque(),
275 }
276 }
277}
278
279#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
281#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
282#[serde(deny_unknown_fields)]
283pub struct StrokeItem {
284 pub paint: Animated<Paint>,
286 pub width: Animated<f32>,
289 #[serde(default = "opaque")]
291 pub opacity: Animated<f32>,
292 #[serde(default)]
294 pub join: LineJoin,
295 #[serde(default = "four")]
298 pub miter_limit: f32,
299 #[serde(default)]
301 pub cap: LineCap,
302 #[serde(default)]
304 pub dash: Option<Dash>,
305}
306
307const fn four() -> f32 {
308 4.0
309}
310
311#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
313#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
314#[serde(rename_all = "snake_case")]
315pub enum LineJoin {
316 #[default]
318 Miter,
319 Round,
321 Bevel,
323}
324
325#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
327#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
328#[serde(rename_all = "snake_case")]
329pub enum LineCap {
330 #[default]
332 Butt,
333 Round,
335 Square,
337}
338
339#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
342#[serde(deny_unknown_fields)]
343pub struct Dash {
344 pub pattern: Vec<Animated<f32>>,
348 #[serde(default = "zero")]
350 pub offset: Animated<f32>,
351}
352
353#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
355#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
356#[serde(deny_unknown_fields)]
357pub struct TrimItem {
358 pub start: Animated<f32>,
360 pub end: Animated<f32>,
363 #[serde(default = "zero")]
365 pub offset: Animated<f32>,
366 #[serde(default)]
368 pub mode: TrimMode,
369}
370
371#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
374#[serde(rename_all = "snake_case")]
375pub enum TrimMode {
376 #[default]
378 Together,
379 Individually,
381}
382
383#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
385#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
386#[serde(rename_all = "snake_case")]
387pub enum FillRule {
388 #[default]
390 Nonzero,
391 Evenodd,
393}
394
395#[derive(Copy, Clone, Debug, Eq, PartialEq)]
397#[non_exhaustive]
398pub enum ShapeProblem {
399 TooManyItems,
401 TooDeep,
403 PathTooLarge,
406 InvalidDash,
409 InvalidKeyframes,
411 OutOfRange,
413 PathKeyframesDiffer,
416 PaintKeyframesDiffer,
418}
419
420impl fmt::Display for ShapeProblem {
421 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
422 formatter.write_str(match self {
423 Self::TooManyItems => "has more items than a shapes generator may hold",
424 Self::TooDeep => "nests groups too deeply",
425 Self::PathTooLarge => "has more subpaths or vertices than a path may hold",
426 Self::InvalidDash => "has an invalid dash pattern",
427 Self::InvalidKeyframes => "has invalid keyframes",
428 Self::OutOfRange => "has a value out of range",
429 Self::PathKeyframesDiffer => "has path keyframes that differ in structure",
430 Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
431 })
432 }
433}
434
435#[derive(Clone, Debug, Eq, PartialEq)]
437pub struct ShapeIssue {
438 pub at: String,
441 pub problem: ShapeProblem,
443}
444
445impl From<ValueIssue> for ShapeIssue {
446 fn from(issue: ValueIssue) -> Self {
447 Self::new(
448 issue.at,
449 match issue.problem {
450 ValueProblem::InvalidKeyframes => ShapeProblem::InvalidKeyframes,
451 ValueProblem::OutOfRange => ShapeProblem::OutOfRange,
452 ValueProblem::PaintKeyframesDiffer => ShapeProblem::PaintKeyframesDiffer,
453 },
454 )
455 }
456}
457
458impl ShapeIssue {
459 fn new(at: impl Into<String>, problem: ShapeProblem) -> Self {
460 Self {
461 at: at.into(),
462 problem,
463 }
464 }
465}
466
467type Checked = Result<(), ShapeIssue>;
468
469pub fn validate_shape_items(items: &[ShapeItem]) -> Result<(), ShapeIssue> {
475 let mut count = 0;
476 validate_items(items, "items", 1, &mut count)
477}
478
479pub(crate) fn validate_items(
480 items: &[ShapeItem],
481 at: &str,
482 depth: usize,
483 count: &mut usize,
484) -> Checked {
485 if depth > MAX_SHAPE_DEPTH {
486 return Err(ShapeIssue::new(at, ShapeProblem::TooDeep));
487 }
488 for (index, item) in items.iter().enumerate() {
489 let at = format!("{at}[{index}]");
490 *count += 1;
491 if *count > MAX_SHAPE_ITEMS {
492 return Err(ShapeIssue::new(at, ShapeProblem::TooManyItems));
493 }
494 match item {
495 ShapeItem::Group { items, transform } => {
496 validate_transform(transform, &format!("{at}.transform"))?;
497 validate_items(items, &format!("{at}.items"), depth + 1, count)?;
498 }
499 ShapeItem::Rectangle(geometry) => validate_rectangle(geometry, &at)?,
500 ShapeItem::Ellipse(geometry) => validate_ellipse(geometry, &at)?,
501 ShapeItem::Polystar(geometry) => validate_polystar(geometry, &at)?,
502 ShapeItem::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path"))?,
503 ShapeItem::Fill { paint, opacity, .. } => {
504 validate_paint(paint, &format!("{at}.paint"))?;
505 scalar(opacity, &format!("{at}.opacity"), unit)?;
506 }
507 ShapeItem::Stroke(stroke) => validate_stroke(stroke, &at)?,
508 ShapeItem::Trim(trim) => {
509 scalar(&trim.start, &format!("{at}.start"), unit)?;
510 scalar(&trim.end, &format!("{at}.end"), unit)?;
511 scalar(&trim.offset, &format!("{at}.offset"), any)?;
512 }
513 }
514 }
515 Ok(())
516}
517
518impl ShapeGeometry {
519 pub fn validate(&self, at: &str) -> Result<(), ShapeIssue> {
525 match self {
526 Self::Rectangle(geometry) => validate_rectangle(geometry, at),
527 Self::Ellipse(geometry) => validate_ellipse(geometry, at),
528 Self::Polystar(geometry) => validate_polystar(geometry, at),
529 Self::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path")),
530 }
531 }
532}
533
534fn sizes(value: Vec2) -> bool {
535 value.x >= 0.0 && value.y >= 0.0
536}
537
538fn validate_rectangle(geometry: &RectangleGeometry, at: &str) -> Checked {
539 point(&geometry.center, &format!("{at}.center"))?;
540 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
541 scalar(
542 &geometry.corner_radius,
543 &format!("{at}.corner_radius"),
544 non_negative,
545 )?;
546 scalar(
547 &geometry.rotation_degrees,
548 &format!("{at}.rotation_degrees"),
549 any,
550 )?;
551 Ok(())
552}
553
554fn validate_ellipse(geometry: &EllipseGeometry, at: &str) -> Checked {
555 point(&geometry.center, &format!("{at}.center"))?;
556 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
557 scalar(
558 &geometry.rotation_degrees,
559 &format!("{at}.rotation_degrees"),
560 any,
561 )?;
562 Ok(())
563}
564
565fn validate_polystar(geometry: &PolystarGeometry, at: &str) -> Checked {
566 point(&geometry.center, &format!("{at}.center"))?;
567 scalar(&geometry.points, &format!("{at}.points"), |points| {
568 (MIN_POLYSTAR_POINTS..=MAX_POLYSTAR_POINTS).contains(&points)
569 })?;
570 scalar(
571 &geometry.outer_radius,
572 &format!("{at}.outer_radius"),
573 non_negative,
574 )?;
575 scalar(
576 &geometry.inner_radius,
577 &format!("{at}.inner_radius"),
578 non_negative,
579 )?;
580 scalar(
581 &geometry.outer_roundness,
582 &format!("{at}.outer_roundness"),
583 unit,
584 )?;
585 scalar(
586 &geometry.inner_roundness,
587 &format!("{at}.inner_roundness"),
588 unit,
589 )?;
590 scalar(
591 &geometry.rotation_degrees,
592 &format!("{at}.rotation_degrees"),
593 any,
594 )?;
595 Ok(())
596}
597
598fn validate_transform(transform: &ShapeTransform, at: &str) -> Checked {
599 vec2(&transform.anchor, &format!("{at}.anchor"), |_| true)?;
600 point(&transform.position, &format!("{at}.position"))?;
601 vec2(&transform.scale, &format!("{at}.scale"), |_| true)?;
602 scalar(
603 &transform.rotation_degrees,
604 &format!("{at}.rotation_degrees"),
605 any,
606 )?;
607 scalar(&transform.opacity, &format!("{at}.opacity"), unit)?;
608 Ok(())
609}
610
611fn validate_stroke(stroke: &StrokeItem, at: &str) -> Checked {
612 validate_paint(&stroke.paint, &format!("{at}.paint"))?;
613 scalar(&stroke.width, &format!("{at}.width"), non_negative)?;
614 scalar(&stroke.opacity, &format!("{at}.opacity"), unit)?;
615 if !stroke.miter_limit.is_finite() || stroke.miter_limit < 1.0 {
616 return Err(ShapeIssue::new(
617 format!("{at}.miter_limit"),
618 ShapeProblem::OutOfRange,
619 ));
620 }
621 if let Some(dash) = &stroke.dash {
622 let at = format!("{at}.dash");
623 if dash.pattern.is_empty() || dash.pattern.len() > MAX_DASH_ENTRIES {
624 return Err(ShapeIssue::new(
625 format!("{at}.pattern"),
626 ShapeProblem::InvalidDash,
627 ));
628 }
629 for (index, entry) in dash.pattern.iter().enumerate() {
630 scalar(entry, &format!("{at}.pattern[{index}]"), non_negative)?;
631 }
632 let positive = dash.pattern.iter().any(|entry| match entry {
635 Animated::Constant { value } => *value > 0.0,
636 Animated::Keyframes { keyframes } => keyframes.iter().any(|key| key.value > 0.0),
637 });
638 if !positive {
639 return Err(ShapeIssue::new(
640 format!("{at}.pattern"),
641 ShapeProblem::InvalidDash,
642 ));
643 }
644 scalar(&dash.offset, &format!("{at}.offset"), any)?;
645 }
646 Ok(())
647}
648
649fn validate_path(path: &Animated<Path>, at: &str) -> Checked {
650 let check_one = |path: &Path| -> Checked {
651 let vertices: usize = path
652 .subpaths
653 .iter()
654 .map(|subpath| subpath.vertices.len())
655 .sum();
656 if path.subpaths.len() > MAX_PATH_SUBPATHS || vertices > MAX_PATH_VERTICES {
657 return Err(ShapeIssue::new(at, ShapeProblem::PathTooLarge));
658 }
659 let finite = |value: Vec2| value.x.is_finite() && value.y.is_finite();
660 let all_finite = path.subpaths.iter().all(|subpath| {
661 subpath.vertices.iter().all(|vertex| {
662 finite(vertex.point) && finite(vertex.in_tangent) && finite(vertex.out_tangent)
663 })
664 });
665 all_finite
666 .then_some(())
667 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
668 };
669 match path {
670 Animated::Constant { value } => check_one(value),
671 Animated::Keyframes { keyframes } => {
672 if !valid_keyframes(keyframes) {
673 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
674 }
675 for keyframe in keyframes {
676 check_one(&keyframe.value)?;
677 }
678 let structure = |path: &Path| {
679 path.subpaths
680 .iter()
681 .map(|subpath| (subpath.closed, subpath.vertices.len()))
682 .collect::<Vec<_>>()
683 };
684 let first = structure(&keyframes[0].value);
685 if keyframes[1..]
686 .iter()
687 .any(|keyframe| structure(&keyframe.value) != first)
688 {
689 return Err(ShapeIssue::new(at, ShapeProblem::PathKeyframesDiffer));
690 }
691 Ok(())
692 }
693 }
694}
695
696impl Path {
697 #[must_use]
699 pub fn vertex_count(&self) -> usize {
700 self.subpaths
701 .iter()
702 .map(|subpath| subpath.vertices.len())
703 .sum()
704 }
705}
706
707impl ShapeItem {
708 #[must_use]
710 pub fn is_animated(&self) -> bool {
711 match self {
712 Self::Group { items, transform } => {
713 transform.is_animated() || items.iter().any(Self::is_animated)
714 }
715 Self::Rectangle(geometry) => geometry.is_animated(),
716 Self::Ellipse(geometry) => geometry.is_animated(),
717 Self::Polystar(geometry) => geometry.is_animated(),
718 Self::Path(geometry) => keyed(&geometry.path),
719 Self::Fill { paint, opacity, .. } => keyed(paint) || keyed(opacity),
720 Self::Stroke(stroke) => {
721 keyed(&stroke.paint)
722 || keyed(&stroke.width)
723 || keyed(&stroke.opacity)
724 || stroke
725 .dash
726 .as_ref()
727 .is_some_and(|dash| dash.pattern.iter().any(keyed) || keyed(&dash.offset))
728 }
729 Self::Trim(trim) => keyed(&trim.start) || keyed(&trim.end) || keyed(&trim.offset),
730 }
731 }
732}
733
734pub(crate) fn for_each_keyed_item(
738 items: &[ShapeItem],
739 at: &str,
740 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
741) {
742 for (index, item) in items.iter().enumerate() {
743 item.for_each_keyed(&format!("{at}[{index}]"), visit);
744 }
745}
746
747impl ShapeItem {
748 pub(crate) fn for_each_keyed(
751 &self,
752 at: &str,
753 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
754 ) {
755 match self {
756 Self::Group { items, transform } => {
757 let transform_at = format!("{at}.transform");
758 visit_keyed(&format!("{transform_at}.anchor"), &transform.anchor, visit);
759 visit_keyed_point(
760 &format!("{transform_at}.position"),
761 &transform.position,
762 visit,
763 );
764 visit_keyed(&format!("{transform_at}.scale"), &transform.scale, visit);
765 visit_keyed(
766 &format!("{transform_at}.rotation_degrees"),
767 &transform.rotation_degrees,
768 visit,
769 );
770 visit_keyed(
771 &format!("{transform_at}.opacity"),
772 &transform.opacity,
773 visit,
774 );
775 for_each_keyed_item(items, &format!("{at}.items"), visit);
776 }
777 Self::Rectangle(geometry) => geometry.for_each_keyed(at, visit),
778 Self::Ellipse(geometry) => geometry.for_each_keyed(at, visit),
779 Self::Polystar(geometry) => geometry.for_each_keyed(at, visit),
780 Self::Path(geometry) => visit_keyed(&format!("{at}.path"), &geometry.path, visit),
781 Self::Fill { paint, opacity, .. } => {
782 visit_keyed(&format!("{at}.paint"), paint, visit);
783 visit_keyed(&format!("{at}.opacity"), opacity, visit);
784 }
785 Self::Stroke(stroke) => {
786 visit_keyed(&format!("{at}.paint"), &stroke.paint, visit);
787 visit_keyed(&format!("{at}.width"), &stroke.width, visit);
788 visit_keyed(&format!("{at}.opacity"), &stroke.opacity, visit);
789 if let Some(dash) = &stroke.dash {
790 for (index, entry) in dash.pattern.iter().enumerate() {
791 visit_keyed(&format!("{at}.dash.pattern[{index}]"), entry, visit);
792 }
793 visit_keyed(&format!("{at}.dash.offset"), &dash.offset, visit);
794 }
795 }
796 Self::Trim(trim) => {
797 visit_keyed(&format!("{at}.start"), &trim.start, visit);
798 visit_keyed(&format!("{at}.end"), &trim.end, visit);
799 visit_keyed(&format!("{at}.offset"), &trim.offset, visit);
800 }
801 }
802 }
803}
804
805impl ShapeGeometry {
806 pub(crate) fn for_each_keyed(
809 &self,
810 at: &str,
811 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
812 ) {
813 match self {
814 Self::Rectangle(geometry) => geometry.for_each_keyed(at, visit),
815 Self::Ellipse(geometry) => geometry.for_each_keyed(at, visit),
816 Self::Polystar(geometry) => geometry.for_each_keyed(at, visit),
817 Self::Path(geometry) => visit_keyed(&format!("{at}.path"), &geometry.path, visit),
818 }
819 }
820
821 #[must_use]
823 pub fn is_animated(&self) -> bool {
824 match self {
825 Self::Rectangle(geometry) => geometry.is_animated(),
826 Self::Ellipse(geometry) => geometry.is_animated(),
827 Self::Polystar(geometry) => geometry.is_animated(),
828 Self::Path(geometry) => keyed(&geometry.path),
829 }
830 }
831}
832
833impl RectangleGeometry {
834 fn is_animated(&self) -> bool {
835 self.center.is_animated()
836 || keyed(&self.size)
837 || keyed(&self.corner_radius)
838 || keyed(&self.rotation_degrees)
839 }
840}
841
842impl EllipseGeometry {
843 fn is_animated(&self) -> bool {
844 self.center.is_animated() || keyed(&self.size) || keyed(&self.rotation_degrees)
845 }
846}
847
848impl RectangleGeometry {
849 fn for_each_keyed(&self, at: &str, visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime])) {
850 visit_keyed_point(&format!("{at}.center"), &self.center, visit);
851 visit_keyed(&format!("{at}.size"), &self.size, visit);
852 visit_keyed(&format!("{at}.corner_radius"), &self.corner_radius, visit);
853 visit_keyed(
854 &format!("{at}.rotation_degrees"),
855 &self.rotation_degrees,
856 visit,
857 );
858 }
859}
860
861impl EllipseGeometry {
862 fn for_each_keyed(&self, at: &str, visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime])) {
863 visit_keyed_point(&format!("{at}.center"), &self.center, visit);
864 visit_keyed(&format!("{at}.size"), &self.size, visit);
865 visit_keyed(
866 &format!("{at}.rotation_degrees"),
867 &self.rotation_degrees,
868 visit,
869 );
870 }
871}
872
873impl PolystarGeometry {
874 fn for_each_keyed(&self, at: &str, visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime])) {
875 visit_keyed_point(&format!("{at}.center"), &self.center, visit);
876 visit_keyed(&format!("{at}.points"), &self.points, visit);
877 visit_keyed(&format!("{at}.outer_radius"), &self.outer_radius, visit);
878 visit_keyed(&format!("{at}.inner_radius"), &self.inner_radius, visit);
879 visit_keyed(
880 &format!("{at}.outer_roundness"),
881 &self.outer_roundness,
882 visit,
883 );
884 visit_keyed(
885 &format!("{at}.inner_roundness"),
886 &self.inner_roundness,
887 visit,
888 );
889 visit_keyed(
890 &format!("{at}.rotation_degrees"),
891 &self.rotation_degrees,
892 visit,
893 );
894 }
895
896 fn is_animated(&self) -> bool {
897 self.center.is_animated()
898 || keyed(&self.points)
899 || keyed(&self.outer_radius)
900 || keyed(&self.inner_radius)
901 || keyed(&self.outer_roundness)
902 || keyed(&self.inner_roundness)
903 || keyed(&self.rotation_degrees)
904 }
905}
906
907impl ShapeTransform {
908 fn is_animated(&self) -> bool {
909 keyed(&self.anchor)
910 || self.position.is_animated()
911 || keyed(&self.scale)
912 || keyed(&self.rotation_degrees)
913 || keyed(&self.opacity)
914 }
915}
916
917const fn keyed<T>(animation: &Animated<T>) -> bool {
918 matches!(animation, Animated::Keyframes { .. })
919}