use crate::animation::valid_keyframes;
use crate::checks::{
ValueIssue, ValueProblem, any, non_negative, point, scalar, unit, validate_paint, vec2,
};
use crate::{Animated, AnimatedPoint, Paint, Vec2};
use serde::{Deserialize, Serialize};
use std::fmt;
pub const MAX_SHAPE_ITEMS: usize = 1_024;
pub const MAX_SHAPE_DEPTH: usize = 32;
pub const MAX_PATH_VERTICES: usize = 4_096;
pub const MAX_PATH_SUBPATHS: usize = 256;
pub const MAX_DASH_ENTRIES: usize = 16;
pub const MAX_POLYSTAR_POINTS: f32 = 100.0;
pub const MIN_POLYSTAR_POINTS: f32 = 3.0;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum ShapeItem {
Group {
items: Vec<ShapeItem>,
#[serde(default)]
transform: ShapeTransform,
},
Rectangle(RectangleGeometry),
Ellipse(EllipseGeometry),
Polystar(PolystarGeometry),
Path(PathGeometry),
Fill {
paint: Animated<Paint>,
#[serde(default = "opaque")]
opacity: Animated<f32>,
#[serde(default)]
rule: FillRule,
},
Stroke(StrokeItem),
Trim(TrimItem),
}
pub(crate) fn opaque() -> Animated<f32> {
Animated::Constant { value: 1.0 }
}
fn zero() -> Animated<f32> {
Animated::Constant { value: 0.0 }
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum ShapeGeometry {
Rectangle(RectangleGeometry),
Ellipse(EllipseGeometry),
Polystar(PolystarGeometry),
Path(PathGeometry),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct RectangleGeometry {
pub center: AnimatedPoint,
pub size: Animated<Vec2>,
#[serde(default = "zero")]
pub corner_radius: Animated<f32>,
#[serde(default = "zero")]
pub rotation_degrees: Animated<f32>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct EllipseGeometry {
pub center: AnimatedPoint,
pub size: Animated<Vec2>,
#[serde(default = "zero")]
pub rotation_degrees: Animated<f32>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PolystarGeometry {
pub kind: PolystarKind,
pub center: AnimatedPoint,
pub points: Animated<f32>,
pub outer_radius: Animated<f32>,
#[serde(default = "zero")]
pub inner_radius: Animated<f32>,
#[serde(default = "zero")]
pub outer_roundness: Animated<f32>,
#[serde(default = "zero")]
pub inner_roundness: Animated<f32>,
#[serde(default = "zero")]
pub rotation_degrees: Animated<f32>,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PolystarKind {
#[default]
Star,
Polygon,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PathGeometry {
pub path: Animated<Path>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Path {
pub subpaths: Vec<Subpath>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Subpath {
#[serde(default)]
pub closed: bool,
pub vertices: Vec<PathVertex>,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PathVertex {
pub point: Vec2,
#[serde(default)]
pub in_tangent: Vec2,
#[serde(default)]
pub out_tangent: Vec2,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ShapeTransform {
#[serde(default = "origin")]
pub anchor: Animated<Vec2>,
#[serde(default)]
pub position: AnimatedPoint,
#[serde(default = "unscaled")]
pub scale: Animated<Vec2>,
#[serde(default = "zero")]
pub rotation_degrees: Animated<f32>,
#[serde(default = "opaque")]
pub opacity: Animated<f32>,
}
fn origin() -> Animated<Vec2> {
Animated::Constant {
value: Vec2::new(0.0, 0.0),
}
}
fn unscaled() -> Animated<Vec2> {
Animated::Constant {
value: Vec2::new(1.0, 1.0),
}
}
impl Default for ShapeTransform {
fn default() -> Self {
Self {
anchor: origin(),
position: AnimatedPoint::default(),
scale: unscaled(),
rotation_degrees: zero(),
opacity: opaque(),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct StrokeItem {
pub paint: Animated<Paint>,
pub width: Animated<f32>,
#[serde(default = "opaque")]
pub opacity: Animated<f32>,
#[serde(default)]
pub join: LineJoin,
#[serde(default = "four")]
pub miter_limit: f32,
#[serde(default)]
pub cap: LineCap,
#[serde(default)]
pub dash: Option<Dash>,
}
const fn four() -> f32 {
4.0
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Dash {
pub pattern: Vec<Animated<f32>>,
#[serde(default = "zero")]
pub offset: Animated<f32>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TrimItem {
pub start: Animated<f32>,
pub end: Animated<f32>,
#[serde(default = "zero")]
pub offset: Animated<f32>,
#[serde(default)]
pub mode: TrimMode,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum TrimMode {
#[default]
Together,
Individually,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum FillRule {
#[default]
Nonzero,
Evenodd,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ShapeProblem {
TooManyItems,
TooDeep,
PathTooLarge,
InvalidDash,
InvalidKeyframes,
OutOfRange,
PathKeyframesDiffer,
PaintKeyframesDiffer,
}
impl fmt::Display for ShapeProblem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::TooManyItems => "has more items than a shapes generator may hold",
Self::TooDeep => "nests groups too deeply",
Self::PathTooLarge => "has more subpaths or vertices than a path may hold",
Self::InvalidDash => "has an invalid dash pattern",
Self::InvalidKeyframes => "has invalid keyframes",
Self::OutOfRange => "has a value out of range",
Self::PathKeyframesDiffer => "has path keyframes that differ in structure",
Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShapeIssue {
pub at: String,
pub problem: ShapeProblem,
}
impl From<ValueIssue> for ShapeIssue {
fn from(issue: ValueIssue) -> Self {
Self::new(
issue.at,
match issue.problem {
ValueProblem::InvalidKeyframes => ShapeProblem::InvalidKeyframes,
ValueProblem::OutOfRange => ShapeProblem::OutOfRange,
ValueProblem::PaintKeyframesDiffer => ShapeProblem::PaintKeyframesDiffer,
},
)
}
}
impl ShapeIssue {
fn new(at: impl Into<String>, problem: ShapeProblem) -> Self {
Self {
at: at.into(),
problem,
}
}
}
type Checked = Result<(), ShapeIssue>;
pub fn validate_shape_items(items: &[ShapeItem]) -> Result<(), ShapeIssue> {
let mut count = 0;
validate_items(items, "items", 1, &mut count)
}
pub(crate) fn validate_items(
items: &[ShapeItem],
at: &str,
depth: usize,
count: &mut usize,
) -> Checked {
if depth > MAX_SHAPE_DEPTH {
return Err(ShapeIssue::new(at, ShapeProblem::TooDeep));
}
for (index, item) in items.iter().enumerate() {
let at = format!("{at}[{index}]");
*count += 1;
if *count > MAX_SHAPE_ITEMS {
return Err(ShapeIssue::new(at, ShapeProblem::TooManyItems));
}
match item {
ShapeItem::Group { items, transform } => {
validate_transform(transform, &format!("{at}.transform"))?;
validate_items(items, &format!("{at}.items"), depth + 1, count)?;
}
ShapeItem::Rectangle(geometry) => validate_rectangle(geometry, &at)?,
ShapeItem::Ellipse(geometry) => validate_ellipse(geometry, &at)?,
ShapeItem::Polystar(geometry) => validate_polystar(geometry, &at)?,
ShapeItem::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path"))?,
ShapeItem::Fill { paint, opacity, .. } => {
validate_paint(paint, &format!("{at}.paint"))?;
scalar(opacity, &format!("{at}.opacity"), unit)?;
}
ShapeItem::Stroke(stroke) => validate_stroke(stroke, &at)?,
ShapeItem::Trim(trim) => {
scalar(&trim.start, &format!("{at}.start"), unit)?;
scalar(&trim.end, &format!("{at}.end"), unit)?;
scalar(&trim.offset, &format!("{at}.offset"), any)?;
}
}
}
Ok(())
}
impl ShapeGeometry {
pub fn validate(&self, at: &str) -> Result<(), ShapeIssue> {
match self {
Self::Rectangle(geometry) => validate_rectangle(geometry, at),
Self::Ellipse(geometry) => validate_ellipse(geometry, at),
Self::Polystar(geometry) => validate_polystar(geometry, at),
Self::Path(geometry) => validate_path(&geometry.path, &format!("{at}.path")),
}
}
}
fn sizes(value: Vec2) -> bool {
value.x >= 0.0 && value.y >= 0.0
}
fn validate_rectangle(geometry: &RectangleGeometry, at: &str) -> Checked {
point(&geometry.center, &format!("{at}.center"))?;
vec2(&geometry.size, &format!("{at}.size"), sizes)?;
scalar(
&geometry.corner_radius,
&format!("{at}.corner_radius"),
non_negative,
)?;
scalar(
&geometry.rotation_degrees,
&format!("{at}.rotation_degrees"),
any,
)?;
Ok(())
}
fn validate_ellipse(geometry: &EllipseGeometry, at: &str) -> Checked {
point(&geometry.center, &format!("{at}.center"))?;
vec2(&geometry.size, &format!("{at}.size"), sizes)?;
scalar(
&geometry.rotation_degrees,
&format!("{at}.rotation_degrees"),
any,
)?;
Ok(())
}
fn validate_polystar(geometry: &PolystarGeometry, at: &str) -> Checked {
point(&geometry.center, &format!("{at}.center"))?;
scalar(&geometry.points, &format!("{at}.points"), |points| {
(MIN_POLYSTAR_POINTS..=MAX_POLYSTAR_POINTS).contains(&points)
})?;
scalar(
&geometry.outer_radius,
&format!("{at}.outer_radius"),
non_negative,
)?;
scalar(
&geometry.inner_radius,
&format!("{at}.inner_radius"),
non_negative,
)?;
scalar(
&geometry.outer_roundness,
&format!("{at}.outer_roundness"),
unit,
)?;
scalar(
&geometry.inner_roundness,
&format!("{at}.inner_roundness"),
unit,
)?;
scalar(
&geometry.rotation_degrees,
&format!("{at}.rotation_degrees"),
any,
)?;
Ok(())
}
fn validate_transform(transform: &ShapeTransform, at: &str) -> Checked {
vec2(&transform.anchor, &format!("{at}.anchor"), |_| true)?;
point(&transform.position, &format!("{at}.position"))?;
vec2(&transform.scale, &format!("{at}.scale"), |_| true)?;
scalar(
&transform.rotation_degrees,
&format!("{at}.rotation_degrees"),
any,
)?;
scalar(&transform.opacity, &format!("{at}.opacity"), unit)?;
Ok(())
}
fn validate_stroke(stroke: &StrokeItem, at: &str) -> Checked {
validate_paint(&stroke.paint, &format!("{at}.paint"))?;
scalar(&stroke.width, &format!("{at}.width"), non_negative)?;
scalar(&stroke.opacity, &format!("{at}.opacity"), unit)?;
if !stroke.miter_limit.is_finite() || stroke.miter_limit < 1.0 {
return Err(ShapeIssue::new(
format!("{at}.miter_limit"),
ShapeProblem::OutOfRange,
));
}
if let Some(dash) = &stroke.dash {
let at = format!("{at}.dash");
if dash.pattern.is_empty() || dash.pattern.len() > MAX_DASH_ENTRIES {
return Err(ShapeIssue::new(
format!("{at}.pattern"),
ShapeProblem::InvalidDash,
));
}
for (index, entry) in dash.pattern.iter().enumerate() {
scalar(entry, &format!("{at}.pattern[{index}]"), non_negative)?;
}
let positive = dash.pattern.iter().any(|entry| match entry {
Animated::Constant { value } => *value > 0.0,
Animated::Keyframes { keyframes } => keyframes.iter().any(|key| key.value > 0.0),
});
if !positive {
return Err(ShapeIssue::new(
format!("{at}.pattern"),
ShapeProblem::InvalidDash,
));
}
scalar(&dash.offset, &format!("{at}.offset"), any)?;
}
Ok(())
}
fn validate_path(path: &Animated<Path>, at: &str) -> Checked {
let check_one = |path: &Path| -> Checked {
let vertices: usize = path
.subpaths
.iter()
.map(|subpath| subpath.vertices.len())
.sum();
if path.subpaths.len() > MAX_PATH_SUBPATHS || vertices > MAX_PATH_VERTICES {
return Err(ShapeIssue::new(at, ShapeProblem::PathTooLarge));
}
let finite = |value: Vec2| value.x.is_finite() && value.y.is_finite();
let all_finite = path.subpaths.iter().all(|subpath| {
subpath.vertices.iter().all(|vertex| {
finite(vertex.point) && finite(vertex.in_tangent) && finite(vertex.out_tangent)
})
});
all_finite
.then_some(())
.ok_or_else(|| ShapeIssue::new(at, ShapeProblem::OutOfRange))
};
match path {
Animated::Constant { value } => check_one(value),
Animated::Keyframes { keyframes } => {
if !valid_keyframes(keyframes) {
return Err(ShapeIssue::new(at, ShapeProblem::InvalidKeyframes));
}
for keyframe in keyframes {
check_one(&keyframe.value)?;
}
let structure = |path: &Path| {
path.subpaths
.iter()
.map(|subpath| (subpath.closed, subpath.vertices.len()))
.collect::<Vec<_>>()
};
let first = structure(&keyframes[0].value);
if keyframes[1..]
.iter()
.any(|keyframe| structure(&keyframe.value) != first)
{
return Err(ShapeIssue::new(at, ShapeProblem::PathKeyframesDiffer));
}
Ok(())
}
}
}
impl Path {
#[must_use]
pub fn vertex_count(&self) -> usize {
self.subpaths
.iter()
.map(|subpath| subpath.vertices.len())
.sum()
}
}
impl ShapeItem {
#[must_use]
pub fn is_animated(&self) -> bool {
match self {
Self::Group { items, transform } => {
transform.is_animated() || items.iter().any(Self::is_animated)
}
Self::Rectangle(geometry) => geometry.is_animated(),
Self::Ellipse(geometry) => geometry.is_animated(),
Self::Polystar(geometry) => geometry.is_animated(),
Self::Path(geometry) => keyed(&geometry.path),
Self::Fill { paint, opacity, .. } => keyed(paint) || keyed(opacity),
Self::Stroke(stroke) => {
keyed(&stroke.paint)
|| keyed(&stroke.width)
|| keyed(&stroke.opacity)
|| stroke
.dash
.as_ref()
.is_some_and(|dash| dash.pattern.iter().any(keyed) || keyed(&dash.offset))
}
Self::Trim(trim) => keyed(&trim.start) || keyed(&trim.end) || keyed(&trim.offset),
}
}
}
impl ShapeGeometry {
#[must_use]
pub fn is_animated(&self) -> bool {
match self {
Self::Rectangle(geometry) => geometry.is_animated(),
Self::Ellipse(geometry) => geometry.is_animated(),
Self::Polystar(geometry) => geometry.is_animated(),
Self::Path(geometry) => keyed(&geometry.path),
}
}
}
impl RectangleGeometry {
fn is_animated(&self) -> bool {
self.center.is_animated()
|| keyed(&self.size)
|| keyed(&self.corner_radius)
|| keyed(&self.rotation_degrees)
}
}
impl EllipseGeometry {
fn is_animated(&self) -> bool {
self.center.is_animated() || keyed(&self.size) || keyed(&self.rotation_degrees)
}
}
impl PolystarGeometry {
fn is_animated(&self) -> bool {
self.center.is_animated()
|| keyed(&self.points)
|| keyed(&self.outer_radius)
|| keyed(&self.inner_radius)
|| keyed(&self.outer_roundness)
|| keyed(&self.inner_roundness)
|| keyed(&self.rotation_degrees)
}
}
impl ShapeTransform {
fn is_animated(&self) -> bool {
keyed(&self.anchor)
|| self.position.is_animated()
|| keyed(&self.scale)
|| keyed(&self.rotation_degrees)
|| keyed(&self.opacity)
}
}
const fn keyed<T>(animation: &Animated<T>) -> bool {
matches!(animation, Animated::Keyframes { .. })
}