1use crate::animation::{valid_keyframes, valid_point_animation};
11use crate::{Animated, AnimatedPoint, Keyframe, Paint, Vec2};
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15pub const MAX_SHAPE_ITEMS: usize = 1_024;
18pub const MAX_SHAPE_DEPTH: usize = 32;
20pub const MAX_PATH_VERTICES: usize = 4_096;
22pub const MAX_PATH_SUBPATHS: usize = 256;
24pub const MAX_DASH_ENTRIES: usize = 16;
26pub const MAX_POLYSTAR_POINTS: f32 = 100.0;
29pub const MIN_POLYSTAR_POINTS: f32 = 3.0;
31
32#[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 {
47 items: Vec<ShapeItem>,
49 #[serde(default)]
51 transform: ShapeTransform,
52 },
53 Rectangle(RectangleGeometry),
55 Ellipse(EllipseGeometry),
57 Polystar(PolystarGeometry),
59 Path(PathGeometry),
61 Fill {
65 paint: Animated<Paint>,
67 #[serde(default = "opaque")]
69 opacity: Animated<f32>,
70 #[serde(default)]
72 rule: FillRule,
73 },
74 Stroke(StrokeItem),
76 Trim(TrimItem),
78}
79
80pub(crate) fn opaque() -> Animated<f32> {
81 Animated::Constant { value: 1.0 }
82}
83
84fn zero() -> Animated<f32> {
85 Animated::Constant { value: 0.0 }
86}
87
88#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
90#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
91#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
92pub enum ShapeGeometry {
93 Rectangle(RectangleGeometry),
95 Ellipse(EllipseGeometry),
97 Polystar(PolystarGeometry),
99 Path(PathGeometry),
101}
102
103#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
106#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
107#[serde(deny_unknown_fields)]
108pub struct RectangleGeometry {
109 pub center: AnimatedPoint,
111 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>,
132 #[serde(default = "zero")]
134 pub rotation_degrees: Animated<f32>,
135}
136
137#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
140#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
141#[serde(deny_unknown_fields)]
142pub struct PolystarGeometry {
143 pub kind: PolystarKind,
145 pub center: AnimatedPoint,
147 pub points: Animated<f32>,
152 pub outer_radius: Animated<f32>,
155 #[serde(default = "zero")]
157 pub inner_radius: Animated<f32>,
158 #[serde(default = "zero")]
161 pub outer_roundness: Animated<f32>,
162 #[serde(default = "zero")]
164 pub inner_roundness: Animated<f32>,
165 #[serde(default = "zero")]
167 pub rotation_degrees: Animated<f32>,
168}
169
170#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
173#[serde(rename_all = "snake_case")]
174pub enum PolystarKind {
175 #[default]
177 Star,
178 Polygon,
180}
181
182#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
189#[serde(deny_unknown_fields)]
190pub struct PathGeometry {
191 pub path: Animated<Path>,
193}
194
195#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
197#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
198#[serde(deny_unknown_fields)]
199pub struct Path {
200 pub subpaths: Vec<Subpath>,
203}
204
205#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
207#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
208#[serde(deny_unknown_fields)]
209pub struct Subpath {
210 #[serde(default)]
212 pub closed: bool,
213 pub vertices: Vec<PathVertex>,
215}
216
217#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
221#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
222#[serde(deny_unknown_fields)]
223pub struct PathVertex {
224 pub point: Vec2,
226 #[serde(default)]
228 pub in_tangent: Vec2,
229 #[serde(default)]
231 pub out_tangent: Vec2,
232}
233
234#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
238#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
239#[serde(deny_unknown_fields)]
240pub struct ShapeTransform {
241 #[serde(default = "origin")]
243 pub anchor: Animated<Vec2>,
244 #[serde(default)]
246 pub position: AnimatedPoint,
247 #[serde(default = "unscaled")]
250 pub scale: Animated<Vec2>,
251 #[serde(default = "zero")]
253 pub rotation_degrees: Animated<f32>,
254 #[serde(default = "opaque")]
257 pub opacity: Animated<f32>,
258}
259
260fn origin() -> Animated<Vec2> {
261 Animated::Constant {
262 value: Vec2::new(0.0, 0.0),
263 }
264}
265
266fn unscaled() -> Animated<Vec2> {
267 Animated::Constant {
268 value: Vec2::new(1.0, 1.0),
269 }
270}
271
272impl Default for ShapeTransform {
273 fn default() -> Self {
274 Self {
275 anchor: origin(),
276 position: AnimatedPoint::default(),
277 scale: unscaled(),
278 rotation_degrees: zero(),
279 opacity: opaque(),
280 }
281 }
282}
283
284#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
287#[serde(deny_unknown_fields)]
288pub struct StrokeItem {
289 pub paint: Animated<Paint>,
291 pub width: Animated<f32>,
294 #[serde(default = "opaque")]
296 pub opacity: Animated<f32>,
297 #[serde(default)]
299 pub join: LineJoin,
300 #[serde(default = "four")]
303 pub miter_limit: f32,
304 #[serde(default)]
306 pub cap: LineCap,
307 #[serde(default)]
309 pub dash: Option<Dash>,
310}
311
312const fn four() -> f32 {
313 4.0
314}
315
316#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
318#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
319#[serde(rename_all = "snake_case")]
320pub enum LineJoin {
321 #[default]
323 Miter,
324 Round,
326 Bevel,
328}
329
330#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
332#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
333#[serde(rename_all = "snake_case")]
334pub enum LineCap {
335 #[default]
337 Butt,
338 Round,
340 Square,
342}
343
344#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
346#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
347#[serde(deny_unknown_fields)]
348pub struct Dash {
349 pub pattern: Vec<Animated<f32>>,
353 #[serde(default = "zero")]
355 pub offset: Animated<f32>,
356}
357
358#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
361#[serde(deny_unknown_fields)]
362pub struct TrimItem {
363 pub start: Animated<f32>,
365 pub end: Animated<f32>,
368 #[serde(default = "zero")]
370 pub offset: Animated<f32>,
371 #[serde(default)]
373 pub mode: TrimMode,
374}
375
376#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
378#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
379#[serde(rename_all = "snake_case")]
380pub enum TrimMode {
381 #[default]
383 Together,
384 Individually,
386}
387
388#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
390#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
391#[serde(rename_all = "snake_case")]
392pub enum FillRule {
393 #[default]
395 Nonzero,
396 Evenodd,
398}
399
400#[derive(Copy, Clone, Debug, Eq, PartialEq)]
402#[non_exhaustive]
403pub enum ShapeProblem {
404 TooManyItems,
406 TooDeep,
408 PathTooLarge,
411 InvalidDash,
414 InvalidKeyframes,
416 OutOfRange,
418 PathKeyframesDiffer,
421 PaintKeyframesDiffer,
423}
424
425impl fmt::Display for ShapeProblem {
426 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
427 formatter.write_str(match self {
428 Self::TooManyItems => "has more items than a shapes generator may hold",
429 Self::TooDeep => "nests groups too deeply",
430 Self::PathTooLarge => "has more subpaths or vertices than a path may hold",
431 Self::InvalidDash => "has an invalid dash pattern",
432 Self::InvalidKeyframes => "has invalid keyframes",
433 Self::OutOfRange => "has a value out of range",
434 Self::PathKeyframesDiffer => "has path keyframes that differ in structure",
435 Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
436 })
437 }
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
442pub struct ShapeIssue {
443 pub at: String,
446 pub problem: ShapeProblem,
448}
449
450impl ShapeIssue {
451 fn new(at: impl Into<String>, problem: ShapeProblem) -> Self {
452 Self {
453 at: at.into(),
454 problem,
455 }
456 }
457}
458
459type Checked = Result<(), ShapeIssue>;
460
461pub fn validate_shape_items(items: &[ShapeItem]) -> Result<(), ShapeIssue> {
467 let mut count = 0;
468 validate_items(items, "items", 1, &mut count)
469}
470
471fn validate_items(items: &[ShapeItem], at: &str, depth: usize, count: &mut usize) -> Checked {
472 if depth > MAX_SHAPE_DEPTH {
473 return Err(ShapeIssue::new(at, ShapeProblem::TooDeep));
474 }
475 for (index, item) in items.iter().enumerate() {
476 let at = format!("{at}[{index}]");
477 *count += 1;
478 if *count > MAX_SHAPE_ITEMS {
479 return Err(ShapeIssue::new(at, ShapeProblem::TooManyItems));
480 }
481 match item {
482 ShapeItem::Group { items, transform } => {
483 validate_transform(transform, &format!("{at}.transform"))?;
484 validate_items(items, &format!("{at}.items"), depth + 1, count)?;
485 }
486 ShapeItem::Rectangle(geometry) => validate_rectangle(geometry, &at)?,
487 ShapeItem::Ellipse(geometry) => validate_ellipse(geometry, &at)?,
488 ShapeItem::Polystar(geometry) => validate_polystar(geometry, &at)?,
489 ShapeItem::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path"))?,
490 ShapeItem::Fill { paint, opacity, .. } => {
491 validate_paint(paint, &format!("{at}.paint"))?;
492 scalar(opacity, &format!("{at}.opacity"), unit)?;
493 }
494 ShapeItem::Stroke(stroke) => validate_stroke(stroke, &at)?,
495 ShapeItem::Trim(trim) => {
496 scalar(&trim.start, &format!("{at}.start"), unit)?;
497 scalar(&trim.end, &format!("{at}.end"), unit)?;
498 scalar(&trim.offset, &format!("{at}.offset"), any)?;
499 }
500 }
501 }
502 Ok(())
503}
504
505impl ShapeGeometry {
506 pub fn validate(&self, at: &str) -> Result<(), ShapeIssue> {
512 match self {
513 Self::Rectangle(geometry) => validate_rectangle(geometry, at),
514 Self::Ellipse(geometry) => validate_ellipse(geometry, at),
515 Self::Polystar(geometry) => validate_polystar(geometry, at),
516 Self::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path")),
517 }
518 }
519}
520
521fn unit(value: f32) -> bool {
522 (0.0..=1.0).contains(&value)
523}
524
525fn non_negative(value: f32) -> bool {
526 value >= 0.0
527}
528
529fn any(_: f32) -> bool {
530 true
531}
532
533fn scalar(animation: &Animated<f32>, at: &str, check: fn(f32) -> bool) -> Checked {
534 let valid = |value: f32| value.is_finite() && check(value);
535 let values_valid = match animation {
536 Animated::Constant { value } => valid(*value),
537 Animated::Keyframes { keyframes } => {
538 if !valid_keyframes(keyframes) {
539 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
540 }
541 keyframes.iter().all(|keyframe| valid(keyframe.value))
542 }
543 };
544 values_valid
545 .then_some(())
546 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
547}
548
549fn vec2(animation: &Animated<Vec2>, at: &str, check: fn(Vec2) -> bool) -> Checked {
550 let valid = |value: Vec2| value.x.is_finite() && value.y.is_finite() && check(value);
551 let values_valid = match animation {
552 Animated::Constant { value } => valid(*value),
553 Animated::Keyframes { keyframes } => {
554 if !valid_keyframes(keyframes) {
555 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
556 }
557 keyframes.iter().all(|keyframe| valid(keyframe.value))
558 }
559 };
560 values_valid
561 .then_some(())
562 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
563}
564
565fn point(animation: &AnimatedPoint, at: &str) -> Checked {
566 if let AnimatedPoint::Keyframes { keyframes } = animation
567 && (keyframes.is_empty()
568 || keyframes
569 .windows(2)
570 .any(|pair| pair[0].time >= pair[1].time)
571 || !keyframes
572 .iter()
573 .all(|keyframe| keyframe.interpolation.is_valid()))
574 {
575 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
576 }
577 valid_point_animation(animation)
578 .then_some(())
579 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
580}
581
582fn sizes(value: Vec2) -> bool {
583 value.x >= 0.0 && value.y >= 0.0
584}
585
586fn validate_rectangle(geometry: &RectangleGeometry, at: &str) -> Checked {
587 point(&geometry.center, &format!("{at}.center"))?;
588 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
589 scalar(
590 &geometry.corner_radius,
591 &format!("{at}.corner_radius"),
592 non_negative,
593 )?;
594 scalar(
595 &geometry.rotation_degrees,
596 &format!("{at}.rotation_degrees"),
597 any,
598 )
599}
600
601fn validate_ellipse(geometry: &EllipseGeometry, at: &str) -> Checked {
602 point(&geometry.center, &format!("{at}.center"))?;
603 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
604 scalar(
605 &geometry.rotation_degrees,
606 &format!("{at}.rotation_degrees"),
607 any,
608 )
609}
610
611fn validate_polystar(geometry: &PolystarGeometry, at: &str) -> Checked {
612 point(&geometry.center, &format!("{at}.center"))?;
613 scalar(&geometry.points, &format!("{at}.points"), |points| {
614 (MIN_POLYSTAR_POINTS..=MAX_POLYSTAR_POINTS).contains(&points)
615 })?;
616 scalar(
617 &geometry.outer_radius,
618 &format!("{at}.outer_radius"),
619 non_negative,
620 )?;
621 scalar(
622 &geometry.inner_radius,
623 &format!("{at}.inner_radius"),
624 non_negative,
625 )?;
626 scalar(
627 &geometry.outer_roundness,
628 &format!("{at}.outer_roundness"),
629 unit,
630 )?;
631 scalar(
632 &geometry.inner_roundness,
633 &format!("{at}.inner_roundness"),
634 unit,
635 )?;
636 scalar(
637 &geometry.rotation_degrees,
638 &format!("{at}.rotation_degrees"),
639 any,
640 )
641}
642
643fn validate_transform(transform: &ShapeTransform, at: &str) -> Checked {
644 vec2(&transform.anchor, &format!("{at}.anchor"), |_| true)?;
645 point(&transform.position, &format!("{at}.position"))?;
646 vec2(&transform.scale, &format!("{at}.scale"), |_| true)?;
647 scalar(
648 &transform.rotation_degrees,
649 &format!("{at}.rotation_degrees"),
650 any,
651 )?;
652 scalar(&transform.opacity, &format!("{at}.opacity"), unit)
653}
654
655fn validate_stroke(stroke: &StrokeItem, at: &str) -> Checked {
656 validate_paint(&stroke.paint, &format!("{at}.paint"))?;
657 scalar(&stroke.width, &format!("{at}.width"), non_negative)?;
658 scalar(&stroke.opacity, &format!("{at}.opacity"), unit)?;
659 if !stroke.miter_limit.is_finite() || stroke.miter_limit < 1.0 {
660 return Err(ShapeIssue::new(
661 format!("{at}.miter_limit"),
662 ShapeProblem::OutOfRange,
663 ));
664 }
665 if let Some(dash) = &stroke.dash {
666 let at = format!("{at}.dash");
667 if dash.pattern.is_empty() || dash.pattern.len() > MAX_DASH_ENTRIES {
668 return Err(ShapeIssue::new(
669 format!("{at}.pattern"),
670 ShapeProblem::InvalidDash,
671 ));
672 }
673 for (index, entry) in dash.pattern.iter().enumerate() {
674 scalar(entry, &format!("{at}.pattern[{index}]"), non_negative)?;
675 }
676 let positive = dash.pattern.iter().any(|entry| match entry {
679 Animated::Constant { value } => *value > 0.0,
680 Animated::Keyframes { keyframes } => keyframes.iter().any(|key| key.value > 0.0),
681 });
682 if !positive {
683 return Err(ShapeIssue::new(
684 format!("{at}.pattern"),
685 ShapeProblem::InvalidDash,
686 ));
687 }
688 scalar(&dash.offset, &format!("{at}.offset"), any)?;
689 }
690 Ok(())
691}
692
693fn validate_path(path: &Animated<Path>, at: &str) -> Checked {
694 let check_one = |path: &Path| -> Checked {
695 let vertices: usize = path
696 .subpaths
697 .iter()
698 .map(|subpath| subpath.vertices.len())
699 .sum();
700 if path.subpaths.len() > MAX_PATH_SUBPATHS || vertices > MAX_PATH_VERTICES {
701 return Err(ShapeIssue::new(at, ShapeProblem::PathTooLarge));
702 }
703 let finite = |value: Vec2| value.x.is_finite() && value.y.is_finite();
704 let all_finite = path.subpaths.iter().all(|subpath| {
705 subpath.vertices.iter().all(|vertex| {
706 finite(vertex.point) && finite(vertex.in_tangent) && finite(vertex.out_tangent)
707 })
708 });
709 all_finite
710 .then_some(())
711 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
712 };
713 match path {
714 Animated::Constant { value } => check_one(value),
715 Animated::Keyframes { keyframes } => {
716 if !valid_keyframes(keyframes) {
717 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
718 }
719 for keyframe in keyframes {
720 check_one(&keyframe.value)?;
721 }
722 let structure = |path: &Path| {
723 path.subpaths
724 .iter()
725 .map(|subpath| (subpath.closed, subpath.vertices.len()))
726 .collect::<Vec<_>>()
727 };
728 let first = structure(&keyframes[0].value);
729 if keyframes[1..]
730 .iter()
731 .any(|keyframe| structure(&keyframe.value) != first)
732 {
733 return Err(ShapeIssue::new(at, ShapeProblem::PathKeyframesDiffer));
734 }
735 Ok(())
736 }
737 }
738}
739
740fn validate_paint(paint: &Animated<Paint>, at: &str) -> Checked {
741 let invalid = || ShapeIssue::new(at, ShapeProblem::OutOfRange);
742 match paint {
743 Animated::Constant { value } => value.is_valid().then_some(()).ok_or_else(invalid),
744 Animated::Keyframes { keyframes } => {
745 if !valid_keyframes(keyframes) {
746 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
747 }
748 if !keyframes.iter().all(|keyframe| keyframe.value.is_valid()) {
749 return Err(invalid());
750 }
751 let first = paint_structure(&keyframes[0]);
752 if keyframes[1..]
753 .iter()
754 .any(|keyframe| paint_structure(keyframe) != first)
755 {
756 return Err(ShapeIssue::new(at, ShapeProblem::PaintKeyframesDiffer));
757 }
758 Ok(())
759 }
760 }
761}
762
763fn paint_structure(keyframe: &Keyframe<Paint>) -> (u8, usize, Option<crate::Spread>) {
766 match &keyframe.value {
767 Paint::Solid { .. } => (0, 1, None),
768 Paint::LinearGradient { stops, spread, .. } => (1, stops.len(), Some(*spread)),
769 Paint::RadialGradient { stops, spread, .. } => (2, stops.len(), Some(*spread)),
770 Paint::ConicGradient { stops, .. } => (3, stops.len(), None),
771 }
772}
773
774impl Path {
775 #[must_use]
777 pub fn vertex_count(&self) -> usize {
778 self.subpaths
779 .iter()
780 .map(|subpath| subpath.vertices.len())
781 .sum()
782 }
783}
784
785impl ShapeItem {
786 #[must_use]
788 pub fn is_animated(&self) -> bool {
789 match self {
790 Self::Group { items, transform } => {
791 transform.is_animated() || items.iter().any(Self::is_animated)
792 }
793 Self::Rectangle(geometry) => geometry.is_animated(),
794 Self::Ellipse(geometry) => geometry.is_animated(),
795 Self::Polystar(geometry) => geometry.is_animated(),
796 Self::Path(geometry) => keyed(&geometry.path),
797 Self::Fill { paint, opacity, .. } => keyed(paint) || keyed(opacity),
798 Self::Stroke(stroke) => {
799 keyed(&stroke.paint)
800 || keyed(&stroke.width)
801 || keyed(&stroke.opacity)
802 || stroke
803 .dash
804 .as_ref()
805 .is_some_and(|dash| dash.pattern.iter().any(keyed) || keyed(&dash.offset))
806 }
807 Self::Trim(trim) => keyed(&trim.start) || keyed(&trim.end) || keyed(&trim.offset),
808 }
809 }
810}
811
812impl ShapeGeometry {
813 #[must_use]
815 pub fn is_animated(&self) -> bool {
816 match self {
817 Self::Rectangle(geometry) => geometry.is_animated(),
818 Self::Ellipse(geometry) => geometry.is_animated(),
819 Self::Polystar(geometry) => geometry.is_animated(),
820 Self::Path(geometry) => keyed(&geometry.path),
821 }
822 }
823}
824
825impl RectangleGeometry {
826 fn is_animated(&self) -> bool {
827 self.center.is_animated()
828 || keyed(&self.size)
829 || keyed(&self.corner_radius)
830 || keyed(&self.rotation_degrees)
831 }
832}
833
834impl EllipseGeometry {
835 fn is_animated(&self) -> bool {
836 self.center.is_animated() || keyed(&self.size) || keyed(&self.rotation_degrees)
837 }
838}
839
840impl PolystarGeometry {
841 fn is_animated(&self) -> bool {
842 self.center.is_animated()
843 || keyed(&self.points)
844 || keyed(&self.outer_radius)
845 || keyed(&self.inner_radius)
846 || keyed(&self.outer_roundness)
847 || keyed(&self.inner_roundness)
848 || keyed(&self.rotation_degrees)
849 }
850}
851
852impl ShapeTransform {
853 fn is_animated(&self) -> bool {
854 keyed(&self.anchor)
855 || self.position.is_animated()
856 || keyed(&self.scale)
857 || keyed(&self.rotation_degrees)
858 || keyed(&self.opacity)
859 }
860}
861
862const fn keyed<T>(animation: &Animated<T>) -> bool {
863 matches!(animation, Animated::Keyframes { .. })
864}