Skip to main content

gpui_kit_material/shapes/
morph.rs

1// Copyright 2022-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 androidx.graphics:graphics-shapes (Morph.kt,
11// FeatureMapping.kt, FloatMapping.kt, PolygonMeasure.kt). Its `require` checks
12// become fallbacks, so that no polygon pair panics.
13//! Morphing: measure both outlines, map the corners of one onto the corners
14//! of the other without crossings, then cut curves until both outlines have
15//! the same number, matched one to one. Interpolating the matched control
16//! points cannot twist the outline the way radial or index-based blending
17//! does, and it needs no star-shaped outlines.
18use super::polygon::{
19    ANGLE_EPSILON, Cubic, DISTANCE_EPSILON, Pt, RoundedPolygon, atan2, interpolate,
20    positive_modulo, pt,
21};
22
23/// Whether `progress` lies in the wrapping range from `from` to `to`.
24fn progress_in_range(progress: f32, from: f32, to: f32) -> bool {
25    if to >= from {
26        (from..=to).contains(&progress)
27    } else {
28        progress >= from || progress <= to
29    }
30}
31
32/// The distance between two progress values, which wrap at 1.
33fn progress_distance(a: f32, b: f32) -> f32 {
34    let distance = (a - b).abs();
35    distance.min(1. - distance)
36}
37
38fn linear_map(xs: &[f32], ys: &[f32], x: f32) -> f32 {
39    let n = xs.len();
40    let start = (0..n)
41        .find(|&i| progress_in_range(x, xs[i], xs[(i + 1) % n]))
42        .unwrap_or(0);
43    let end = (start + 1) % n;
44    let size_x = positive_modulo(xs[end] - xs[start], 1.);
45    let size_y = positive_modulo(ys[end] - ys[start], 1.);
46    let position = if size_x < 0.001 {
47        0.5
48    } else {
49        positive_modulo(x - xs[start], 1.) / size_x
50    };
51    positive_modulo(ys[start] + size_y * position, 1.)
52}
53
54/// A wrapping, piecewise-linear map between outline progress on two shapes.
55struct DoubleMapper {
56    source: Vec<f32>,
57    target: Vec<f32>,
58}
59
60impl DoubleMapper {
61    /// The mapping, or the identity where it would not be monotonic.
62    fn new(mappings: &[(f32, f32)]) -> Self {
63        let source: Vec<f32> = mappings.iter().map(|m| m.0).collect();
64        let target: Vec<f32> = mappings.iter().map(|m| m.1).collect();
65        if valid_progress(&source) && valid_progress(&target) {
66            Self { source, target }
67        } else {
68            Self {
69                source: vec![0., 0.5],
70                target: vec![0., 0.5],
71            }
72        }
73    }
74    fn map(&self, x: f32) -> f32 {
75        linear_map(&self.source, &self.target, x)
76    }
77    fn map_back(&self, x: f32) -> f32 {
78        linear_map(&self.target, &self.source, x)
79    }
80}
81
82/// In [0, 1), distinct, and increasing except for at most one wrap.
83fn valid_progress(values: &[f32]) -> bool {
84    let Some(&last) = values.last() else {
85        return false;
86    };
87    let mut previous = last;
88    let mut wraps = 0;
89    for &current in values {
90        if !(0. ..1.).contains(&current) || progress_distance(current, previous) <= DISTANCE_EPSILON
91        {
92            return false;
93        }
94        if current < previous {
95            wraps += 1;
96            if wraps > 1 {
97                return false;
98            }
99        }
100        previous = current;
101    }
102    true
103}
104
105/// A corner and where it sits on its outline.
106#[derive(Clone, Copy)]
107struct MeasuredCorner {
108    progress: f32,
109    representative: Pt,
110    convex: bool,
111}
112
113/// Maps corners, nearest first, keeping the mapping free of crossings.
114fn feature_mapper(corners1: &[MeasuredCorner], corners2: &[MeasuredCorner]) -> DoubleMapper {
115    let mut pairs: Vec<(f32, usize, usize)> = Vec::new();
116    for (i, a) in corners1.iter().enumerate() {
117        for (j, b) in corners2.iter().enumerate() {
118            // Corners only map onto corners of the same convexity.
119            if a.convex == b.convex {
120                let d = (a.representative - b.representative).distance_squared();
121                if d != f32::MAX {
122                    pairs.push((d, i, j));
123                }
124            }
125        }
126    }
127    pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
128    match pairs.as_slice() {
129        [] => DoubleMapper::new(&[(0., 0.), (0.5, 0.5)]),
130        [(_, i, j)] => {
131            let (f1, f2) = (corners1[*i].progress, corners2[*j].progress);
132            DoubleMapper::new(&[(f1, f2), ((f1 + 0.5) % 1., (f2 + 0.5) % 1.)])
133        }
134        _ => {
135            let mut mapping: Vec<(f32, f32)> = Vec::new();
136            let mut used1 = vec![false; corners1.len()];
137            let mut used2 = vec![false; corners2.len()];
138            for &(_, i, j) in &pairs {
139                if used1[i] || used2[j] {
140                    continue;
141                }
142                let (f1, f2) = (corners1[i].progress, corners2[j].progress);
143                let Err(at) = mapping.binary_search_by(|m| m.0.total_cmp(&f1)) else {
144                    continue;
145                };
146                let n = mapping.len();
147                if n >= 1 {
148                    let (before1, before2) = mapping[(at + n - 1) % n];
149                    let (after1, after2) = mapping[at % n];
150                    if progress_distance(f1, before1) < DISTANCE_EPSILON
151                        || progress_distance(f1, after1) < DISTANCE_EPSILON
152                        || progress_distance(f2, before2) < DISTANCE_EPSILON
153                        || progress_distance(f2, after2) < DISTANCE_EPSILON
154                    {
155                        continue;
156                    }
157                    if n > 1 && !progress_in_range(f2, before2, after2) {
158                        continue;
159                    }
160                }
161                mapping.insert(at, (f1, f2));
162                used1[i] = true;
163                used2[j] = true;
164            }
165            DoubleMapper::new(&mapping)
166        }
167    }
168}
169
170/// `graphics-shapes` 1.0.1's `doMapping`: for each of `f1`'s corners in turn,
171/// the nearest of `f2`'s that leaves enough for the corners still to come.
172fn greedy_picks(f1: &[MeasuredCorner], f2: &[MeasuredCorner]) -> Vec<MeasuredCorner> {
173    let distance = |a: &MeasuredCorner, b: &MeasuredCorner| {
174        if a.convex == b.convex {
175            (a.representative - b.representative).distance_squared()
176        } else {
177            f32::MAX
178        }
179    };
180    let nearest = |corner: &MeasuredCorner, from: usize, to: usize| {
181        let mut best = from;
182        let mut best_distance = distance(corner, &f2[from % f2.len()]);
183        for i in from + 1..=to {
184            let d = distance(corner, &f2[i % f2.len()]);
185            if d < best_distance {
186                best = i;
187                best_distance = d;
188            }
189        }
190        best
191    };
192    let (m, n) = (f1.len(), f2.len());
193    let first = nearest(&f1[0], 0, n - 1);
194    let mut picks = vec![f2[first]];
195    let mut last_picked = first;
196    for (i, corner) in f1.iter().enumerate().skip(1) {
197        let mut last = first as isize - (m - i) as isize;
198        if last <= last_picked as isize {
199            last += n as isize;
200        }
201        let best = nearest(corner, last_picked + 1, last.max(0) as usize);
202        picks.push(f2[best % n]);
203        last_picked = best;
204    }
205    picks
206}
207
208/// `graphics-shapes` 1.0.1's `featureMapper`: the shorter list maps into the
209/// longer. No corners at all maps by identity, where the source throws.
210fn greedy_mapper(corners1: &[MeasuredCorner], corners2: &[MeasuredCorner]) -> DoubleMapper {
211    if corners1.is_empty() || corners2.is_empty() {
212        return DoubleMapper::new(&[(0., 0.), (0.5, 0.5)]);
213    }
214    let pairs: Vec<(f32, f32)> = if corners1.len() > corners2.len() {
215        greedy_picks(corners2, corners1)
216            .iter()
217            .zip(corners2)
218            .map(|(a, b)| (a.progress, b.progress))
219            .collect()
220    } else {
221        corners1
222            .iter()
223            .zip(greedy_picks(corners1, corners2))
224            .map(|(a, b)| (a.progress, b.progress))
225            .collect()
226    };
227    DoubleMapper::new(&pairs)
228}
229
230const MEASURE_SEGMENTS: u32 = 3;
231
232/// How a [`Morph`] measures its outlines and maps their corners.
233#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
234pub enum MorphMatching {
235    /// `graphics-shapes` since 1.1: progress is curve length, and corners map
236    /// nearest first without crossing. Handles outlines that are not
237    /// star-shaped around their center.
238    #[default]
239    Current,
240    /// `graphics-shapes` 1.0.1, the version Compose Material3 1.5.0-beta01 and
241    /// m3e pin: progress is the angle swept around the center, and corners map
242    /// by a greedy walk.
243    Compose1_5,
244}
245
246/// The measure in use, with the center the angle sweeps around.
247#[derive(Clone, Copy)]
248enum Measurer {
249    Length,
250    Angle(Pt),
251}
252
253impl Measurer {
254    fn of(matching: MorphMatching, polygon: &RoundedPolygon) -> Self {
255        match matching {
256            MorphMatching::Current => Self::Length,
257            MorphMatching::Compose1_5 => {
258                let (x, y) = polygon.center();
259                Self::Angle(pt(x, y))
260            }
261        }
262    }
263
264    fn measure(self, cubic: &Cubic) -> f32 {
265        match self {
266            Self::Length => closest_progress_to(cubic, f32::INFINITY).1,
267            Self::Angle(center) => {
268                let sweep = positive_modulo(
269                    angle_around(center, cubic.anchor1()) - angle_around(center, cubic.anchor0()),
270                    TWO_PI,
271                );
272                // An empty curve must not measure almost a full turn.
273                if sweep > TWO_PI - DISTANCE_EPSILON {
274                    0.
275                } else {
276                    sweep
277                }
278            }
279        }
280    }
281
282    fn cut_point(self, cubic: &Cubic, measure: f32) -> f32 {
283        match self {
284            Self::Length => closest_progress_to(cubic, measure).0,
285            Self::Angle(center) => {
286                let start = angle_around(center, cubic.anchor0());
287                find_minimum(|t| {
288                    let at = cubic.point_at(t);
289                    (positive_modulo(angle_around(center, at) - start, TWO_PI) - measure).abs()
290                })
291            }
292        }
293    }
294}
295
296const TWO_PI: f32 = 2. * std::f32::consts::PI;
297
298fn angle_around(center: Pt, point: (f32, f32)) -> f32 {
299    (atan2(point.1 - center.y, point.0 - center.x) + TWO_PI) % TWO_PI
300}
301
302/// The source's `findMinimum`: a ternary search over 0-1.
303fn find_minimum(f: impl Fn(f32) -> f32) -> f32 {
304    let (mut a, mut b) = (0_f32, 1_f32);
305    while b - a > 1e-5 {
306        let c1 = (2. * a + b) / 3.;
307        let c2 = (2. * b + a) / 3.;
308        if f(c1) < f(c2) {
309            b = c2;
310        } else {
311            a = c1;
312        }
313    }
314    (a + b) / 2.
315}
316
317/// The length of `cubic` up to `threshold`, on three chords: the parameter
318/// reached, and the length walked. The source's `LengthMeasurer`.
319fn closest_progress_to(cubic: &Cubic, threshold: f32) -> (f32, f32) {
320    let mut total = 0_f32;
321    let mut remainder = threshold;
322    let mut previous = pt(cubic.points[0], cubic.points[1]);
323    for i in 1..=MEASURE_SEGMENTS {
324        let progress = i as f32 / MEASURE_SEGMENTS as f32;
325        let point = cubic.point_on_curve(progress);
326        let segment = (point - previous).distance();
327        if segment >= remainder {
328            return (
329                progress - (1. - remainder / segment) / MEASURE_SEGMENTS as f32,
330                threshold,
331            );
332        }
333        remainder -= segment;
334        total += segment;
335        previous = point;
336    }
337    (1., total)
338}
339
340#[derive(Clone, Copy)]
341struct MeasuredCubic {
342    cubic: Cubic,
343    start: f32,
344    end: f32,
345    size: f32,
346    measurer: Measurer,
347}
348
349impl MeasuredCubic {
350    fn new(measurer: Measurer, cubic: Cubic, start: f32, end: f32) -> Self {
351        Self {
352            cubic,
353            start,
354            end,
355            size: measurer.measure(&cubic),
356            measurer,
357        }
358    }
359
360    fn cut_at(&self, progress: f32) -> (Self, Self) {
361        // Rounding upstream can land just outside this curve's range.
362        let bounded = if progress < self.start {
363            self.start
364        } else if progress > self.end {
365            self.end
366        } else {
367            progress
368        };
369        let relative = (bounded - self.start) / (self.end - self.start);
370        let t = self.measurer.cut_point(&self.cubic, relative * self.size);
371        // A zero-length chord divides zero by zero.
372        let t = if t.is_nan() { 0. } else { t.clamp(0., 1.) };
373        let (c1, c2) = self.cubic.split(t);
374        (
375            Self::new(self.measurer, c1, self.start, bounded),
376            Self::new(self.measurer, c2, bounded, self.end),
377        )
378    }
379}
380
381fn measured_cubics(
382    measurer: Measurer,
383    cubics: &[Cubic],
384    outline_progress: &[f32],
385) -> Vec<MeasuredCubic> {
386    let mut measured: Vec<MeasuredCubic> = Vec::with_capacity(cubics.len());
387    let mut start = 0.;
388    for (i, cubic) in cubics.iter().enumerate() {
389        if outline_progress[i + 1] - outline_progress[i] > DISTANCE_EPSILON {
390            measured.push(MeasuredCubic::new(
391                measurer,
392                *cubic,
393                start,
394                outline_progress[i + 1],
395            ));
396            start = outline_progress[i + 1];
397        }
398    }
399    match measured.last_mut() {
400        Some(last) => last.end = 1.,
401        // A zero-length outline: one point, covering all of its progress.
402        None => measured.push(MeasuredCubic::new(
403            measurer,
404            cubics.first().copied().unwrap_or_default(),
405            0.,
406            1.,
407        )),
408    }
409    measured
410}
411
412fn measure_polygon(
413    measurer: Measurer,
414    polygon: &RoundedPolygon,
415) -> (Vec<MeasuredCubic>, Vec<MeasuredCorner>) {
416    let mut cubics = Vec::new();
417    let mut corner_cubics: Vec<(usize, Pt, bool)> = Vec::new();
418    for feature in &polygon.features {
419        for (index, cubic) in feature.cubics.iter().enumerate() {
420            if let Some(convex) = feature.corner
421                && index == feature.cubics.len() / 2
422            {
423                let first = feature.cubics.first().copied().unwrap_or_default();
424                let last = feature.cubics.last().copied().unwrap_or_default();
425                let representative = pt(
426                    (first.points[0] + last.points[6]) / 2.,
427                    (first.points[1] + last.points[7]) / 2.,
428                );
429                corner_cubics.push((cubics.len(), representative, convex));
430            }
431            cubics.push(*cubic);
432        }
433    }
434    let mut measures = Vec::with_capacity(cubics.len() + 1);
435    measures.push(0_f32);
436    let mut sum = 0_f32;
437    for cubic in &cubics {
438        sum += measurer.measure(cubic);
439        measures.push(sum);
440    }
441    let outline_progress: Vec<f32> = measures.iter().map(|m| m / sum).collect();
442    let corners = corner_cubics
443        .into_iter()
444        .map(|(i, representative, convex)| MeasuredCorner {
445            progress: {
446                let middle = (outline_progress[i] + outline_progress[i + 1]) / 2.;
447                // 1.0.1 has no wrap here, and the wrap's rounding decides ties.
448                match measurer {
449                    Measurer::Length => positive_modulo(middle, 1.),
450                    Measurer::Angle(_) => middle,
451                }
452            },
453            representative,
454            convex,
455        })
456        .collect();
457    (
458        measured_cubics(measurer, &cubics, &outline_progress),
459        corners,
460    )
461}
462
463/// The outline, restarted at `cut`: its progress runs from 0 there.
464fn cut_and_shift(cubics: Vec<MeasuredCubic>, cut: f32) -> Vec<MeasuredCubic> {
465    if cut < DISTANCE_EPSILON {
466        return cubics;
467    }
468    let target_index = cubics
469        .iter()
470        .position(|c| (c.start..=c.end).contains(&cut))
471        .unwrap_or(0);
472    let measurer = cubics[target_index].measurer;
473    let (b1, b2) = cubics[target_index].cut_at(cut);
474    let n = cubics.len();
475    let mut shifted = Vec::with_capacity(n + 1);
476    shifted.push(b2.cubic);
477    for i in 1..n {
478        shifted.push(cubics[(i + target_index) % n].cubic);
479    }
480    shifted.push(b1.cubic);
481    let mut progress = Vec::with_capacity(n + 2);
482    for index in 0..n + 2 {
483        progress.push(if index == 0 {
484            0.
485        } else if index == n + 1 {
486            1.
487        } else {
488            positive_modulo(cubics[(target_index + index - 1) % n].end - cut, 1.)
489        });
490    }
491    measured_cubics(measurer, &shifted, &progress)
492}
493
494fn match_cubics(
495    p1: &RoundedPolygon,
496    p2: &RoundedPolygon,
497    matching: MorphMatching,
498) -> Vec<(Cubic, Cubic)> {
499    let (bs1, corners1) = measure_polygon(Measurer::of(matching, p1), p1);
500    let (measured2, corners2) = measure_polygon(Measurer::of(matching, p2), p2);
501    let mapper = match matching {
502        MorphMatching::Current => feature_mapper(&corners1, &corners2),
503        MorphMatching::Compose1_5 => greedy_mapper(&corners1, &corners2),
504    };
505    let cut = mapper.map(0.);
506    let bs2 = cut_and_shift(measured2, cut);
507
508    let mut matched = Vec::with_capacity(bs1.len().max(bs2.len()) * 2);
509    let (mut i1, mut i2) = (1, 1);
510    let mut b1 = bs1.first().copied();
511    let mut b2 = bs2.first().copied();
512    while let (Some(c1), Some(c2)) = (b1, b2) {
513        let b1a = if i1 == bs1.len() { 1. } else { c1.end };
514        let b2a = if i2 == bs2.len() {
515            1.
516        } else {
517            mapper.map_back(positive_modulo(c2.end + cut, 1.))
518        };
519        let min = b1a.min(b2a);
520        let (segment1, next1) = if b1a > min + ANGLE_EPSILON {
521            let (a, b) = c1.cut_at(min);
522            (a, Some(b))
523        } else {
524            let next = bs1.get(i1).copied();
525            i1 += 1;
526            (c1, next)
527        };
528        let (segment2, next2) = if b2a > min + ANGLE_EPSILON {
529            let (a, b) = c2.cut_at(positive_modulo(mapper.map(min) - cut, 1.));
530            (a, Some(b))
531        } else {
532            let next = bs2.get(i2).copied();
533            i2 += 1;
534            (c2, next)
535        };
536        matched.push((segment1.cubic, segment2.cubic));
537        b1 = next1;
538        b2 = next2;
539    }
540    matched
541}
542
543/// An animation between two [`RoundedPolygon`]s of any structure: both
544/// outlines cut into matching curves. See `androidx.graphics.shapes.Morph`.
545#[derive(Clone, Debug)]
546pub struct Morph {
547    start: RoundedPolygon,
548    end: RoundedPolygon,
549    matched: Vec<(Cubic, Cubic)>,
550}
551
552impl Morph {
553    /// Match `start` and `end`. This measures both outlines, so hold on to
554    /// the result: [`ShapeKind::morph_to`](super::ShapeKind::morph_to) keeps
555    /// one per pair of Material shapes.
556    pub fn new(start: &RoundedPolygon, end: &RoundedPolygon) -> Self {
557        Self::with_matching(start, end, MorphMatching::default())
558    }
559
560    /// [`Self::new`], matching the outlines by `matching`.
561    pub fn with_matching(
562        start: &RoundedPolygon,
563        end: &RoundedPolygon,
564        matching: MorphMatching,
565    ) -> Self {
566        Self {
567            start: start.clone(),
568            end: end.clone(),
569            matched: match_cubics(start, end, matching),
570        }
571    }
572
573    /// How many curves the outline has at any progress.
574    pub fn len(&self) -> usize {
575        self.matched.len()
576    }
577
578    /// Whether the morph has no curves. Polygons always give it at least one.
579    pub fn is_empty(&self) -> bool {
580        self.matched.is_empty()
581    }
582
583    /// Visit the outline at `progress`, without allocating: 0 is the start
584    /// shape, 1 the end shape, and values beyond them extrapolate, for a
585    /// spring's overshoot.
586    pub fn for_each_cubic(&self, progress: f32, mut each: impl FnMut(&Cubic)) {
587        for (a, b) in &self.matched {
588            let mut cubic = Cubic::default();
589            for (value, (from, to)) in cubic.points.iter_mut().zip(a.points.iter().zip(&b.points)) {
590                *value = interpolate(*from, *to, progress);
591            }
592            each(&cubic);
593        }
594    }
595
596    /// The outline at `progress`, closed exactly on its first anchor.
597    pub fn as_cubics(&self, progress: f32) -> Vec<Cubic> {
598        let mut cubics = Vec::with_capacity(self.matched.len());
599        self.for_each_cubic(progress, |cubic| cubics.push(*cubic));
600        if let [first, .., last] = cubics.as_mut_slice() {
601            last.points[6] = first.points[0];
602            last.points[7] = first.points[1];
603        }
604        cubics
605    }
606
607    /// The box covering both shapes' [`RoundedPolygon::bounds`].
608    pub fn bounds(&self, approximate: bool) -> [f32; 4] {
609        union(self.start.bounds(approximate), self.end.bounds(approximate))
610    }
611
612    /// The box covering both shapes' [`RoundedPolygon::max_bounds`].
613    pub fn max_bounds(&self) -> [f32; 4] {
614        union(self.start.max_bounds(), self.end.max_bounds())
615    }
616}
617
618fn union(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
619    [
620        a[0].min(b[0]),
621        a[1].min(b[1]),
622        a[2].max(b[2]),
623        a[3].max(b[3]),
624    ]
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::shapes::polygon::CornerRounding;
631    use core::prelude::v1::test;
632
633    #[test]
634    fn degenerate_and_mismatched_polygons_morph_without_panicking() {
635        let point =
636            RoundedPolygon::from_vertices(&[(1., 1.); 3], CornerRounding::UNROUNDED, None, None)
637                .expect("finite");
638        let star = RoundedPolygon::star(5, 1., 0.4, CornerRounding::new(0.1), None, (0., 0.))
639            .expect("valid");
640        let triangle = RoundedPolygon::regular(3, 1., (0., 0.), CornerRounding::UNROUNDED, None)
641            .expect("valid");
642        for (a, b) in [
643            (&point, &star),
644            (&star, &point),
645            (&point, &point),
646            (&star, &triangle),
647            (&triangle, &star),
648        ] {
649            let morph = Morph::new(a, b);
650            assert!(!morph.is_empty());
651            for progress in [0., 0.5, 1., 1.2] {
652                assert!(
653                    morph
654                        .as_cubics(progress)
655                        .iter()
656                        .all(|c| c.points.iter().all(|v| v.is_finite())),
657                    "{progress}"
658                );
659            }
660        }
661    }
662
663    #[test]
664    fn the_mapper_rejects_repeats_and_double_wraps() {
665        assert!(valid_progress(&[0.3, 0.6, 0.]));
666        assert!(!valid_progress(&[0.5, 0., 0.7]));
667        assert!(!valid_progress(&[0.5]));
668        assert!(!valid_progress(&[]));
669        assert!(!valid_progress(&[0.2, 1.2]));
670        let identity = DoubleMapper::new(&[(0.5, 0.5)]);
671        assert_eq!(identity.map(0.25), 0.25);
672    }
673}