summaryrefslogtreecommitdiff
path: root/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt
diff options
context:
space:
mode:
Diffstat (limited to 'toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt')
-rw-r--r--toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt66
1 files changed, 66 insertions, 0 deletions
diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt
new file mode 100644
index 0000000..d9b33a4
--- /dev/null
+++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 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 com.google.android.torus.utils.animation
+
+import com.google.android.torus.math.MathUtils
+import kotlin.math.pow
+
+/** Utilities to help implement "easing" operations. */
+object EasingUtils {
+ /**
+ * Easing function to interpolate a smooth curve that "follows" some other signal value in
+ * real-time. The "follow curve" is an exponentially-weighted moving average (EWMA) of the
+ * signal values: assuming a fixed timestep, the "follow value" at time t is determined by the
+ * signal value S_t as `F_t = k * S_t + (1 - k) * F_(t-1)`, for some "easing rate" k between
+ * 0 and 1. Note this formulation assumes that the "signal curve" moves by discrete steps with
+ * zero velocity in between. This may cause slightly unexpected "follow" behavior -- e.g. the
+ * curve may start to "settle" toward the new signal value even if we don't expect it to be
+ * stable, or it may lag and/or move somewhat abruptly if the "signal curve" reverses direction.
+ * These discrepancies would be most noticeable at frame rates that are especially low or
+ * highly-variable, and so far they haven't seemed problematic in any of our applications.
+ *
+ * @param currentValue The value of the "follow curve" prior to this update step (i.e., either
+ * the value returned the last time this function was called, or the initial value where the
+ * follow curve should start). In most applications the initial value will be set to match
+ * the first reading of the signal value.
+ * @param targetValue The most recent reading of the "signal value." If this value remains
+ * constant, the "follow curve" will eventually settle to it (asymptotically).
+ * @param easingRate A parameter to control the "follow speed" between 0 (the follow curve
+ * remains at its |currentValue| regardless of the new signal) and 1 (the follow curve
+ * immediately snaps to the new |targetValue|, effectively disabling easing).
+ * This parameter is typically tuned empirically. If the simulation is running at 60FPS, the
+ * easing function exactly matches the "fixed timestep" version above, with easing rate k.
+ * @param deltaSeconds The amount of time elapsed since determining the old |currentValue|, in
+ * seconds, during which the "follow curve" is assumed to have been converging towards the new
+ * |targetValue|.
+ *
+ * @return the value of the "easing curve" after updating by |deltaSeconds|.
+ */
+ @JvmStatic
+ fun calculateEasing(
+ currentValue: Float, targetValue: Float, easingRate: Float, deltaSeconds: Float
+ ): Float {
+ /* The exponential form of easing we use to support variable frame rates is inverted from
+ * the fixed timestep version above; an easing rate of zero "disables easing" so that the
+ * follow curve "snaps" to the new value, while an easing rate of one leaves the follow
+ * curve at its current value. We can simply take the complement: */
+ val exponentialEasingRate = 1f - easingRate
+
+ val lerpBy = 1f - exponentialEasingRate.pow(deltaSeconds)
+ return MathUtils.lerp(currentValue, targetValue, lerpBy)
+ }
+}