Skip to main content

flowkit/
corner.rs

1use std::ops::Sub;
2
3use glam::Vec2;
4
5use crate::winding_order::WindingOrder;
6
7/// Identifies a corner of a rectangle.
8///
9/// ```text
10///  [-1.0, 1.0]       [1.0, 1.0]
11///      TopLeft┌─────┐TopRight
12///             │     │
13///   BottomLeft└─────┘BottomRight
14/// [-1.0, -1.0]       [1.0, -1.0]
15/// ```
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum Corner {
18    TopRight,
19    TopLeft,
20    BottomLeft,
21    BottomRight,
22    // An invalid corner indicator.
23    Invalid,
24}
25
26impl Corner {
27    /// A vector representation of the top-right corner.
28    pub const TOP_RIGHT: Vec2 = Vec2::ONE;
29
30    /// A vector representation of the top-left corner.
31    pub const TOP_LEFT: Vec2 = Vec2::new(-1.0, 1.0);
32
33    /// A vector representation of the bottom-left corner.
34    pub const BOTTOM_LEFT: Vec2 = Vec2::NEG_ONE;
35
36    /// A vector representation of the bottom-right corner.
37    pub const BOTTOM_RIGHT: Vec2 = Vec2::new(1.0, -1.0);
38}
39
40impl Corner {
41    /// Creates a new corner from a vector.
42    #[inline]
43    pub const fn new(value: Vec2) -> Self {
44        match value {
45            Self::TOP_RIGHT => Self::TopRight,
46            Self::TOP_LEFT => Self::TopLeft,
47            Self::BOTTOM_LEFT => Self::BottomLeft,
48            Self::BOTTOM_RIGHT => Self::BottomRight,
49            _ => Self::Invalid,
50        }
51    }
52
53    /// Calculates the corner based on the previous, current, and next points.
54    #[inline]
55    pub fn calculate(prev: Vec2, current: Vec2, next: Vec2) -> Self {
56        let center = prev.midpoint(next);
57        let sign = current.sub(center).signum();
58        Self::new(sign)
59    }
60
61    /// Casts a corner to a vector.
62    #[inline]
63    pub const fn as_vec2(&self) -> Vec2 {
64        match self {
65            Self::TopRight => Self::TOP_RIGHT,
66            Self::TopLeft => Self::TOP_LEFT,
67            Self::BottomLeft => Self::BOTTOM_LEFT,
68            Self::BottomRight => Self::BOTTOM_RIGHT,
69            Self::Invalid => Vec2::ZERO,
70        }
71    }
72}
73
74/// Calculates the parameters for a corner path.
75///
76/// Links:
77/// * [Desperately seeking squircles](https://www.figma.com/blog/desperately-seeking-squircles/)
78/// * [Flutter implementation](https://github.com/aloisdeniel/figma_squircle)
79/// * [JavaScript implementation](https://github.com/phamfoo/figma-squircle)
80#[derive(Clone, Copy)]
81pub struct CornerPathParams {
82    pub a: f32,
83    pub b: f32,
84    pub c: f32,
85    pub d: f32,
86    pub p: f32,
87    pub corner_radius: f32,
88    pub arc_section_length: f32,
89    pub arc_theta: f32,
90}
91
92impl CornerPathParams {
93    #[inline]
94    pub fn new(mut corner_radius: f32, max_radius: f32, smoothness: f32) -> Self {
95        // From figure 12.2 in the article
96        let mut p = (1.0 + smoothness) * corner_radius;
97
98        if p > max_radius {
99            p = max_radius;
100            corner_radius = p / (1.0 + smoothness);
101        }
102
103        let quarter = f32::to_radians(45.0);
104        let angle_alpha = quarter * smoothness;
105        let angle_beta = (90.0 * (1.0 - smoothness)).to_radians();
106        let arc_theta = quarter - angle_beta * 0.5;
107
108        // This was called `h_longest` in the original code
109        // In the article this is the distance between 2 control points: P3 and P4
110        let p3_to_p4_distance = corner_radius * arc_theta.tan() * 0.5;
111
112        // This was called `l` in the original code
113        let arc_section_length = corner_radius * f32::sqrt(2.0) * angle_beta.sin() * 0.5;
114
115        // From figure 11.1 in the article
116        // a, b, c and d
117        let c = p3_to_p4_distance * angle_alpha.cos();
118        let d = c * angle_alpha.tan();
119        let b = (p - arc_section_length - c - d) / 3.0;
120        let a = b * 2.0;
121
122        Self {
123            a,
124            b,
125            c,
126            d,
127            p,
128            corner_radius,
129            arc_section_length,
130            arc_theta,
131        }
132    }
133
134    /// Generate actions for a smooth corner.
135    ///
136    /// Clockwise and horizontal by default.
137    /// Uses the `top-right` corner as the base model.
138    #[inline]
139    pub fn squircle(self, current: Vec2, corner: Corner, winding_order: WindingOrder) -> Squircle {
140        let Self {
141            a,
142            b,
143            c,
144            d,
145            p,
146            corner_radius,
147            arc_theta,
148            ..
149        } = self;
150
151        // directions
152        let edges = corner.as_vec2();
153        let orientation = winding_order.as_f32();
154        let Vec2 { x, y } = edges;
155        let product = x * y;
156
157        // counter-clockwise
158        let is_ccw = orientation == 1.0;
159        // top-left or bottom-right
160        let is_tl_or_br = product == -1.0;
161        // it only takes one swap
162        let should_swap = is_ccw ^ is_tl_or_br;
163
164        // arc center
165        let radii = Vec2::splat(corner_radius);
166        let center = current - radii * edges;
167        let sweep_angle = arc_theta * orientation;
168
169        // calculates new `d` by multiplying product with `-d`
170        let d = product * -d;
171
172        // horizontal direction
173        let mut h = {
174            let p0 = current - Vec2::new(p, 0.0) * x;
175            let ctrl1 = p0 + Vec2::new(a, 0.0) * x;
176            let ctrl2 = p0 + Vec2::new(a + b, 0.0) * x;
177            let to0 = p0 + Vec2::new(a + b + c, d) * x;
178            [p0, ctrl1, ctrl2, to0]
179        };
180        // vertical direction
181        let mut v = {
182            let p0 = current - Vec2::new(0.0, p) * y;
183            let ctrl1 = p0 + Vec2::new(0.0, a) * y;
184            let ctrl2 = p0 + Vec2::new(0.0, a + b) * y;
185            let to0 = p0 + Vec2::new(d, a + b + c) * y;
186            [p0, ctrl1, ctrl2, to0]
187        };
188
189        if should_swap {
190            ::core::mem::swap(&mut h, &mut v);
191        }
192
193        Squircle {
194            h,
195            v,
196            center,
197            radii,
198            sweep_angle,
199        }
200    }
201}
202
203/// A squircle's corner.
204///
205/// ```text
206///              p          center
207///              | ctrl1     /
208///              | | ctrl2  /
209///              | | | to  /
210/// horizontal ↔ ↓ ↓ ↓ ↓  /
211///            ╭────────╮
212///            |        │ ← to
213///            |        │ ←--- ctrl2
214///            |        │ ←------ ctrl1
215///            |        │ ←--------- p
216///            ╰────────╯ ↕ vertical
217/// ```
218///
219/// | Direction  |  Points                 |
220/// |------------|-------------------------|
221/// | horizontal | `[p, ctrl1, ctrl2, to]` |
222/// | vertical   | `[to, ctrl2, ctrl1, p]` |
223#[derive(Clone, Copy)]
224pub struct Squircle {
225    pub h: [Vec2; 4],
226    pub v: [Vec2; 4],
227    pub center: Vec2,
228    pub radii: Vec2,
229    pub sweep_angle: f32,
230}