Skip to main content

alpha2_geometry/
shapes.rs

1//! Parametric shapes as Bezier paths in pixel space, with the start points and directions After
2//! Effects and Lottie use so trims cut them in the same places.
3
4use crate::affine::rotation_about;
5use kurbo::{BezPath, Point, Vec2};
6
7/// The four-cubic circle constant: control-point distance along the end tangents as a fraction
8/// of the radius.
9pub const KAPPA: f64 = 0.552_284_749_830_793_6;
10
11/// A rectangle of full `size` about `center`, corners rounded by `radius` (capped at half the
12/// shorter side), rotated `degrees` clockwise. It starts at the top-right corner (below the arc
13/// when rounded) and runs clockwise.
14#[must_use]
15pub fn rectangle(center: Point, size: Vec2, radius: f64, degrees: f64) -> BezPath {
16    let (hw, hh) = (size.x.abs() * 0.5, size.y.abs() * 0.5);
17    let r = radius.max(0.0).min(hw).min(hh);
18    let (left, right, top, bottom) = (center.x - hw, center.x + hw, center.y - hh, center.y + hh);
19    let mut path = BezPath::new();
20    if r <= 0.0 {
21        path.move_to((right, top));
22        path.line_to((right, bottom));
23        path.line_to((left, bottom));
24        path.line_to((left, top));
25        path.close_path();
26    } else {
27        let k = r * (1.0 - KAPPA);
28        path.move_to((right, top + r));
29        path.line_to((right, bottom - r));
30        path.curve_to(
31            (right, bottom - k),
32            (right - k, bottom),
33            (right - r, bottom),
34        );
35        path.line_to((left + r, bottom));
36        path.curve_to((left + k, bottom), (left, bottom - k), (left, bottom - r));
37        path.line_to((left, top + r));
38        path.curve_to((left, top + k), (left + k, top), (left + r, top));
39        path.line_to((right - r, top));
40        path.curve_to((right - k, top), (right, top + k), (right, top + r));
41        path.close_path();
42    }
43    turned(path, center, degrees)
44}
45
46/// The ellipse inscribed in `size` about `center`, rotated `degrees` clockwise, as four cubics
47/// clockwise from the top.
48#[must_use]
49pub fn ellipse(center: Point, size: Vec2, degrees: f64) -> BezPath {
50    let (rx, ry) = (size.x.abs() * 0.5, size.y.abs() * 0.5);
51    let (kx, ky) = (rx * KAPPA, ry * KAPPA);
52    let (x, y) = (center.x, center.y);
53    let mut path = BezPath::new();
54    path.move_to((x, y - ry));
55    path.curve_to((x + kx, y - ry), (x + rx, y - ky), (x + rx, y));
56    path.curve_to((x + rx, y + ky), (x + kx, y + ry), (x, y + ry));
57    path.curve_to((x - kx, y + ry), (x - rx, y + ky), (x - rx, y));
58    path.curve_to((x - rx, y - ky), (x - kx, y - ry), (x, y - ry));
59    path.close_path();
60    turned(path, center, degrees)
61}
62
63/// A star or polygon, as a Lottie polystar draws it.
64#[derive(Copy, Clone, Debug, PartialEq)]
65pub struct Polystar {
66    /// Whether it has inner corners.
67    pub star: bool,
68    /// Its centre, in pixels.
69    pub center: Point,
70    /// Number of points; the fractional part is dropped.
71    pub points: f64,
72    /// The points' distance from the centre, in pixels.
73    pub outer_radius: f64,
74    /// A star's inner corners' distance from the centre, in pixels.
75    pub inner_radius: f64,
76    /// How round the points are, in `0..=1`.
77    pub outer_roundness: f64,
78    /// How round a star's inner corners are, in `0..=1`.
79    pub inner_roundness: f64,
80    /// Clockwise rotation, in degrees.
81    pub degrees: f64,
82}
83
84/// A polystar's outline, starting straight up from the centre before rotation and running
85/// clockwise. Corner tangents follow the circle through the corner, with length roundness times
86/// a quarter (polygon) or half (star) of the arc per corner, as in Lottie.
87#[must_use]
88pub fn polystar(star: &Polystar) -> BezPath {
89    // Floored as Lottie floors it; validation keeps it at three or more.
90    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
91    let points = star.points.max(0.0).floor().min(1e4) as u32;
92    let mut path = BezPath::new();
93    if points == 0 {
94        return path;
95    }
96    let corners = if star.star { points * 2 } else { points };
97    let step = std::f64::consts::TAU / f64::from(corners);
98    let arc = |radius: f64| std::f64::consts::TAU * radius / f64::from(corners * 2);
99    let mut vertices = Vec::with_capacity(corners as usize);
100    for corner in 0..corners {
101        let outer = !star.star || corner % 2 == 0;
102        let (radius, roundness) = if outer {
103            (star.outer_radius, star.outer_roundness)
104        } else {
105            (star.inner_radius, star.inner_roundness)
106        };
107        let angle = -std::f64::consts::FRAC_PI_2 + step * f64::from(corner);
108        let (sin, cos) = (libm::sin(angle), libm::cos(angle));
109        let offset = Vec2::new(radius * cos, radius * sin);
110        // Clockwise along the circle, the direction of travel.
111        let tangent = Vec2::new(-sin, cos);
112        let segment = if star.star {
113            arc(radius)
114        } else {
115            arc(radius) * 0.5
116        };
117        let handle = tangent * (segment * roundness);
118        vertices.push((star.center + offset, handle));
119    }
120    path.move_to(vertices[0].0);
121    for index in 0..vertices.len() {
122        let (from, from_handle) = vertices[index];
123        let (to, to_handle) = vertices[(index + 1) % vertices.len()];
124        if from_handle == Vec2::ZERO && to_handle == Vec2::ZERO {
125            path.line_to(to);
126        } else {
127            path.curve_to(from + from_handle, to - to_handle, to);
128        }
129    }
130    path.close_path();
131    turned(path, star.center, star.degrees)
132}
133
134/// A Bezier subpath vertex in pixels, with tangents relative to the point.
135#[derive(Copy, Clone, Debug, Default, PartialEq)]
136pub struct Vertex {
137    /// The vertex position.
138    pub point: Point,
139    /// The control point of the segment ending here, relative to `point`.
140    pub in_tangent: Vec2,
141    /// The control point of the segment starting here, relative to `point`.
142    pub out_tangent: Vec2,
143}
144
145/// Appends a subpath through `vertices`, using lines where both tangents are zero and cubics
146/// elsewhere, and returning to the first vertex when `closed`.
147pub fn append_subpath(path: &mut BezPath, vertices: &[Vertex], closed: bool) {
148    let Some(first) = vertices.first() else {
149        return;
150    };
151    path.move_to(first.point);
152    let segment = |path: &mut BezPath, from: &Vertex, to: &Vertex| {
153        if from.out_tangent == Vec2::ZERO && to.in_tangent == Vec2::ZERO {
154            path.line_to(to.point);
155        } else {
156            path.curve_to(
157                from.point + from.out_tangent,
158                to.point + to.in_tangent,
159                to.point,
160            );
161        }
162    };
163    for pair in vertices.windows(2) {
164        segment(path, &pair[0], &pair[1]);
165    }
166    if closed {
167        if vertices.len() > 1 {
168            segment(path, &vertices[vertices.len() - 1], first);
169        }
170        path.close_path();
171    }
172}
173
174fn turned(path: BezPath, center: Point, degrees: f64) -> BezPath {
175    if degrees == 0.0 {
176        path
177    } else {
178        rotation_about(degrees, center) * path
179    }
180}