Skip to main content

alpha2_geometry/
shapes.rs

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