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 androidx.collection.FloatList
20 import androidx.collection.MutableFloatList
21 import kotlin.jvm.JvmField
22 import kotlin.math.abs
23 import kotlin.math.min
24 
25 /**
26  * Checks if the given progress is in the given progress range, since progress is in the [0..1)
27  * interval, and wraps, there is a special case when progressTo < progressFrom. For example, if the
28  * progress range is 0.7 to 0.2, both 0.8 and 0.1 are inside and 0.5 is outside.
29  */
progressInRangenull30 internal fun progressInRange(progress: Float, progressFrom: Float, progressTo: Float) =
31     if (progressTo >= progressFrom) {
32         progress in progressFrom..progressTo
33     } else {
34         progress >= progressFrom || progress <= progressTo
35     }
36 
37 /**
38  * Maps from one set of progress values to another. This is used by DoubleMapper to retrieve the
39  * value on one shape that maps to the appropriate value on the other.
40  */
linearMapnull41 internal fun linearMap(xValues: FloatList, yValues: FloatList, x: Float): Float {
42     require(x in 0f..1f) { "Invalid progress: $x" }
43     val segmentStartIndex =
44         xValues.indices.first { progressInRange(x, xValues[it], xValues[(it + 1) % xValues.size]) }
45     val segmentEndIndex = (segmentStartIndex + 1) % xValues.size
46     val segmentSizeX = positiveModulo(xValues[segmentEndIndex] - xValues[segmentStartIndex], 1f)
47     val segmentSizeY = positiveModulo(yValues[segmentEndIndex] - yValues[segmentStartIndex], 1f)
48     val positionInSegment =
49         segmentSizeX.let {
50             if (it < 0.001f) 0.5f else positiveModulo(x - xValues[segmentStartIndex], 1f) / it
51         }
52     return positiveModulo(yValues[segmentStartIndex] + segmentSizeY * positionInSegment, 1f)
53 }
54 
55 /**
56  * DoubleMapper creates mappings from values in the
57  * [0..1) source space to values in the [0..1) target space, and back. This mapping is created given a finite list of representative mappings, and this is extended to the whole interval by linear interpolation, and wrapping around. For example, if we have mappings 0.2 to 0.5 and 0.4 to 0.6, then 0.3 (which is in the middle of the source interval) will be mapped to 0.55 (the middle of the targets for the interval), 0.21 will map to 0.505, and so on. As a more complete example, if we use x to represent a value in the source space and y for the target space, and given as input the mappings 0 to 0, 0.5 to 0.25, this will create a mapping that: { if x in [0 .. 0.5]
58  * } y = x / 2 { if x in [0.5 .. 1] } y = 0.25 + (x - 0.5) * 1.5 = x * 1.5 - 0.5 The mapping can
59  * also be used the other way around (using the mapBack function), resulting in: { if y in
60  * [0 .. 0.25] } x = y * 2 { if y in [0.25 .. 1] } x = (y + 0.5) / 1.5 This is used to create
61  * mappings of progress values between the start and end shape, which is then used to insert new
62  * curves and match curves overall.
63  */
64 internal class DoubleMapper(vararg mappings: Pair<Float, Float>) {
65     private val sourceValues = MutableFloatList(mappings.size)
66     private val targetValues = MutableFloatList(mappings.size)
67 
68     init {
69         for (i in mappings.indices) {
70             sourceValues.add(mappings[i].first)
71             targetValues.add(mappings[i].second)
72         }
73         // Both source values and target values should be monotonically increasing, with the
74         // exception of maybe one time (since progress wraps around).
75         validateProgress(sourceValues)
76         validateProgress(targetValues)
77     }
78 
mapnull79     fun map(x: Float) = linearMap(sourceValues, targetValues, x)
80 
81     fun mapBack(x: Float) = linearMap(targetValues, sourceValues, x)
82 
83     companion object {
84         @JvmField
85         val Identity =
86             DoubleMapper(
87                 // We need any 2 points in the (x, x) diagonal, with x in the [0, 1) range,
88                 // We spread them as much as possible to minimize float errors.
89                 0f to 0f,
90                 0.5f to 0.5f
91             )
92     }
93 }
94 
95 // Verify that a list of progress values are all in the range [0.0, 1.0) and is monotonically
96 // increasing, with the exception of maybe one time in which the progress wraps around, this check
97 // needs to include all pairs of consecutive elements in the list plus the last to first element
98 // pair.
99 // For example: (0.0, 0.3, 0.6) is a valid list, so are (0.3, 0.6, 0.0) and (0.6, 0.3, 0.0).
100 // On the other hand, something like (0.5, 0.0, 0.7) is not (since it goes down twice, from 0.5 to
101 // 0.0 and then from to 0.7 to 0.5).
validateProgressnull102 internal fun validateProgress(p: FloatList) {
103     var prev = p.last()
104     var wraps = 0
105     for (i in 0 until p.size) {
106         val curr = p[i]
107         require(curr >= 0f && curr < 1f) {
108             "FloatMapping - Progress outside of range: " + p.joinToString()
109         }
110         require(progressDistance(curr, prev) > DistanceEpsilon) {
111             "FloatMapping - Progress repeats a value: " + p.joinToString()
112         }
113         if (curr < prev) {
114             wraps++
115             require(wraps <= 1) {
116                 "FloatMapping - Progress wraps more than once: " + p.joinToString()
117             }
118         }
119         prev = curr
120     }
121 }
122 
123 // Distance between two progress values.
124 // Since progress wraps around, we consider a difference of 0.99 as a distance of 0.01
<lambda>null125 internal fun progressDistance(p1: Float, p2: Float) = abs(p1 - p2).let { min(it, 1f - it) }
126