1 /*
2  * Copyright 2023 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.math.abs
20 
21 // TODO: b/374764251 Introduce an IgnorableFeature?
22 
23 /**
24  * While a polygon's shape can be drawn solely using a list of [Cubic] objects representing its raw
25  * curves and lines, features add an extra layer of context to groups of cubics. Features group
26  * cubics into (straight) edges, convex corners, or concave corners. For example, rounding a
27  * rectangle adds many cubics around its edges, but the rectangle's overall number of corners
28  * remains the same. [Morph] therefore uses this grouping for several reasons:
29  * - Noise Reduction: Grouping cubics reduces the amount of noise introduced by individual cubics
30  *   (as seen in the rounded rectangle example).
31  * - Mapping Base: The grouping serves as the base set for [Morph]'s mapping process.
32  * - Curve Type Mapping: [Morph] maps similar curve types (convex, concave) together. Note that
33  *   edges or features created with [buildIgnorableFeature] are ignored in the default mapping.
34  *
35  * By using features, you can manipulate polygon shapes with more context and control.
36  */
37 abstract class Feature(val cubics: List<Cubic>) {
38 
39     companion object Factory {
40         /**
41          * Group a list of [Cubic] objects to a feature that should be ignored in the default
42          * [Morph] mapping. The feature can have any indentation.
43          *
44          * Sometimes, it's helpful to ignore certain features when morphing shapes. This is because
45          * only the features you mark as important will be smoothly transitioned between the start
46          * and end shapes. Additionally, the default morph algorithm will try to match convex
47          * corners to convex corners and concave to concave. Marking features as ignorable will
48          * influence this matching. For example, given a 12-pointed star, marking all concave
49          * corners as ignorable will create a [Morph] that only considers the outer corners of the
50          * star. As a result, depending on the morphed to shape, the animation may have fewer
51          * intersections and rotations. Another example for the other way around is a [Morph]
52          * between a pointed up triangle to a square. Marking the square's top edge as a convex
53          * corner matches it to the triangle's upper corner. Instead of moving triangle's upper
54          * corner to one of rectangle's corners, the animation now splits the triangle to match
55          * squares' outer corners.
56          *
57          * @param cubics The list of raw cubics describing the feature's shape
58          * @throws IllegalArgumentException for lists of empty cubics or non-continuous cubics
59          */
buildIgnorableFeaturenull60         fun buildIgnorableFeature(cubics: List<Cubic>): Feature = validated(Edge(cubics))
61 
62         /**
63          * Group a [Cubic] object to an edge (neither inward or outward identification in a shape).
64          *
65          * @param cubic The raw cubic describing the edge's shape
66          * @throws IllegalArgumentException for lists of empty cubics or non-continuous cubics
67          */
68         fun buildEdge(cubic: Cubic): Feature = Edge(listOf(cubic))
69 
70         /**
71          * Group a list of [Cubic] objects to a convex corner (outward indentation in a shape).
72          *
73          * @param cubics The list of raw cubics describing the corner's shape
74          * @throws IllegalArgumentException for lists of empty cubics or non-continuous cubics
75          */
76         fun buildConvexCorner(cubics: List<Cubic>): Feature = validated(Corner(cubics, true))
77 
78         /**
79          * Group a list of [Cubic] objects to a concave corner (inward indentation in a shape).
80          *
81          * @param cubics The list of raw cubics describing the corner's shape
82          * @throws IllegalArgumentException for lists of empty cubics or non-continuous cubics
83          */
84         fun buildConcaveCorner(cubics: List<Cubic>): Feature = validated(Corner(cubics, false))
85 
86         private fun validated(feature: Feature): Feature {
87             require(feature.cubics.isNotEmpty()) { "Features need at least one cubic." }
88 
89             require(isContinuous(feature)) {
90                 "Feature must be continuous, with the anchor points of all cubics " +
91                     "matching the anchor points of the preceding and succeeding cubics"
92             }
93 
94             return feature
95         }
96 
isContinuousnull97         private fun isContinuous(feature: Feature): Boolean {
98             var prevCubic = feature.cubics.first()
99             for (index in 1..feature.cubics.lastIndex) {
100                 val cubic = feature.cubics[index]
101                 if (
102                     abs(cubic.anchor0X - prevCubic.anchor1X) > DistanceEpsilon ||
103                         abs(cubic.anchor0Y - prevCubic.anchor1Y) > DistanceEpsilon
104                 ) {
105                     return false
106                 }
107                 prevCubic = cubic
108             }
109             return true
110         }
111     }
112 
113     /**
114      * Transforms the points in this [Feature] with the given [PointTransformer] and returns a new
115      * [Feature]
116      *
117      * @param f The [PointTransformer] used to transform this [Feature]
118      */
transformednull119     abstract fun transformed(f: PointTransformer): Feature
120 
121     /**
122      * Returns a new [Feature] with the points that define the shape of this [Feature] in reversed
123      * order.
124      */
125     abstract fun reversed(): Feature
126 
127     /**
128      * Whether this Feature gets ignored in the Morph mapping. See [buildIgnorableFeature] for more
129      * details.
130      */
131     abstract val isIgnorableFeature: Boolean
132 
133     /** Whether this Feature is an Edge with no inward or outward indentation. */
134     abstract val isEdge: Boolean
135 
136     /** Whether this Feature is a convex corner (outward indentation in a shape). */
137     abstract val isConvexCorner: Boolean
138 
139     /** Whether this Feature is a concave corner (inward indentation in a shape). */
140     abstract val isConcaveCorner: Boolean
141 
142     /**
143      * Edges have only a list of the cubic curves which make up the edge. Edges lie between corners
144      * and have no vertex or concavity; the curves are simply straight lines (represented by Cubic
145      * curves).
146      */
147     internal class Edge(cubics: List<Cubic>) : Feature(cubics) {
148         override fun transformed(f: PointTransformer) =
149             Edge(
150                 buildList {
151                     // Performance: Builds the list by avoiding creating an unnecessary Iterator to
152                     // iterate through the cubics List.
153                     for (i in cubics.indices) {
154                         add(cubics[i].transformed(f))
155                     }
156                 }
157             )
158 
159         override fun reversed(): Edge {
160             val reversedCubics = mutableListOf<Cubic>()
161 
162             for (i in cubics.lastIndex downTo 0) {
163                 reversedCubics.add(cubics[i].reverse())
164             }
165 
166             return Edge(reversedCubics)
167         }
168 
169         override fun toString(): String = "Edge"
170 
171         override val isIgnorableFeature = true
172 
173         override val isEdge = true
174 
175         override val isConvexCorner = false
176 
177         override val isConcaveCorner = false
178     }
179 
180     /**
181      * Corners contain the list of cubic curves which describe how the corner is rounded (or not),
182      * and a flag indicating whether the corner is convex. A regular polygon has all convex corners,
183      * while a star polygon generally (but not necessarily) has both convex (outer) and concave
184      * (inner) corners.
185      */
186     internal class Corner(cubics: List<Cubic>, val convex: Boolean = true) : Feature(cubics) {
transformednull187         override fun transformed(f: PointTransformer): Feature {
188             return Corner(
189                 buildList {
190                     // Performance: Builds the list by avoiding creating an unnecessary Iterator to
191                     // iterate through the cubics List.
192                     for (i in cubics.indices) {
193                         add(cubics[i].transformed(f))
194                     }
195                 },
196                 convex
197             )
198         }
199 
reversednull200         override fun reversed(): Corner {
201             val reversedCubics = mutableListOf<Cubic>()
202 
203             for (i in cubics.lastIndex downTo 0) {
204                 reversedCubics.add(cubics[i].reverse())
205             }
206 
207             // TODO: b/369320447 - Revert flag negation when [RoundedPolygon] ignores orientation
208             // for setting the flag
209             return Corner(reversedCubics, !convex)
210         }
211 
toStringnull212         override fun toString(): String {
213             return "Corner: cubics=${cubics.joinToString(separator = ", "){"[$it]"}} convex=$convex"
214         }
215 
216         override val isIgnorableFeature = false
217 
218         override val isEdge = false
219 
220         override val isConvexCorner = convex
221 
222         override val isConcaveCorner = !convex
223     }
224 }
225