Skip to main content

gpui_kit_material/shapes/
catalog.rs

1// Copyright 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy
5// at http://www.apache.org/licenses/LICENSE-2.0 (see LICENSE-androidx next to
6// this file). Unless required by applicable law or agreed to in writing,
7// software distributed under the License is distributed on an "AS IS" BASIS,
8// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9//
10// Ported to Rust from Compose Material3's MaterialShapes.kt
11// (compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/).
12//! The M3 Expressive shape library: the outlines of Compose's
13//! `MaterialShapes`. Every value here is that source's; neither the Figma kit
14//! nor md3.io defines these outlines, and m3e ports the same file.
15use super::morph::Morph;
16use super::polygon::{CornerRounding, Pt, RoundedPolygon, angle_degrees, cos, pt, sin};
17use std::sync::OnceLock;
18
19/// One of the 35 shapes of the M3 Expressive shape library.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21#[allow(missing_docs)] // Each variant is named for the shape it draws.
22pub enum ShapeKind {
23    Circle,
24    Square,
25    Slanted,
26    Arch,
27    Fan,
28    Arrow,
29    SemiCircle,
30    Oval,
31    Pill,
32    Triangle,
33    Diamond,
34    ClamShell,
35    Pentagon,
36    Gem,
37    Sunny,
38    VerySunny,
39    Cookie4Sided,
40    Cookie6Sided,
41    Cookie7Sided,
42    Cookie9Sided,
43    Cookie12Sided,
44    Ghostish,
45    Clover4Leaf,
46    Clover8Leaf,
47    Burst,
48    SoftBurst,
49    Boom,
50    SoftBoom,
51    Flower,
52    Puffy,
53    PuffyDiamond,
54    PixelCircle,
55    PixelTriangle,
56    Bun,
57    Heart,
58}
59
60const COUNT: usize = 35;
61
62static POLYGONS: [OnceLock<RoundedPolygon>; COUNT] = [const { OnceLock::new() }; COUNT];
63// Boxed: most pairs are never asked for, and a `Morph` is several words wide.
64static MORPHS: [OnceLock<Box<Morph>>; COUNT * COUNT] = [const { OnceLock::new() }; COUNT * COUNT];
65
66impl ShapeKind {
67    /// Every shape, in the order of Compose's `MaterialShapes`.
68    pub const ALL: [Self; COUNT] = [
69        Self::Circle,
70        Self::Square,
71        Self::Slanted,
72        Self::Arch,
73        Self::Fan,
74        Self::Arrow,
75        Self::SemiCircle,
76        Self::Oval,
77        Self::Pill,
78        Self::Triangle,
79        Self::Diamond,
80        Self::ClamShell,
81        Self::Pentagon,
82        Self::Gem,
83        Self::Sunny,
84        Self::VerySunny,
85        Self::Cookie4Sided,
86        Self::Cookie6Sided,
87        Self::Cookie7Sided,
88        Self::Cookie9Sided,
89        Self::Cookie12Sided,
90        Self::Ghostish,
91        Self::Clover4Leaf,
92        Self::Clover8Leaf,
93        Self::Burst,
94        Self::SoftBurst,
95        Self::Boom,
96        Self::SoftBoom,
97        Self::Flower,
98        Self::Puffy,
99        Self::PuffyDiamond,
100        Self::PixelCircle,
101        Self::PixelTriangle,
102        Self::Bun,
103        Self::Heart,
104    ];
105
106    /// The outline, normalized to a 0-1 box. Built once per shape, on first
107    /// use, and kept for the life of the process.
108    pub fn polygon(self) -> &'static RoundedPolygon {
109        POLYGONS[self as usize].get_or_init(|| outline(self).normalized())
110    }
111
112    /// The morph from this shape to `to`. Built once per ordered pair, on
113    /// first use, and kept for the life of the process.
114    pub fn morph_to(self, to: Self) -> &'static Morph {
115        MORPHS[self as usize * COUNT + to as usize]
116            .get_or_init(|| Box::new(Morph::new(self.polygon(), to.polygon())))
117    }
118}
119
120const R15: CornerRounding = CornerRounding::new(0.15);
121const R20: CornerRounding = CornerRounding::new(0.2);
122const R30: CornerRounding = CornerRounding::new(0.3);
123const R50: CornerRounding = CornerRounding::new(0.5);
124const R100: CornerRounding = CornerRounding::new(1.);
125const SHARP: CornerRounding = CornerRounding::UNROUNDED;
126const ORIGIN: (f32, f32) = (0., 0.);
127const PI: f32 = std::f32::consts::PI;
128
129const fn p(x: f32, y: f32) -> (Pt, CornerRounding) {
130    (pt(x, y), SHARP)
131}
132const fn r(x: f32, y: f32, radius: f32) -> (Pt, CornerRounding) {
133    (pt(x, y), CornerRounding::new(radius))
134}
135const fn s(x: f32, y: f32, radius: f32, smoothing: f32) -> (Pt, CornerRounding) {
136    (pt(x, y), CornerRounding::smoothed(radius, smoothing))
137}
138
139fn to_radians(degrees: f32) -> f32 {
140    degrees / 360. * 2. * PI
141}
142
143/// The source's `doRepeat`: turn the section's points around (0.5, 0.5),
144/// reflecting every other section when `mirroring`.
145fn repeated(
146    points: &[(Pt, CornerRounding)],
147    reps: usize,
148    mirroring: bool,
149) -> Vec<(Pt, CornerRounding)> {
150    let center = pt(0.5, 0.5);
151    let mut out = Vec::new();
152    if mirroring {
153        let angles: Vec<f32> = points.iter().map(|q| angle_degrees(q.0 - center)).collect();
154        let distances: Vec<f32> = points.iter().map(|q| (q.0 - center).distance()).collect();
155        let actual_reps = reps * 2;
156        let section = 360. / actual_reps as f32;
157        for it in 0..actual_reps {
158            for index in 0..points.len() {
159                let even = it % 2 == 0;
160                let i = if even {
161                    index
162                } else {
163                    points.len() - 1 - index
164                };
165                if i > 0 || even {
166                    let turned = if even {
167                        angles[i]
168                    } else {
169                        section - angles[i] + 2. * angles[0]
170                    };
171                    let a = to_radians(section * it as f32 + turned);
172                    out.push((pt(cos(a), sin(a)) * distances[i] + center, points[i].1));
173                }
174            }
175        }
176    } else {
177        let np = points.len();
178        for it in 0..np * reps {
179            let a = to_radians((it / np) as f32 * 360. / reps as f32);
180            let off = points[it % np].0 - center;
181            out.push((
182                pt(
183                    off.x * cos(a) - off.y * sin(a),
184                    off.x * sin(a) + off.y * cos(a),
185                ) + center,
186                points[it % np].1,
187            ));
188        }
189    }
190    out
191}
192
193fn custom(points: &[(Pt, CornerRounding)], reps: usize, mirroring: bool) -> RoundedPolygon {
194    let actual = repeated(points, reps, mirroring);
195    let vertices: Vec<(f32, f32)> = actual.iter().map(|q| (q.0.x, q.0.y)).collect();
196    let rounding: Vec<CornerRounding> = actual.iter().map(|q| q.1).collect();
197    RoundedPolygon::from_vertices(&vertices, SHARP, Some(&rounding), Some((0.5, 0.5)))
198        .unwrap_or_else(unbuildable)
199}
200
201/// A single point, standing in for an outline that cannot be built. The
202/// tables below all have finite vertices and matching lengths, which is all
203/// the constructors refuse, and the fixture test builds every one of them.
204fn unbuildable() -> RoundedPolygon {
205    RoundedPolygon::from_vertices(&[(0.5, 0.5); 3], SHARP, None, None).unwrap_or_else(|| {
206        unreachable!("three finite vertices with no per-vertex rounding are always accepted")
207    })
208}
209
210fn star(points: usize, inner: f32, rounding: CornerRounding) -> RoundedPolygon {
211    RoundedPolygon::star(points, 1., inner, rounding, None, ORIGIN).unwrap_or_else(unbuildable)
212}
213
214fn outline(kind: ShapeKind) -> RoundedPolygon {
215    use ShapeKind as K;
216    match kind {
217        K::Circle => RoundedPolygon::circle(10, 1., ORIGIN).unwrap_or_else(unbuildable),
218        K::Square => {
219            RoundedPolygon::rectangle(1., 1., R30, None, ORIGIN).unwrap_or_else(unbuildable)
220        }
221        K::Slanted => custom(
222            &[
223                s(0.926, 0.970, 0.189, 0.811),
224                s(-0.021, 0.967, 0.187, 0.057),
225            ],
226            2,
227            false,
228        ),
229        K::Arch => RoundedPolygon::regular(4, 1., ORIGIN, SHARP, Some(&[R100, R100, R20, R20]))
230            .unwrap_or_else(unbuildable)
231            .rotated(-135.),
232        K::Fan => custom(
233            &[
234                s(1.004, 1.000, 0.148, 0.417),
235                r(0.000, 1.000, 0.151),
236                r(0.000, -0.003, 0.148),
237                r(0.978, 0.020, 0.803),
238            ],
239            1,
240            false,
241        ),
242        K::Arrow => custom(
243            &[
244                r(0.500, 0.892, 0.313),
245                r(-0.216, 1.050, 0.207),
246                s(0.499, -0.160, 0.215, 1.000),
247                r(1.225, 1.060, 0.211),
248            ],
249            1,
250            false,
251        ),
252        K::SemiCircle => {
253            RoundedPolygon::rectangle(1.6, 1., SHARP, Some(&[R20, R20, R100, R100]), ORIGIN)
254                .unwrap_or_else(unbuildable)
255        }
256        K::Oval => RoundedPolygon::circle(8, 1., ORIGIN)
257            .unwrap_or_else(unbuildable)
258            .transformed(|x, y| (x, y * 0.64))
259            .rotated(-45.),
260        K::Pill => custom(
261            &[
262                r(0.961, 0.039, 0.426),
263                p(1.001, 0.428),
264                r(1.000, 0.609, 1.000),
265            ],
266            2,
267            true,
268        ),
269        K::Triangle => RoundedPolygon::regular(3, 1., ORIGIN, R20, None)
270            .unwrap_or_else(unbuildable)
271            .rotated(-90.),
272        K::Diamond => custom(
273            &[s(0.500, 1.096, 0.151, 0.524), r(0.040, 0.500, 0.159)],
274            2,
275            false,
276        ),
277        K::ClamShell => custom(
278            &[
279                r(0.171, 0.841, 0.159),
280                r(-0.020, 0.500, 0.140),
281                r(0.170, 0.159, 0.159),
282            ],
283            2,
284            false,
285        ),
286        K::Pentagon => custom(
287            &[
288                r(0.500, -0.009, 0.172),
289                r(1.030, 0.365, 0.164),
290                r(0.828, 0.970, 0.169),
291            ],
292            1,
293            true,
294        ),
295        K::Gem => custom(
296            &[
297                s(0.499, 1.023, 0.241, 0.778),
298                r(-0.005, 0.792, 0.208),
299                r(0.073, 0.258, 0.228),
300                r(0.433, -0.000, 0.491),
301            ],
302            1,
303            true,
304        ),
305        K::Sunny => star(8, 0.8, R15),
306        K::VerySunny => custom(&[r(0.500, 1.080, 0.085), r(0.358, 0.843, 0.085)], 8, false),
307        K::Cookie4Sided => custom(&[r(1.237, 1.236, 0.258), r(0.500, 0.918, 0.233)], 4, false),
308        K::Cookie6Sided => custom(&[r(0.723, 0.884, 0.394), r(0.500, 1.099, 0.398)], 6, false),
309        K::Cookie7Sided => star(7, 0.75, R50).rotated(-90.),
310        K::Cookie9Sided => star(9, 0.8, R50).rotated(-90.),
311        K::Cookie12Sided => star(12, 0.8, R50).rotated(-90.),
312        K::Ghostish => custom(
313            &[
314                r(0.500, 0., 1.000),
315                r(1., 0., 1.000),
316                s(1., 1.140, 0.254, 0.106),
317                r(0.575, 0.906, 0.253),
318            ],
319            1,
320            true,
321        ),
322        K::Clover4Leaf => custom(&[p(0.500, 0.074), r(0.725, -0.099, 0.476)], 4, true),
323        K::Clover8Leaf => custom(&[p(0.500, 0.036), r(0.758, -0.101, 0.209)], 8, false),
324        K::Burst => custom(
325            &[r(0.500, -0.006, 0.006), r(0.592, 0.158, 0.006)],
326            12,
327            false,
328        ),
329        K::SoftBurst => custom(&[r(0.193, 0.277, 0.053), r(0.176, 0.055, 0.053)], 10, false),
330        K::Boom => custom(
331            &[r(0.457, 0.296, 0.007), r(0.500, -0.051, 0.007)],
332            15,
333            false,
334        ),
335        K::SoftBoom => custom(
336            &[
337                p(0.733, 0.454),
338                r(0.839, 0.437, 0.532),
339                s(0.949, 0.449, 0.439, 1.000),
340                r(0.998, 0.478, 0.174),
341            ],
342            16,
343            true,
344        ),
345        K::Flower => custom(
346            &[
347                p(0.370, 0.187),
348                r(0.416, 0.049, 0.381),
349                r(0.479, 0.001, 0.095),
350            ],
351            8,
352            true,
353        ),
354        K::Puffy => custom(
355            &[
356                p(0.500, 0.053),
357                r(0.545, -0.040, 0.405),
358                r(0.670, -0.035, 0.426),
359                r(0.717, 0.066, 0.574),
360                p(0.722, 0.128),
361                r(0.777, 0.002, 0.360),
362                r(0.914, 0.149, 0.660),
363                r(0.926, 0.289, 0.660),
364                p(0.881, 0.346),
365                r(0.940, 0.344, 0.126),
366                r(1.003, 0.437, 0.255),
367            ],
368            2,
369            true,
370        )
371        .transformed(|x, y| (x, y * 0.742)),
372        K::PuffyDiamond => custom(
373            &[
374                r(0.870, 0.130, 0.146),
375                p(0.818, 0.357),
376                r(1.000, 0.332, 0.853),
377            ],
378            4,
379            true,
380        ),
381        K::PixelCircle => custom(
382            &[
383                p(0.500, 0.000),
384                p(0.704, 0.000),
385                p(0.704, 0.065),
386                p(0.843, 0.065),
387                p(0.843, 0.148),
388                p(0.926, 0.148),
389                p(0.926, 0.296),
390                p(1.000, 0.296),
391            ],
392            2,
393            true,
394        ),
395        K::PixelTriangle => custom(
396            &[
397                p(0.110, 0.500),
398                p(0.113, 0.000),
399                p(0.287, 0.000),
400                p(0.287, 0.087),
401                p(0.421, 0.087),
402                p(0.421, 0.170),
403                p(0.560, 0.170),
404                p(0.560, 0.265),
405                p(0.674, 0.265),
406                p(0.675, 0.344),
407                p(0.789, 0.344),
408                p(0.789, 0.439),
409                p(0.888, 0.439),
410            ],
411            1,
412            true,
413        ),
414        K::Bun => custom(
415            &[
416                p(0.796, 0.500),
417                r(0.853, 0.518, 1.),
418                r(0.992, 0.631, 1.),
419                r(0.968, 1.000, 1.),
420            ],
421            2,
422            true,
423        ),
424        K::Heart => custom(
425            &[
426                r(0.500, 0.268, 0.016),
427                r(0.792, -0.066, 0.958),
428                r(1.064, 0.276, 1.000),
429                r(0.501, 0.946, 0.129),
430            ],
431            1,
432            true,
433        ),
434    }
435}