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