1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.graphics.shapes
18 
19 import kotlin.jvm.JvmOverloads
20 import kotlin.jvm.JvmStatic
21 import kotlin.math.max
22 import kotlin.math.min
23 
24 /**
25  * This class is used to animate between start and end polygons objects.
26  *
27  * Morphing between arbitrary objects can be problematic because it can be difficult to determine
28  * how the points of a given shape map to the points of some other shape. [Morph] simplifies the
29  * problem by only operating on [RoundedPolygon] objects, which are known to have similar,
30  * contiguous structures. For one thing, the shape of a polygon is contiguous from start to end
31  * (compared to an arbitrary Path object, which could have one or more `moveTo` operations in the
32  * shape). Also, all edges of a polygon shape are represented by [Cubic] objects, thus the start and
33  * end shapes use similar operations. Two Polygon shapes then only differ in the quantity and
34  * placement of their curves. The morph works by determining how to map the curves of the two shapes
35  * together (based on proximity and other information, such as distance to polygon vertices and
36  * concavity), and splitting curves when the shapes do not have the same number of curves or when
37  * the curve placement within the shapes is very different.
38  */
39 class Morph(private val start: RoundedPolygon, private val end: RoundedPolygon) {
40     /**
41      * The structure which holds the actual shape being morphed. It contains all cubics necessary to
42      * represent the start and end shapes (the original cubics in the shapes may be cut to align the
43      * start/end shapes), matched one to one in each Pair.
44      */
45     @PublishedApi
46     internal val morphMatch: List<Pair<Cubic, Cubic>>
47         get() = _morphMatch
48 
49     private val _morphMatch: List<Pair<Cubic, Cubic>> = match(start, end)
50 
51     /**
52      * Calculates the axis-aligned bounds of the object.
53      *
54      * @param approximate when true, uses a faster calculation to create the bounding box based on
55      *   the min/max values of all anchor and control points that make up the shape. Default value
56      *   is true.
57      * @param bounds a buffer to hold the results. If not supplied, a temporary buffer will be
58      *   created.
59      * @return The axis-aligned bounding box for this object, where the rectangles left, top, right,
60      *   and bottom values will be stored in entries 0, 1, 2, and 3, in that order.
61      */
62     @JvmOverloads
63     fun calculateBounds(
64         bounds: FloatArray = FloatArray(4),
65         approximate: Boolean = true
66     ): FloatArray {
67         start.calculateBounds(bounds, approximate)
68         val minX = bounds[0]
69         val minY = bounds[1]
70         val maxX = bounds[2]
71         val maxY = bounds[3]
72         end.calculateBounds(bounds, approximate)
73         bounds[0] = min(minX, bounds[0])
74         bounds[1] = min(minY, bounds[1])
75         bounds[2] = max(maxX, bounds[2])
76         bounds[3] = max(maxY, bounds[3])
77         return bounds
78     }
79 
80     /**
81      * Like [calculateBounds], this function calculates the axis-aligned bounds of the object and
82      * returns that rectangle. But this function determines the max dimension of the shape (by
83      * calculating the distance from its center to the start and midpoint of each curve) and returns
84      * a square which can be used to hold the object in any rotation. This function can be used, for
85      * example, to calculate the max size of a UI element meant to hold this shape in any rotation.
86      *
87      * @param bounds a buffer to hold the results. If not supplied, a temporary buffer will be
88      *   created.
89      * @return The axis-aligned max bounding box for this object, where the rectangles left, top,
90      *   right, and bottom values will be stored in entries 0, 1, 2, and 3, in that order.
91      */
92     fun calculateMaxBounds(bounds: FloatArray = FloatArray(4)): FloatArray {
93         start.calculateMaxBounds(bounds)
94         val minX = bounds[0]
95         val minY = bounds[1]
96         val maxX = bounds[2]
97         val maxY = bounds[3]
98         end.calculateMaxBounds(bounds)
99         bounds[0] = min(minX, bounds[0])
100         bounds[1] = min(minY, bounds[1])
101         bounds[2] = max(maxX, bounds[2])
102         bounds[3] = max(maxY, bounds[3])
103         return bounds
104     }
105 
106     /**
107      * Returns a representation of the morph object at a given [progress] value as a list of Cubics.
108      * Note that this function causes a new list to be created and populated, so there is some
109      * overhead.
110      *
111      * @param progress a value from 0 to 1 that determines the morph's current shape, between the
112      *   start and end shapes provided at construction time. A value of 0 results in the start
113      *   shape, a value of 1 results in the end shape, and any value in between results in a shape
114      *   which is a linear interpolation between those two shapes. The range is generally [0..1] and
115      *   values outside could result in undefined shapes, but values close to (but outside) the
116      *   range can be used to get an exaggerated effect (e.g., for a bounce or overshoot animation).
117      */
118     fun asCubics(progress: Float): List<Cubic> {
119         return buildList {
120             // The first/last mechanism here ensures that the final anchor point in the shape
121             // exactly matches the first anchor point. There can be rendering artifacts introduced
122             // by those points being slightly off, even by much less than a pixel
123             var firstCubic: Cubic? = null
124             var lastCubic: Cubic? = null
125             for (i in _morphMatch.indices) {
126                 val cubic =
127                     Cubic(
128                         FloatArray(8) {
129                             interpolate(
130                                 _morphMatch[i].first.points[it],
131                                 _morphMatch[i].second.points[it],
132                                 progress
133                             )
134                         }
135                     )
136                 if (firstCubic == null) firstCubic = cubic
137                 if (lastCubic != null) add(lastCubic)
138                 lastCubic = cubic
139             }
140             if (lastCubic != null && firstCubic != null)
141                 add(
142                     Cubic(
143                         lastCubic.anchor0X,
144                         lastCubic.anchor0Y,
145                         lastCubic.control0X,
146                         lastCubic.control0Y,
147                         lastCubic.control1X,
148                         lastCubic.control1Y,
149                         firstCubic.anchor0X,
150                         firstCubic.anchor0Y
151                     )
152                 )
153         }
154     }
155 
156     /**
157      * Returns a representation of the morph object at a given [progress] value, iterating over the
158      * cubics and calling the callback. This function is faster than [asCubics], since it doesn't
159      * allocate new [Cubic] instances, but to do this it reuses the same [MutableCubic] instance
160      * during iteration.
161      *
162      * @param progress a value from 0 to 1 that determines the morph's current shape, between the
163      *   start and end shapes provided at construction time. A value of 0 results in the start
164      *   shape, a value of 1 results in the end shape, and any value in between results in a shape
165      *   which is a linear interpolation between those two shapes. The range is generally [0..1] and
166      *   values outside could result in undefined shapes, but values close to (but outside) the
167      *   range can be used to get an exaggerated effect (e.g., for a bounce or overshoot animation).
168      * @param mutableCubic An instance of [MutableCubic] that will be used to set each cubic in
169      *   time.
170      * @param callback The function to be called for each Cubic
171      */
172     @JvmOverloads
173     inline fun forEachCubic(
174         progress: Float,
175         mutableCubic: MutableCubic = MutableCubic(),
176         callback: (MutableCubic) -> Unit
177     ) {
178         for (i in morphMatch.indices) {
179             mutableCubic.interpolate(morphMatch[i].first, morphMatch[i].second, progress)
180             callback(mutableCubic)
181         }
182     }
183 
184     internal companion object {
185         /**
186          * [match], called at Morph construction time, creates the structure used to animate between
187          * the start and end shapes. The technique is to match geometry (curves) between the shapes
188          * when and where possible, and to create new/placeholder curves when necessary (when one of
189          * the shapes has more curves than the other). The result is a list of pairs of Cubic
190          * curves. Those curves are the matched pairs: the first of each pair holds the geometry of
191          * the start shape, the second holds the geometry for the end shape. Changing the progress
192          * of a Morph object simply interpolates between all pairs of curves for the morph shape.
193          *
194          * Curves on both shapes are matched by running the [Measurer] to determine where the points
195          * are in each shape (proportionally, along the outline), and then running [featureMapper]
196          * which decides how to map (match) all of the curves with each other.
197          */
198         @JvmStatic
199         internal fun match(p1: RoundedPolygon, p2: RoundedPolygon): List<Pair<Cubic, Cubic>> {
200             // TODO Commented out due to the use of javaClass ("Error: Platform reference in a
201             //  common module")
202             /*
203             if (DEBUG) {
204                repeat(2) { polyIndex ->
205                    debugLog(LOG_TAG) {
206                        listOf("Initial start:\n", "Initial end:\n")[polyIndex] +
207                            listOf(p1, p2)[polyIndex].features.joinToString("\n") { feature ->
208                                "${feature.javaClass.name.split("$").last()} - " +
209                                    ((feature as? Feature.Corner)?.convex?.let {
210                                        if (it) "Convex - " else "Concave - " } ?: "") +
211                                    feature.cubics.joinToString("|")
212                            }
213                    }
214                }
215             }
216             */
217 
218             // Measure polygons, returns lists of measured cubics for each polygon, which
219             // we then use to match start/end curves
220             val measuredPolygon1 = MeasuredPolygon.measurePolygon(LengthMeasurer(), p1)
221             val measuredPolygon2 = MeasuredPolygon.measurePolygon(LengthMeasurer(), p2)
222 
223             // features1 and 2 will contain the list of corners (just the inner circular curve)
224             // along with the progress at the middle of those corners. These measurement values
225             // are then used to compare and match between the two polygons
226             val features1 = measuredPolygon1.features
227             val features2 = measuredPolygon2.features
228 
229             // Map features: doubleMapper is the result of mapping the features in each shape to the
230             // closest feature in the other shape.
231             // Given a progress in one of the shapes it can be used to find the corresponding
232             // progress in the other shape (in both directions)
233             val doubleMapper = featureMapper(features1, features2)
234 
235             // cut point on poly2 is the mapping of the 0 point on poly1
236             val polygon2CutPoint = doubleMapper.map(0f)
237             debugLog(LOG_TAG) { "polygon2CutPoint = $polygon2CutPoint" }
238 
239             // Cut and rotate.
240             // Polygons start at progress 0, and the featureMapper has decided that we want to match
241             // progress 0 in the first polygon to `polygon2CutPoint` on the second polygon.
242             // So we need to cut the second polygon there and "rotate it", so as we walk through
243             // both polygons we can find the matching.
244             // The resulting bs1/2 are MeasuredPolygons, whose MeasuredCubics start from
245             // outlineProgress=0 and increasing until outlineProgress=1
246             val bs1 = measuredPolygon1
247             val bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint)
248 
249             if (DEBUG) {
250                 (0 until bs1.size).forEach { index ->
251                     debugLog(LOG_TAG) { "start $index: ${bs1.getOrNull(index)}" }
252                 }
253                 (0 until bs2.size).forEach { index ->
254                     debugLog(LOG_TAG) { "End $index: ${bs2.getOrNull(index)}" }
255                 }
256             }
257 
258             // Match
259             // Now we can compare the two lists of measured cubics and create a list of pairs
260             // of cubics [ret], which are the start/end curves that represent the Morph object
261             // and the start and end shapes, and which can be interpolated to animate the
262             // between those shapes.
263             val ret = mutableListOf<Pair<Cubic, Cubic>>()
264             // i1/i2 are the indices of the current cubic on the start (1) and end (2) shapes
265             var i1 = 0
266             var i2 = 0
267             // b1, b2 are the current measured cubic for each polygon
268             var b1 = bs1.getOrNull(i1++)
269             var b2 = bs2.getOrNull(i2++)
270             // Iterate until all curves are accounted for and matched
271             while (b1 != null && b2 != null) {
272                 // Progresses are in shape1's perspective
273                 // b1a, b2a are ending progress values of current measured cubics in [0,1] range
274                 val b1a = if (i1 == bs1.size) 1f else b1.endOutlineProgress
275                 val b2a =
276                     if (i2 == bs2.size) 1f
277                     else
278                         doubleMapper.mapBack(
279                             positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1f)
280                         )
281                 val minb = min(b1a, b2a)
282                 debugLog(LOG_TAG) { "$b1a $b2a | $minb" }
283                 // min b is the progress at which the curve that ends first ends.
284                 // If both curves ends roughly there, no cutting is needed, we have a match.
285                 // If one curve extends beyond, we need to cut it.
286                 val (seg1, newb1) =
287                     if (b1a > minb + AngleEpsilon) {
288                         debugLog(LOG_TAG) { "Cut 1" }
289                         b1.cutAtProgress(minb)
290                     } else {
291                         b1 to bs1.getOrNull(i1++)
292                     }
293                 val (seg2, newb2) =
294                     if (b2a > minb + AngleEpsilon) {
295                         debugLog(LOG_TAG) { "Cut 2" }
296                         b2.cutAtProgress(
297                             positiveModulo(doubleMapper.map(minb) - polygon2CutPoint, 1f)
298                         )
299                     } else {
300                         b2 to bs2.getOrNull(i2++)
301                     }
302                 debugLog(LOG_TAG) { "Match: $seg1 -> $seg2" }
303                 ret.add(seg1.cubic to seg2.cubic)
304                 b1 = newb1
305                 b2 = newb2
306             }
307             require(b1 == null && b2 == null) {
308                 "Expected both Polygon's Cubic to be fully matched"
309             }
310 
311             if (DEBUG) {
312                 // Export as SVG path.
313                 val showPoint: (Point) -> String = {
314                     "${(it.x * 100).toStringWithLessPrecision()} ${(it.y * 100).toStringWithLessPrecision()}"
315                 }
316                 repeat(2) { listIx ->
317                     val points = ret.map { if (listIx == 0) it.first else it.second }
318                     debugLog(LOG_TAG) {
319                         "M " +
320                             showPoint(Point(points.first().anchor0X, points.first().anchor0Y)) +
321                             " " +
322                             points.joinToString(" ") {
323                                 "C " +
324                                     showPoint(Point(it.control0X, it.control0Y)) +
325                                     ", " +
326                                     showPoint(Point(it.control1X, it.control1Y)) +
327                                     ", " +
328                                     showPoint(Point(it.anchor1X, it.anchor1Y))
329                             } +
330                             " Z"
331                     }
332                 }
333             }
334             return ret
335         }
336     }
337 }
338 
339 private val LOG_TAG = "Morph"
340