
commonMain.korlibs.math.interpolation.Easing.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of korma Show documentation
Show all versions of korma Show documentation
Mathematic library for Multiplatform Kotlin 1.3
The newest version!
package korlibs.math.interpolation
import korlibs.math.geom.*
import korlibs.math.geom.bezier.*
import korlibs.memory.*
import kotlin.math.*
private inline fun combine(it: Float, start: Easing, end: Easing) =
if (it < .5f) .5f * start(it * 2f) else .5f * end((it - .5f) * 2f) + .5f
private const val BOUNCE_FACTOR = 1.70158f
private const val HALF_PI = PI.toFloat() / 2f
@Suppress("unused")
fun interface Easing {
operator fun invoke(it: Float): Float
operator fun invoke(it: Double): Double = invoke(it.toFloat()).toDouble()
companion object {
operator fun invoke(name: () -> String, block: (Float) -> Float): Easing {
return object : Easing {
override fun invoke(it: Float): Float = block(it)
override fun toString(): String = name()
}
}
fun steps(steps: Int, easing: Easing): Easing = Easing({ "steps($steps, $easing)" }) {
easing((it * steps).toInt().toFloat() / steps)
}
fun cubic(x1: Float, y1: Float, x2: Float, y2: Float, name: String? = null): Easing = EasingCubic(x1, y1, x2, y2, name)
fun cubic(x1: Double, y1: Double, x2: Double, y2: Double, name: String? = null): Easing = EasingCubic(x1, y1, x2, y2, name)
fun cubic(f: (t: Float, b: Float, c: Float, d: Float) -> Float): Easing = Easing { f(it, 0f, 1f, 1f) }
fun combine(start: Easing, end: Easing) = Easing { combine(it, start, end) }
private val _ALL_LIST: List by lazy(LazyThreadSafetyMode.PUBLICATION) {
Easings.values().toList()
}
val ALL_LIST: List get() = _ALL_LIST
/**
* Retrieves a mapping of all standard easings defined directly in [Easing], for example "SMOOTH" -> Easing.SMOOTH.
*/
val ALL: Map by lazy(LazyThreadSafetyMode.PUBLICATION) {
_ALL_LIST.associateBy { it.name }
}
// Author's note:
// 1. Make sure new standard easings are added both here and in the Easings enum class
// 2. Make sure the name is the same, otherwise [ALL] will return confusing results
val SMOOTH: Easing get() = Easings.SMOOTH
val EASE_IN_ELASTIC: Easing get() = Easings.EASE_IN_ELASTIC
val EASE_OUT_ELASTIC: Easing get() = Easings.EASE_OUT_ELASTIC
val EASE_OUT_BOUNCE: Easing get() = Easings.EASE_OUT_BOUNCE
val LINEAR: Easing get() = Easings.LINEAR
val EASE: Easing get() = Easings.EASE
val EASE_IN: Easing get() = Easings.EASE_IN
val EASE_OUT: Easing get() = Easings.EASE_OUT
val EASE_IN_OUT: Easing get() = Easings.EASE_IN_OUT
val EASE_IN_OLD: Easing get() = Easings.EASE_IN_OLD
val EASE_OUT_OLD: Easing get() = Easings.EASE_OUT_OLD
val EASE_IN_OUT_OLD: Easing get() = Easings.EASE_IN_OUT_OLD
val EASE_OUT_IN_OLD: Easing get() = Easings.EASE_OUT_IN_OLD
val EASE_IN_BACK: Easing get() = Easings.EASE_IN_BACK
val EASE_OUT_BACK: Easing get() = Easings.EASE_OUT_BACK
val EASE_IN_OUT_BACK: Easing get() = Easings.EASE_IN_OUT_BACK
val EASE_OUT_IN_BACK: Easing get() = Easings.EASE_OUT_IN_BACK
val EASE_IN_OUT_ELASTIC: Easing get() = Easings.EASE_IN_OUT_ELASTIC
val EASE_OUT_IN_ELASTIC: Easing get() = Easings.EASE_OUT_IN_ELASTIC
val EASE_IN_BOUNCE: Easing get() = Easings.EASE_IN_BOUNCE
val EASE_IN_OUT_BOUNCE: Easing get() = Easings.EASE_IN_OUT_BOUNCE
val EASE_OUT_IN_BOUNCE: Easing get() = Easings.EASE_OUT_IN_BOUNCE
val EASE_IN_QUAD: Easing get() = Easings.EASE_IN_QUAD
val EASE_OUT_QUAD: Easing get() = Easings.EASE_OUT_QUAD
val EASE_IN_OUT_QUAD: Easing get() = Easings.EASE_IN_OUT_QUAD
val EASE_SINE: Easing get() = Easings.EASE_SINE
val EASE_CLAMP_START: Easing get() = Easings.EASE_CLAMP_START
val EASE_CLAMP_END: Easing get() = Easings.EASE_CLAMP_END
val EASE_CLAMP_MIDDLE: Easing get() = Easings.EASE_CLAMP_MIDDLE
}
}
// @TODO: We need to heavily optimize this. If we can have a formula instead of doing a bisect, this would be much faster.
class EasingCubic(val x1: Float, val y1: Float, val x2: Float, val y2: Float, val name: String? = null) : Easing {
constructor(x1: Double, y1: Double, x2: Double, y2: Double, name: String? = null) : this(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat(), name)
val cubic = Bezier(Point(0f, 0f), Point(x1.clamp(0f, 1f), y1), Point(x2.clamp(0f, 1f), y2), Point(1.0, 1.0))
override fun toString(): String = name ?: "cubic-bezier($x1, $y1, $x2, $y2)"
// @TODO: this doesn't work properly for `it` outside range [0, 1], and not in constant time
override fun invoke(it: Float): Float {
var pivotLeft = if (it < 0f) it * 10f else 0f
var pivotRight = if (it > 1f) it * 10f else 1f
//var pivot = (pivotLeft + pivotRight) * 0.5
var pivot = it
//println(" - x=$x, time=$time, pivotLeft=$pivotLeft, pivotRight=$pivotRight, pivot=$pivot")
var lastX = 0f
var lastY = 0f
var steps = 0
for (n in 0 until 50) {
steps++
val res = cubic.calc(pivot)
lastX = res.x
lastY = res.y
if ((lastX - it).absoluteValue < 0.001) break
if (it < lastX) {
pivotRight = pivot
pivot = (pivotLeft + pivot) * 0.5f
} else if (it > lastX) {
pivotLeft = pivot
pivot = (pivotRight + pivot) * 0.5f
} else {
break
}
}
//println("Requested steps=$steps, deviation=${(lastX - x).absoluteValue} requestedX=$x, lastX=$lastX, pivot=$pivot, pivotLeft=$pivotLeft, pivotRight=$pivotRight, lastY=$lastY")
return lastY
}
/*
override fun invoke(it: Double): Double {
val points = listOf(cubic.p0, cubic.p1, cubic.p2, cubic.p3)
val time = it
/** Step 0 */
val n = 5
val x = doubleArrayOf(0.0, points[0].x, points[1].x, points[2].x, points[3].x, 1.0)
val a = doubleArrayOf(0.0, points[0].y, points[1].y, points[2].y, points[3].y, 1.0)
val h = DoubleArray(n)
val A = DoubleArray(n)
val l = DoubleArray(n + 1)
val u = DoubleArray(n + 1)
val z = DoubleArray(n + 1)
val c = DoubleArray(n + 1)
val b = DoubleArray(n)
val d = DoubleArray(n)
/** Step 1 */
for (i in 0 until n) h[i] = x[i + 1] - x[i]
/** Step 2 */
for (i in 1 until n) A[i] = (3.0 * (a[i + 1] - a[i]) / h[i]) - (3.0 * (a[i] - a[i - 1]) / h[i - 1])
/** Step 3 */
l[0] = 1.0
u[0] = 0.0
z[0] = 0.0
/** Step 4 */
for (i in 1 until n) {
l[i] = 2.0 * (x[i + 1] - x[i - 1]) - (h[i - 1] * u[i - 1])
u[i] = h[i] / l[i]
z[i] = (A[i] - h[i - 1] * z[i - 1]) / l[i]
}
/** Step 5 */
l[n] = 1.0
z[n] = 0.0
c[n] = 0.0
/** Step 6 */
for (j in (n - 1) downTo 0) {
c[j] = z[j] - (u[j] * c[j + 1])
b[j] = ((a[j + 1] - a[j]) / h[j]) - (h[j] * (c[j + 1] + 2 * c[j]) / 3)
d[j] = (c[j + 1] - c[j]) / (3 * h[j])
}
// get t position
var result = 0.0
var t = time
for (i in 0 until n) {
if (t >= x[i] && t < x[i + 1]) {
t -= x[i]
result = a[i] + b[i] * t + c[i] * t * t + d[i] * t * t * t
}
}
return result
}
*/
}
private enum class Easings : Easing {
SMOOTH {
override fun invoke(it: Float): Float = it * it * (3 - 2 * it)
},
EASE_IN_ELASTIC {
override fun invoke(it: Float): Float =
if (it == 0f || it == 1f) {
it
} else {
val p = 0.3f
val s = p / 4.0f
val inv = it - 1
-1f * 2f.pow(10f * inv) * sin((inv - s) * (2f * PI.toFloat()) / p)
}
},
EASE_OUT_ELASTIC {
override fun invoke(it: Float): Float =
if (it == 0f || it == 1f) {
it
} else {
val p = 0.3f
val s = p / 4.0f
2.0f.pow(-10.0f * it) * sin((it - s) * (2.0f * PI.toFloat()) / p) + 1
}
},
EASE_OUT_BOUNCE {
override fun invoke(it: Float): Float {
val s = 7.5625f
val p = 2.75f
return when {
it < 1f / p -> s * it.pow(2f)
it < 2f / p -> s * (it - 1.5f / p).pow(2.0f) + 0.75f
it < 2.5f / p -> s * (it - 2.25f / p).pow(2.0f) + 0.9375f
else -> s * (it - 2.625f / p).pow(2.0f) + 0.984375f
}
}
},
//Easing.cubic(0.0, 0.0, 1.0, 1.0, "linear"),
LINEAR {
override fun invoke(it: Float): Float = it
},
// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function
EASE {
val easing = EasingCubic(0.25f, 0.1f, 0.25f, 1.0f, "ease")
override fun invoke(it: Float): Float = easing.invoke(it)
},
EASE_IN {
val easing = EasingCubic(0.42f, 0.0f, 1.0f, 1.0f, "ease-in")
override fun invoke(it: Float): Float = easing.invoke(it)
},
EASE_OUT {
val easing = EasingCubic(0.0f, 0.0f, 0.58f, 1.0f, "ease-out")
override fun invoke(it: Float): Float = easing.invoke(it)
},
EASE_IN_OUT {
val easing = EasingCubic(0.42f, 0.0f, 0.58f, 1.0f, "ease-in-out")
override fun invoke(it: Float): Float = easing.invoke(it)
},
//EASE_OUT_IN {
// val easing = EasingCubic(-, "ease-out-in")
// override fun invoke(it: Double): Double = easing.invoke(it)
//},
EASE_IN_OLD {
override fun invoke(it: Float): Float = it * it * it
},
EASE_OUT_OLD {
override fun invoke(it: Float): Float =
(it - 1f).let { inv ->
inv * inv * inv + 1
}
},
EASE_IN_OUT_OLD {
override fun invoke(it: Float): Float = combine(it, EASE_IN_OLD, EASE_OUT_OLD)
},
EASE_OUT_IN_OLD {
override fun invoke(it: Float): Float = combine(it, EASE_OUT_OLD, EASE_IN_OLD)
},
EASE_IN_BACK {
override fun invoke(it: Float): Float = it.pow(2f) * ((BOUNCE_FACTOR + 1f) * it - BOUNCE_FACTOR)
},
EASE_OUT_BACK {
override fun invoke(it: Float): Float =
(it - 1f).let { inv ->
inv.pow(2f) * ((BOUNCE_FACTOR + 1f) * inv + BOUNCE_FACTOR) + 1f
}
},
EASE_IN_OUT_BACK {
override fun invoke(it: Float): Float = combine(it, EASE_IN_BACK, EASE_OUT_BACK)
},
EASE_OUT_IN_BACK {
override fun invoke(it: Float): Float = combine(it, EASE_OUT_BACK, EASE_IN_BACK)
},
EASE_IN_OUT_ELASTIC {
override fun invoke(it: Float): Float = combine(it, EASE_IN_ELASTIC, EASE_OUT_ELASTIC)
},
EASE_OUT_IN_ELASTIC {
override fun invoke(it: Float): Float = combine(it, EASE_OUT_ELASTIC, EASE_IN_ELASTIC)
},
EASE_IN_BOUNCE {
override fun invoke(it: Float): Float = 1f - EASE_OUT_BOUNCE(1f - it)
},
EASE_IN_OUT_BOUNCE {
override fun invoke(it: Float): Float = combine(it, EASE_IN_BOUNCE, EASE_OUT_BOUNCE)
},
EASE_OUT_IN_BOUNCE {
override fun invoke(it: Float): Float = combine(it, EASE_OUT_BOUNCE, EASE_IN_BOUNCE)
},
EASE_IN_QUAD {
override fun invoke(it: Float): Float = 1f * it * it
},
EASE_OUT_QUAD {
override fun invoke(it: Float): Float = -1f * it * (it - 2)
},
EASE_IN_OUT_QUAD {
override fun invoke(it: Float): Float =
(it * 2f).let { t ->
if (t < 1) {
+1f / 2 * t * t
} else {
-1f / 2 * ((t - 1) * ((t - 1) - 2) - 1)
}
}
},
EASE_SINE {
override fun invoke(it: Float): Float = sin(it * HALF_PI)
},
EASE_CLAMP_START {
override fun invoke(it: Float): Float = if (it <= 0f) 0f else 1f
},
EASE_CLAMP_END {
override fun invoke(it: Float): Float = if (it < 1f) 0f else 1f
},
EASE_CLAMP_MIDDLE {
override fun invoke(it: Float): Float = if (it < 0.5f) 0f else 1f
};
override fun toString(): String = super.toString().replace('_', '-').lowercase()
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy