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