freya_core/style/
corner_radius.rs

1use std::f32::consts::SQRT_2;
2
3use freya_engine::prelude::*;
4use torin::scaled::Scaled;
5
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(PartialEq, Clone, Debug, Default, Copy)]
8pub struct CornerRadius {
9    pub top_left: f32,
10    pub top_right: f32,
11    pub bottom_right: f32,
12    pub bottom_left: f32,
13    pub smoothing: f32,
14}
15
16impl From<f32> for CornerRadius {
17    fn from(value: f32) -> Self {
18        CornerRadius::new_all(value)
19    }
20}
21
22impl CornerRadius {
23    pub const fn new_all(radius: f32) -> Self {
24        Self {
25            top_left: radius,
26            top_right: radius,
27            bottom_right: radius,
28            bottom_left: radius,
29            smoothing: 0.,
30        }
31    }
32
33    pub fn fill_top(&mut self, value: f32) {
34        self.top_left = value;
35        self.top_right = value;
36    }
37
38    pub fn fill_bottom(&mut self, value: f32) {
39        self.bottom_left = value;
40        self.bottom_right = value;
41    }
42
43    pub fn fill_all(&mut self, value: f32) {
44        self.fill_bottom(value);
45        self.fill_top(value);
46    }
47
48    // https://github.com/aloisdeniel/figma_squircle/blob/main/lib/src/path_smooth_corners.dart
49    pub fn smoothed_path(&self, rect: RRect) -> Path {
50        let mut path = Path::new();
51
52        let width = rect.width();
53        let height = rect.height();
54
55        let top_right = rect.radii(SkCorner::UpperRight).x;
56        if top_right > 0.0 {
57            let (a, b, c, d, l, p, radius) =
58                compute_smooth_corner(top_right, self.smoothing, width, height);
59
60            path.move_to((f32::max(width / 2.0, width - p), 0.0))
61                .cubic_to(
62                    (width - (p - a), 0.0),
63                    (width - (p - a - b), 0.0),
64                    (width - (p - a - b - c), d),
65                )
66                .r_arc_to_rotated(
67                    (radius, radius),
68                    0.0,
69                    SkArcSize::Small,
70                    PathDirection::CW,
71                    (l, l),
72                )
73                .cubic_to(
74                    (width, p - a - b),
75                    (width, p - a),
76                    (width, f32::min(height / 2.0, p)),
77                );
78        } else {
79            path.move_to((width / 2.0, 0.0))
80                .line_to((width, 0.0))
81                .line_to((width, height / 2.0));
82        }
83
84        let bottom_right = rect.radii(SkCorner::LowerRight).x;
85        if bottom_right > 0.0 {
86            let (a, b, c, d, l, p, radius) =
87                compute_smooth_corner(bottom_right, self.smoothing, width, height);
88
89            path.line_to((width, f32::max(height / 2.0, height - p)))
90                .cubic_to(
91                    (width, height - (p - a)),
92                    (width, height - (p - a - b)),
93                    (width - d, height - (p - a - b - c)),
94                )
95                .r_arc_to_rotated(
96                    (radius, radius),
97                    0.0,
98                    ArcSize::Small,
99                    PathDirection::CW,
100                    (-l, l),
101                )
102                .cubic_to(
103                    (width - (p - a - b), height),
104                    (width - (p - a), height),
105                    (f32::max(width / 2.0, width - p), height),
106                );
107        } else {
108            path.line_to((width, height)).line_to((width / 2.0, height));
109        }
110
111        let bottom_left = rect.radii(SkCorner::LowerLeft).x;
112        if bottom_left > 0.0 {
113            let (a, b, c, d, l, p, radius) =
114                compute_smooth_corner(bottom_left, self.smoothing, width, height);
115
116            path.line_to((f32::min(width / 2.0, p), height))
117                .cubic_to(
118                    (p - a, height),
119                    (p - a - b, height),
120                    (p - a - b - c, height - d),
121                )
122                .r_arc_to_rotated(
123                    (radius, radius),
124                    0.0,
125                    ArcSize::Small,
126                    PathDirection::CW,
127                    (-l, -l),
128                )
129                .cubic_to(
130                    (0.0, height - (p - a - b)),
131                    (0.0, height - (p - a)),
132                    (0.0, f32::max(height / 2.0, height - p)),
133                );
134        } else {
135            path.line_to((0.0, height)).line_to((0.0, height / 2.0));
136        }
137
138        let top_left = rect.radii(SkCorner::UpperLeft).x;
139        if top_left > 0.0 {
140            let (a, b, c, d, l, p, radius) =
141                compute_smooth_corner(top_left, self.smoothing, width, height);
142
143            path.line_to((0.0, f32::min(height / 2.0, p)))
144                .cubic_to((0.0, p - a), (0.0, p - a - b), (d, p - a - b - c))
145                .r_arc_to_rotated(
146                    (radius, radius),
147                    0.0,
148                    ArcSize::Small,
149                    PathDirection::CW,
150                    (l, -l),
151                )
152                .cubic_to(
153                    (p - a - b, 0.0),
154                    (p - a, 0.0),
155                    (f32::min(width / 2.0, p), 0.0),
156                );
157        } else {
158            path.line_to((0.0, 0.0));
159        }
160
161        path.close();
162        path
163    }
164
165    pub fn pretty(&self) -> String {
166        format!(
167            "({}, {}, {}, {})",
168            self.top_left, self.top_right, self.bottom_right, self.bottom_left
169        )
170    }
171}
172
173// https://www.figma.com/blog/desperately-seeking-squircles/
174fn compute_smooth_corner(
175    corner_radius: f32,
176    smoothing: f32,
177    width: f32,
178    height: f32,
179) -> (f32, f32, f32, f32, f32, f32, f32) {
180    let max_p = f32::min(width, height) / 2.0;
181    let corner_radius = f32::min(corner_radius, max_p);
182
183    let p = f32::min((1.0 + smoothing) * corner_radius, max_p);
184
185    let angle_alpha: f32;
186    let angle_beta: f32;
187
188    if corner_radius <= max_p / 2.0 {
189        angle_alpha = 45.0 * smoothing;
190        angle_beta = 90.0 * (1.0 - smoothing);
191    } else {
192        let diff_ratio = (corner_radius - max_p / 2.0) / (max_p / 2.0);
193
194        angle_alpha = 45.0 * smoothing * (1.0 - diff_ratio);
195        angle_beta = 90.0 * (1.0 - smoothing * (1.0 - diff_ratio));
196    }
197
198    let angle_theta = (90.0 - angle_beta) / 2.0;
199    let dist_p3_p4 = corner_radius * (angle_theta / 2.0).to_radians().tan();
200
201    let l = (angle_beta / 2.0).to_radians().sin() * corner_radius * SQRT_2;
202    let c = dist_p3_p4 * angle_alpha.to_radians().cos();
203    let d = c * angle_alpha.to_radians().tan();
204    let b = (p - l - c - d) / 3.0;
205    let a = 2.0 * b;
206
207    (a, b, c, d, l, p, corner_radius)
208}
209
210impl Scaled for CornerRadius {
211    fn scale(&mut self, scale: f32) {
212        self.top_left *= scale;
213        self.top_right *= scale;
214        self.bottom_left *= scale;
215        self.bottom_right *= scale;
216    }
217}