FloatMapping.kt
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.graphics.shapes
/**
* Checks if the given progress is in the given progress range, since progress is in the [0..1)
* interval, and wraps, there is a special case when progressTo < progressFrom.
* For example, if the progress range is 0.7 to 0.2, both 0.8 and 0.1 are inside and 0.5 is outside.
*/
internal fun progressInRange(progress: Float, progressFrom: Float, progressTo: Float) =
if (progressTo >= progressFrom) {
progress in progressFrom..progressTo
} else {
progress >= progressFrom || progress <= progressTo
}
/**
* Maps from one set of progress values to another. This is used by DoubleMapper to retrieve the
* value on one shape that maps to the appropriate value on the other.
*/
internal fun linearMap(xValues: List<Float>, yValues: List<Float>, x: Float): Float {
require(x in 0f..1f) { "Invalid progress: $x" }
val segmentStartIndex = xValues.indices.first {
progressInRange(x, xValues[it], xValues[(it + 1) % xValues.size])
}
val segmentEndIndex = (segmentStartIndex + 1) % xValues.size
val segmentSizeX = positiveModule(
xValues[segmentEndIndex] - xValues[segmentStartIndex],
1f
)
val segmentSizeY = positiveModule(
yValues[segmentEndIndex] - yValues[segmentStartIndex],
1f
)
val positionInSegment = segmentSizeX.let {
if (it < 0.001f) 0.5f else positiveModule(x - xValues[segmentStartIndex], 1f) / it
}
return positiveModule(
yValues[segmentStartIndex] + segmentSizeY * positionInSegment,
1f
)
}
/**
* DoubleMapper creates mappings from values in the [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] } 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 also be used the other way around (using the mapBack function), resulting in:
* { if y in [0 .. 0.25] } x = y * 2
* { if y in [0.25 .. 1] } x = (y + 0.5) / 1.5
* This is used to create mappings of progress values between the start and end shape, which is then
* used to insert new curves and match curves overall.
*/
internal class DoubleMapper(vararg mappings: Pair<Float, Float>) {
private val sourceValues = mappings.map { it.first }
private val targetValues = mappings.map { it.second }
init {
validateProgress(sourceValues)
validateProgress(targetValues)
}
fun map(x: Float) = linearMap(sourceValues, targetValues, x)
fun mapBack(x: Float) = linearMap(targetValues, sourceValues, x)
companion object {
@JvmField
val Identity = DoubleMapper(
// We need any 2 points in the (x, x) diagonal, with x in the [0, 1) range,
// We spread them as much as possible to minimize float errors.
0f to 0f,
0.5f to 0.5f
)
}
}
internal fun validateProgress(p: List<Float>) {
require(p.all { it in 0f..1f }) {
"FloatMapping - Progress outside of range: " + p.joinToString()
}
val wraps = (1 until p.size).count { p[it] < p[it - 1] }
require(wraps <= 1) {
"FloatMapping - Progress wraps more than once: " + p.joinToString()
}
}