freya_core/values/
corner_radius.rs

1use std::{
2    f32::consts::SQRT_2,
3    fmt,
4};
5
6use freya_engine::prelude::*;
7use torin::scaled::Scaled;
8
9use crate::parsing::{
10    Parse,
11    ParseError,
12};
13
14#[derive(PartialEq, Clone, Debug, Default, Copy)]
15pub struct CornerRadius {
16    pub top_left: f32,
17    pub top_right: f32,
18    pub bottom_right: f32,
19    pub bottom_left: f32,
20    pub smoothing: f32,
21}
22
23// https://www.figma.com/blog/desperately-seeking-squircles/
24fn compute_smooth_corner(
25    corner_radius: f32,
26    smoothing: f32,
27    width: f32,
28    height: f32,
29) -> (f32, f32, f32, f32, f32, f32, f32) {
30    let max_p = f32::min(width, height) / 2.0;
31    let corner_radius = f32::min(corner_radius, max_p);
32
33    let p = f32::min((1.0 + smoothing) * corner_radius, max_p);
34
35    let angle_alpha: f32;
36    let angle_beta: f32;
37
38    if corner_radius <= max_p / 2.0 {
39        angle_alpha = 45.0 * smoothing;
40        angle_beta = 90.0 * (1.0 - smoothing);
41    } else {
42        let diff_ratio = (corner_radius - max_p / 2.0) / (max_p / 2.0);
43
44        angle_alpha = 45.0 * smoothing * (1.0 - diff_ratio);
45        angle_beta = 90.0 * (1.0 - smoothing * (1.0 - diff_ratio));
46    }
47
48    let angle_theta = (90.0 - angle_beta) / 2.0;
49    let dist_p3_p4 = corner_radius * (angle_theta / 2.0).to_radians().tan();
50
51    let l = (angle_beta / 2.0).to_radians().sin() * corner_radius * SQRT_2;
52    let c = dist_p3_p4 * angle_alpha.to_radians().cos();
53    let d = c * angle_alpha.to_radians().tan();
54    let b = (p - l - c - d) / 3.0;
55    let a = 2.0 * b;
56
57    (a, b, c, d, l, p, corner_radius)
58}
59
60impl CornerRadius {
61    pub fn fill_top(&mut self, value: f32) {
62        self.top_left = value;
63        self.top_right = value;
64    }
65
66    pub fn fill_bottom(&mut self, value: f32) {
67        self.bottom_left = value;
68        self.bottom_right = value;
69    }
70
71    pub fn fill_all(&mut self, value: f32) {
72        self.fill_bottom(value);
73        self.fill_top(value);
74    }
75
76    // https://github.com/aloisdeniel/figma_squircle/blob/main/lib/src/path_smooth_corners.dart
77    pub fn smoothed_path(&self, rect: RRect) -> Path {
78        let mut path = Path::new();
79
80        let width = rect.width();
81        let height = rect.height();
82
83        let top_right = rect.radii(Corner::UpperRight).x;
84        if top_right > 0.0 {
85            let (a, b, c, d, l, p, radius) =
86                compute_smooth_corner(top_right, self.smoothing, width, height);
87
88            path.move_to((f32::max(width / 2.0, width - p), 0.0))
89                .cubic_to(
90                    (width - (p - a), 0.0),
91                    (width - (p - a - b), 0.0),
92                    (width - (p - a - b - c), d),
93                )
94                .r_arc_to_rotated(
95                    (radius, radius),
96                    0.0,
97                    ArcSize::Small,
98                    PathDirection::CW,
99                    (l, l),
100                )
101                .cubic_to(
102                    (width, p - a - b),
103                    (width, p - a),
104                    (width, f32::min(height / 2.0, p)),
105                );
106        } else {
107            path.move_to((width / 2.0, 0.0))
108                .line_to((width, 0.0))
109                .line_to((width, height / 2.0));
110        }
111
112        let bottom_right = rect.radii(Corner::LowerRight).x;
113        if bottom_right > 0.0 {
114            let (a, b, c, d, l, p, radius) =
115                compute_smooth_corner(bottom_right, self.smoothing, width, height);
116
117            path.line_to((width, f32::max(height / 2.0, height - p)))
118                .cubic_to(
119                    (width, height - (p - a)),
120                    (width, height - (p - a - b)),
121                    (width - d, height - (p - a - b - c)),
122                )
123                .r_arc_to_rotated(
124                    (radius, radius),
125                    0.0,
126                    ArcSize::Small,
127                    PathDirection::CW,
128                    (-l, l),
129                )
130                .cubic_to(
131                    (width - (p - a - b), height),
132                    (width - (p - a), height),
133                    (f32::max(width / 2.0, width - p), height),
134                );
135        } else {
136            path.line_to((width, height)).line_to((width / 2.0, height));
137        }
138
139        let bottom_left = rect.radii(Corner::LowerLeft).x;
140        if bottom_left > 0.0 {
141            let (a, b, c, d, l, p, radius) =
142                compute_smooth_corner(bottom_left, self.smoothing, width, height);
143
144            path.line_to((f32::min(width / 2.0, p), height))
145                .cubic_to(
146                    (p - a, height),
147                    (p - a - b, height),
148                    (p - a - b - c, height - d),
149                )
150                .r_arc_to_rotated(
151                    (radius, radius),
152                    0.0,
153                    ArcSize::Small,
154                    PathDirection::CW,
155                    (-l, -l),
156                )
157                .cubic_to(
158                    (0.0, height - (p - a - b)),
159                    (0.0, height - (p - a)),
160                    (0.0, f32::max(height / 2.0, height - p)),
161                );
162        } else {
163            path.line_to((0.0, height)).line_to((0.0, height / 2.0));
164        }
165
166        let top_left = rect.radii(Corner::UpperLeft).x;
167        if top_left > 0.0 {
168            let (a, b, c, d, l, p, radius) =
169                compute_smooth_corner(top_left, self.smoothing, width, height);
170
171            path.line_to((0.0, f32::min(height / 2.0, p)))
172                .cubic_to((0.0, p - a), (0.0, p - a - b), (d, p - a - b - c))
173                .r_arc_to_rotated(
174                    (radius, radius),
175                    0.0,
176                    ArcSize::Small,
177                    PathDirection::CW,
178                    (l, -l),
179                )
180                .cubic_to(
181                    (p - a - b, 0.0),
182                    (p - a, 0.0),
183                    (f32::min(width / 2.0, p), 0.0),
184                );
185        } else {
186            path.line_to((0.0, 0.0));
187        }
188
189        path.close();
190        path
191    }
192
193    pub fn pretty(&self) -> String {
194        format!(
195            "({}, {}, {}, {})",
196            self.top_left, self.top_right, self.bottom_right, self.bottom_left
197        )
198    }
199}
200
201impl Parse for CornerRadius {
202    fn parse(value: &str) -> Result<Self, ParseError> {
203        let mut radius = CornerRadius::default();
204        let mut values = value.split_ascii_whitespace();
205
206        match values.clone().count() {
207            // Same in all corners
208            1 => {
209                radius.fill_all(
210                    values
211                        .next()
212                        .ok_or(ParseError)?
213                        .parse::<f32>()
214                        .map_err(|_| ParseError)?,
215                );
216            }
217            // By Top and Bottom
218            2 => {
219                // Top
220                radius.fill_top(
221                    values
222                        .next()
223                        .ok_or(ParseError)?
224                        .parse::<f32>()
225                        .map_err(|_| ParseError)?,
226                );
227
228                // Bottom
229                radius.fill_bottom(
230                    values
231                        .next()
232                        .ok_or(ParseError)?
233                        .parse::<f32>()
234                        .map_err(|_| ParseError)?,
235                )
236            }
237            // Each corner
238            4 => {
239                radius = CornerRadius {
240                    top_left: values
241                        .next()
242                        .ok_or(ParseError)?
243                        .parse::<f32>()
244                        .map_err(|_| ParseError)?,
245                    top_right: values
246                        .next()
247                        .ok_or(ParseError)?
248                        .parse::<f32>()
249                        .map_err(|_| ParseError)?,
250                    bottom_right: values
251                        .next()
252                        .ok_or(ParseError)?
253                        .parse::<f32>()
254                        .map_err(|_| ParseError)?,
255                    bottom_left: values
256                        .next()
257                        .ok_or(ParseError)?
258                        .parse::<f32>()
259                        .map_err(|_| ParseError)?,
260                    ..Default::default()
261                }
262            }
263            _ => return Err(ParseError),
264        }
265
266        Ok(radius)
267    }
268}
269
270impl fmt::Display for CornerRadius {
271    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272        write!(
273            f,
274            "{} {} {} {}",
275            self.top_left, self.top_right, self.bottom_left, self.bottom_right
276        )
277    }
278}
279
280impl Scaled for CornerRadius {
281    fn scale(&mut self, scale: f32) {
282        self.top_left *= scale;
283        self.top_right *= scale;
284        self.bottom_left *= scale;
285        self.bottom_right *= scale;
286    }
287}