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