1use crate::animation::valid_keyframes;
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
83pub(crate) fn opaque() -> Animated<f32> {
84 Animated::Constant { value: 1.0 }
85}
86
87fn zero() -> Animated<f32> {
88 Animated::Constant { value: 0.0 }
89}
90
91#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
93#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
94#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
95pub enum ShapeGeometry {
96 Rectangle(RectangleGeometry),
98 Ellipse(EllipseGeometry),
100 Polystar(PolystarGeometry),
102 Path(PathGeometry),
104}
105
106#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110#[serde(deny_unknown_fields)]
111pub struct RectangleGeometry {
112 pub center: AnimatedPoint,
114 pub size: Animated<Vec2>,
116 #[serde(default = "zero")]
119 pub corner_radius: Animated<f32>,
120 #[serde(default = "zero")]
122 pub rotation_degrees: Animated<f32>,
123}
124
125#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129#[serde(deny_unknown_fields)]
130pub struct EllipseGeometry {
131 pub center: AnimatedPoint,
133 pub size: Animated<Vec2>,
135 #[serde(default = "zero")]
137 pub rotation_degrees: Animated<f32>,
138}
139
140#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
143#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
144#[serde(deny_unknown_fields)]
145pub struct PolystarGeometry {
146 pub kind: PolystarKind,
148 pub center: AnimatedPoint,
150 pub points: Animated<f32>,
155 pub outer_radius: Animated<f32>,
158 #[serde(default = "zero")]
160 pub inner_radius: Animated<f32>,
161 #[serde(default = "zero")]
164 pub outer_roundness: Animated<f32>,
165 #[serde(default = "zero")]
167 pub inner_roundness: Animated<f32>,
168 #[serde(default = "zero")]
170 pub rotation_degrees: Animated<f32>,
171}
172
173#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
176#[serde(rename_all = "snake_case")]
177pub enum PolystarKind {
178 #[default]
180 Star,
181 Polygon,
183}
184
185#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
192#[serde(deny_unknown_fields)]
193pub struct PathGeometry {
194 pub path: Animated<Path>,
196}
197
198#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201#[serde(deny_unknown_fields)]
202pub struct Path {
203 pub subpaths: Vec<Subpath>,
206}
207
208#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
210#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
211#[serde(deny_unknown_fields)]
212pub struct Subpath {
213 #[serde(default)]
215 pub closed: bool,
216 pub vertices: Vec<PathVertex>,
218}
219
220#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225#[serde(deny_unknown_fields)]
226pub struct PathVertex {
227 pub point: Vec2,
229 #[serde(default)]
231 pub in_tangent: Vec2,
232 #[serde(default)]
234 pub out_tangent: Vec2,
235}
236
237#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
242#[serde(deny_unknown_fields)]
243pub struct ShapeTransform {
244 #[serde(default = "origin")]
246 pub anchor: Animated<Vec2>,
247 #[serde(default)]
249 pub position: AnimatedPoint,
250 #[serde(default = "unscaled")]
253 pub scale: Animated<Vec2>,
254 #[serde(default = "zero")]
256 pub rotation_degrees: Animated<f32>,
257 #[serde(default = "opaque")]
260 pub opacity: Animated<f32>,
261}
262
263fn origin() -> Animated<Vec2> {
264 Animated::Constant {
265 value: Vec2::new(0.0, 0.0),
266 }
267}
268
269fn unscaled() -> Animated<Vec2> {
270 Animated::Constant {
271 value: Vec2::new(1.0, 1.0),
272 }
273}
274
275impl Default for ShapeTransform {
276 fn default() -> Self {
277 Self {
278 anchor: origin(),
279 position: AnimatedPoint::default(),
280 scale: unscaled(),
281 rotation_degrees: zero(),
282 opacity: opaque(),
283 }
284 }
285}
286
287#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
290#[serde(deny_unknown_fields)]
291pub struct StrokeItem {
292 pub paint: Animated<Paint>,
294 pub width: Animated<f32>,
297 #[serde(default = "opaque")]
299 pub opacity: Animated<f32>,
300 #[serde(default)]
302 pub join: LineJoin,
303 #[serde(default = "four")]
306 pub miter_limit: f32,
307 #[serde(default)]
309 pub cap: LineCap,
310 #[serde(default)]
312 pub dash: Option<Dash>,
313}
314
315const fn four() -> f32 {
316 4.0
317}
318
319#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
322#[serde(rename_all = "snake_case")]
323pub enum LineJoin {
324 #[default]
326 Miter,
327 Round,
329 Bevel,
331}
332
333#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
335#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
336#[serde(rename_all = "snake_case")]
337pub enum LineCap {
338 #[default]
340 Butt,
341 Round,
343 Square,
345}
346
347#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
350#[serde(deny_unknown_fields)]
351pub struct Dash {
352 pub pattern: Vec<Animated<f32>>,
356 #[serde(default = "zero")]
358 pub offset: Animated<f32>,
359}
360
361#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
363#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
364#[serde(deny_unknown_fields)]
365pub struct TrimItem {
366 pub start: Animated<f32>,
368 pub end: Animated<f32>,
371 #[serde(default = "zero")]
373 pub offset: Animated<f32>,
374 #[serde(default)]
376 pub mode: TrimMode,
377}
378
379#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
382#[serde(rename_all = "snake_case")]
383pub enum TrimMode {
384 #[default]
386 Together,
387 Individually,
389}
390
391#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
393#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
394#[serde(rename_all = "snake_case")]
395pub enum FillRule {
396 #[default]
398 Nonzero,
399 Evenodd,
401}
402
403#[derive(Copy, Clone, Debug, Eq, PartialEq)]
405#[non_exhaustive]
406pub enum ShapeProblem {
407 TooManyItems,
409 TooDeep,
411 PathTooLarge,
414 InvalidDash,
417 InvalidKeyframes,
419 OutOfRange,
421 PathKeyframesDiffer,
424 PaintKeyframesDiffer,
426}
427
428impl fmt::Display for ShapeProblem {
429 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
430 formatter.write_str(match self {
431 Self::TooManyItems => "has more items than a shapes generator may hold",
432 Self::TooDeep => "nests groups too deeply",
433 Self::PathTooLarge => "has more subpaths or vertices than a path may hold",
434 Self::InvalidDash => "has an invalid dash pattern",
435 Self::InvalidKeyframes => "has invalid keyframes",
436 Self::OutOfRange => "has a value out of range",
437 Self::PathKeyframesDiffer => "has path keyframes that differ in structure",
438 Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
439 })
440 }
441}
442
443#[derive(Clone, Debug, Eq, PartialEq)]
445pub struct ShapeIssue {
446 pub at: String,
449 pub problem: ShapeProblem,
451}
452
453impl From<ValueIssue> for ShapeIssue {
454 fn from(issue: ValueIssue) -> Self {
455 Self::new(
456 issue.at,
457 match issue.problem {
458 ValueProblem::InvalidKeyframes => ShapeProblem::InvalidKeyframes,
459 ValueProblem::OutOfRange => ShapeProblem::OutOfRange,
460 ValueProblem::PaintKeyframesDiffer => ShapeProblem::PaintKeyframesDiffer,
461 },
462 )
463 }
464}
465
466impl ShapeIssue {
467 fn new(at: impl Into<String>, problem: ShapeProblem) -> Self {
468 Self {
469 at: at.into(),
470 problem,
471 }
472 }
473}
474
475type Checked = Result<(), ShapeIssue>;
476
477pub fn validate_shape_items(items: &[ShapeItem]) -> Result<(), ShapeIssue> {
483 let mut count = 0;
484 validate_items(items, "items", 1, &mut count)
485}
486
487pub(crate) fn validate_items(
488 items: &[ShapeItem],
489 at: &str,
490 depth: usize,
491 count: &mut usize,
492) -> Checked {
493 if depth > MAX_SHAPE_DEPTH {
494 return Err(ShapeIssue::new(at, ShapeProblem::TooDeep));
495 }
496 for (index, item) in items.iter().enumerate() {
497 let at = format!("{at}[{index}]");
498 *count += 1;
499 if *count > MAX_SHAPE_ITEMS {
500 return Err(ShapeIssue::new(at, ShapeProblem::TooManyItems));
501 }
502 match item {
503 ShapeItem::Group { items, transform } => {
504 validate_transform(transform, &format!("{at}.transform"))?;
505 validate_items(items, &format!("{at}.items"), depth + 1, count)?;
506 }
507 ShapeItem::Rectangle(geometry) => validate_rectangle(geometry, &at)?,
508 ShapeItem::Ellipse(geometry) => validate_ellipse(geometry, &at)?,
509 ShapeItem::Polystar(geometry) => validate_polystar(geometry, &at)?,
510 ShapeItem::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path"))?,
511 ShapeItem::Fill { paint, opacity, .. } => {
512 validate_paint(paint, &format!("{at}.paint"))?;
513 scalar(opacity, &format!("{at}.opacity"), unit)?;
514 }
515 ShapeItem::Stroke(stroke) => validate_stroke(stroke, &at)?,
516 ShapeItem::Trim(trim) => {
517 scalar(&trim.start, &format!("{at}.start"), unit)?;
518 scalar(&trim.end, &format!("{at}.end"), unit)?;
519 scalar(&trim.offset, &format!("{at}.offset"), any)?;
520 }
521 }
522 }
523 Ok(())
524}
525
526impl ShapeGeometry {
527 pub fn validate(&self, at: &str) -> Result<(), ShapeIssue> {
533 match self {
534 Self::Rectangle(geometry) => validate_rectangle(geometry, at),
535 Self::Ellipse(geometry) => validate_ellipse(geometry, at),
536 Self::Polystar(geometry) => validate_polystar(geometry, at),
537 Self::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path")),
538 }
539 }
540}
541
542fn sizes(value: Vec2) -> bool {
543 value.x >= 0.0 && value.y >= 0.0
544}
545
546fn validate_rectangle(geometry: &RectangleGeometry, at: &str) -> Checked {
547 point(&geometry.center, &format!("{at}.center"))?;
548 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
549 scalar(
550 &geometry.corner_radius,
551 &format!("{at}.corner_radius"),
552 non_negative,
553 )?;
554 scalar(
555 &geometry.rotation_degrees,
556 &format!("{at}.rotation_degrees"),
557 any,
558 )?;
559 Ok(())
560}
561
562fn validate_ellipse(geometry: &EllipseGeometry, at: &str) -> Checked {
563 point(&geometry.center, &format!("{at}.center"))?;
564 vec2(&geometry.size, &format!("{at}.size"), sizes)?;
565 scalar(
566 &geometry.rotation_degrees,
567 &format!("{at}.rotation_degrees"),
568 any,
569 )?;
570 Ok(())
571}
572
573fn validate_polystar(geometry: &PolystarGeometry, at: &str) -> Checked {
574 point(&geometry.center, &format!("{at}.center"))?;
575 scalar(&geometry.points, &format!("{at}.points"), |points| {
576 (MIN_POLYSTAR_POINTS..=MAX_POLYSTAR_POINTS).contains(&points)
577 })?;
578 scalar(
579 &geometry.outer_radius,
580 &format!("{at}.outer_radius"),
581 non_negative,
582 )?;
583 scalar(
584 &geometry.inner_radius,
585 &format!("{at}.inner_radius"),
586 non_negative,
587 )?;
588 scalar(
589 &geometry.outer_roundness,
590 &format!("{at}.outer_roundness"),
591 unit,
592 )?;
593 scalar(
594 &geometry.inner_roundness,
595 &format!("{at}.inner_roundness"),
596 unit,
597 )?;
598 scalar(
599 &geometry.rotation_degrees,
600 &format!("{at}.rotation_degrees"),
601 any,
602 )?;
603 Ok(())
604}
605
606fn validate_transform(transform: &ShapeTransform, at: &str) -> Checked {
607 vec2(&transform.anchor, &format!("{at}.anchor"), |_| true)?;
608 point(&transform.position, &format!("{at}.position"))?;
609 vec2(&transform.scale, &format!("{at}.scale"), |_| true)?;
610 scalar(
611 &transform.rotation_degrees,
612 &format!("{at}.rotation_degrees"),
613 any,
614 )?;
615 scalar(&transform.opacity, &format!("{at}.opacity"), unit)?;
616 Ok(())
617}
618
619fn validate_stroke(stroke: &StrokeItem, at: &str) -> Checked {
620 validate_paint(&stroke.paint, &format!("{at}.paint"))?;
621 scalar(&stroke.width, &format!("{at}.width"), non_negative)?;
622 scalar(&stroke.opacity, &format!("{at}.opacity"), unit)?;
623 if !stroke.miter_limit.is_finite() || stroke.miter_limit < 1.0 {
624 return Err(ShapeIssue::new(
625 format!("{at}.miter_limit"),
626 ShapeProblem::OutOfRange,
627 ));
628 }
629 if let Some(dash) = &stroke.dash {
630 let at = format!("{at}.dash");
631 if dash.pattern.is_empty() || dash.pattern.len() > MAX_DASH_ENTRIES {
632 return Err(ShapeIssue::new(
633 format!("{at}.pattern"),
634 ShapeProblem::InvalidDash,
635 ));
636 }
637 for (index, entry) in dash.pattern.iter().enumerate() {
638 scalar(entry, &format!("{at}.pattern[{index}]"), non_negative)?;
639 }
640 let positive = dash.pattern.iter().any(|entry| match entry {
643 Animated::Constant { value } => *value > 0.0,
644 Animated::Keyframes { keyframes } => keyframes.iter().any(|key| key.value > 0.0),
645 });
646 if !positive {
647 return Err(ShapeIssue::new(
648 format!("{at}.pattern"),
649 ShapeProblem::InvalidDash,
650 ));
651 }
652 scalar(&dash.offset, &format!("{at}.offset"), any)?;
653 }
654 Ok(())
655}
656
657fn validate_path(path: &Animated<Path>, at: &str) -> Checked {
658 let check_one = |path: &Path| -> Checked {
659 let vertices: usize = path
660 .subpaths
661 .iter()
662 .map(|subpath| subpath.vertices.len())
663 .sum();
664 if path.subpaths.len() > MAX_PATH_SUBPATHS || vertices > MAX_PATH_VERTICES {
665 return Err(ShapeIssue::new(at, ShapeProblem::PathTooLarge));
666 }
667 let finite = |value: Vec2| value.x.is_finite() && value.y.is_finite();
668 let all_finite = path.subpaths.iter().all(|subpath| {
669 subpath.vertices.iter().all(|vertex| {
670 finite(vertex.point) && finite(vertex.in_tangent) && finite(vertex.out_tangent)
671 })
672 });
673 all_finite
674 .then_some(())
675 .ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
676 };
677 match path {
678 Animated::Constant { value } => check_one(value),
679 Animated::Keyframes { keyframes } => {
680 if !valid_keyframes(keyframes) {
681 return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
682 }
683 for keyframe in keyframes {
684 check_one(&keyframe.value)?;
685 }
686 let structure = |path: &Path| {
687 path.subpaths
688 .iter()
689 .map(|subpath| (subpath.closed, subpath.vertices.len()))
690 .collect::<Vec<_>>()
691 };
692 let first = structure(&keyframes[0].value);
693 if keyframes[1..]
694 .iter()
695 .any(|keyframe| structure(&keyframe.value) != first)
696 {
697 return Err(ShapeIssue::new(at, ShapeProblem::PathKeyframesDiffer));
698 }
699 Ok(())
700 }
701 }
702}
703
704impl Path {
705 #[must_use]
707 pub fn vertex_count(&self) -> usize {
708 self.subpaths
709 .iter()
710 .map(|subpath| subpath.vertices.len())
711 .sum()
712 }
713}
714
715impl ShapeItem {
716 #[must_use]
718 pub fn is_animated(&self) -> bool {
719 match self {
720 Self::Group { items, transform } => {
721 transform.is_animated() || items.iter().any(Self::is_animated)
722 }
723 Self::Rectangle(geometry) => geometry.is_animated(),
724 Self::Ellipse(geometry) => geometry.is_animated(),
725 Self::Polystar(geometry) => geometry.is_animated(),
726 Self::Path(geometry) => keyed(&geometry.path),
727 Self::Fill { paint, opacity, .. } => keyed(paint) || keyed(opacity),
728 Self::Stroke(stroke) => {
729 keyed(&stroke.paint)
730 || keyed(&stroke.width)
731 || keyed(&stroke.opacity)
732 || stroke
733 .dash
734 .as_ref()
735 .is_some_and(|dash| dash.pattern.iter().any(keyed) || keyed(&dash.offset))
736 }
737 Self::Trim(trim) => keyed(&trim.start) || keyed(&trim.end) || keyed(&trim.offset),
738 }
739 }
740}
741
742impl ShapeGeometry {
743 #[must_use]
745 pub fn is_animated(&self) -> bool {
746 match self {
747 Self::Rectangle(geometry) => geometry.is_animated(),
748 Self::Ellipse(geometry) => geometry.is_animated(),
749 Self::Polystar(geometry) => geometry.is_animated(),
750 Self::Path(geometry) => keyed(&geometry.path),
751 }
752 }
753}
754
755impl RectangleGeometry {
756 fn is_animated(&self) -> bool {
757 self.center.is_animated()
758 || keyed(&self.size)
759 || keyed(&self.corner_radius)
760 || keyed(&self.rotation_degrees)
761 }
762}
763
764impl EllipseGeometry {
765 fn is_animated(&self) -> bool {
766 self.center.is_animated() || keyed(&self.size) || keyed(&self.rotation_degrees)
767 }
768}
769
770impl PolystarGeometry {
771 fn is_animated(&self) -> bool {
772 self.center.is_animated()
773 || keyed(&self.points)
774 || keyed(&self.outer_radius)
775 || keyed(&self.inner_radius)
776 || keyed(&self.outer_roundness)
777 || keyed(&self.inner_roundness)
778 || keyed(&self.rotation_degrees)
779 }
780}
781
782impl ShapeTransform {
783 fn is_animated(&self) -> bool {
784 keyed(&self.anchor)
785 || self.position.is_animated()
786 || keyed(&self.scale)
787 || keyed(&self.rotation_degrees)
788 || keyed(&self.opacity)
789 }
790}
791
792const fn keyed<T>(animation: &Animated<T>) -> bool {
793 matches!(animation, Animated::Keyframes { .. })
794}