1 /*
2  * Copyright (C) 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 com.google.android.torus.utils.animation
18 
19 import com.google.android.torus.math.MathUtils
20 import kotlin.math.pow
21 
22 /** Utilities to help implement "easing" operations. */
23 object EasingUtils {
24     /**
25      * Easing function to interpolate a smooth curve that "follows" some other signal value in
26      * real-time. The "follow curve" is an exponentially-weighted moving average (EWMA) of the
27      * signal values: assuming a fixed timestep, the "follow value" at time t is determined by the
28      * signal value S_t as `F_t = k * S_t + (1 - k) * F_(t-1)`, for some "easing rate" k between
29      * 0 and 1. Note this formulation assumes that the "signal curve" moves by discrete steps with
30      * zero velocity in between. This may cause slightly unexpected "follow" behavior -- e.g. the
31      * curve may start to "settle" toward the new signal value even if we don't expect it to be
32      * stable, or it may lag and/or move somewhat abruptly if the "signal curve" reverses direction.
33      * These discrepancies would be most noticeable at frame rates that are especially low or
34      * highly-variable, and so far they haven't seemed problematic in any of our applications.
35      *
36      * @param currentValue The value of the "follow curve" prior to this update step (i.e., either
37      * the value returned the last time this function was called, or the initial value where the
38      * follow curve should start). In most applications the initial value will be set to match
39      * the first reading of the signal value.
40      * @param targetValue The most recent reading of the "signal value." If this value remains
41      * constant, the "follow curve" will eventually settle to it (asymptotically).
42      * @param easingRate A parameter to control the "follow speed" between 0 (the follow curve
43      * remains at its |currentValue| regardless of the new signal) and 1 (the follow curve
44      * immediately snaps to the new |targetValue|, effectively disabling easing).
45      * This parameter is typically tuned empirically. If the simulation is running at 60FPS, the
46      * easing function exactly matches the "fixed timestep" version above, with easing rate k.
47      * @param deltaSeconds The amount of time elapsed since determining the old |currentValue|, in
48      * seconds, during which the "follow curve" is assumed to have been converging towards the new
49      * |targetValue|.
50      *
51      * @return the value of the "easing curve" after updating by |deltaSeconds|.
52      */
53     @JvmStatic
calculateEasingnull54     fun calculateEasing(
55         currentValue: Float, targetValue: Float, easingRate: Float, deltaSeconds: Float
56     ): Float {
57         /* The exponential form of easing we use to support variable frame rates is inverted from
58          * the fixed timestep version above; an easing rate of zero "disables easing" so that the
59          * follow curve "snaps" to the new value, while an easing rate of one leaves the follow
60          * curve at its current value. We can simply take the complement: */
61         val exponentialEasingRate = 1f - easingRate
62 
63         val lerpBy = 1f - exponentialEasingRate.pow(deltaSeconds)
64         return MathUtils.lerp(currentValue, targetValue, lerpBy)
65     }
66 }
67