1 /*
2  * Copyright 2019 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 @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
18 
19 package androidx.compose.animation.core
20 
21 import androidx.compose.ui.util.packFloats
22 import androidx.compose.ui.util.unpackFloat1
23 import androidx.compose.ui.util.unpackFloat2
24 import kotlin.math.cos
25 import kotlin.math.exp
26 import kotlin.math.sin
27 import kotlin.math.sqrt
28 
29 @kotlin.jvm.JvmInline
30 internal value class Motion(val packedValue: Long) {
31     inline val value: Float
32         get() = unpackFloat1(packedValue)
33 
34     inline val velocity: Float
35         get() = unpackFloat2(packedValue)
36 }
37 
Motionnull38 internal inline fun Motion(value: Float, velocity: Float) = Motion(packFloats(value, velocity))
39 
40 /**
41  * Spring Simulation simulates spring physics, and allows you to query the motion (i.e. value and
42  * velocity) at certain time in the future based on the starting velocity and value.
43  *
44  * By configuring the stiffness and damping ratio, callers can create a spring with the look and
45  * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
46  * is, the harder it is to stretch it, the faster it undergoes dampening.
47  *
48  * Spring damping ratio describes how oscillations in a system decay after a disturbance. When
49  * damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
50  * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
51  * return to equilibrium within the shortest amount of time. When damping ratio is less than 1 (i.e.
52  * under-damped), the mass tends to overshoot, and return, and overshoot again. Without any damping
53  * (i.e. damping ratio = 0), the mass will oscillate forever.
54  */
55 internal class SpringSimulation(var finalPosition: Float) {
56     // Natural frequency
57     private var naturalFreq = sqrt(Spring.StiffnessVeryLow.toDouble())
58 
59     /** Stiffness of the spring. */
60     var stiffness: Float
61         set(value) {
62             if (stiffness <= 0) {
63                 throwIllegalArgumentException("Spring stiffness constant must be positive.")
64             }
65             naturalFreq = sqrt(value.toDouble())
66         }
67         get() {
68             return (naturalFreq * naturalFreq).toFloat()
69         }
70 
71     /**
72      * Returns the damping ratio of the spring.
73      *
74      * @return damping ratio of the spring
75      */
76     var dampingRatio: Float = Spring.DampingRatioNoBouncy
77         set(value) {
78             if (value < 0) {
79                 throwIllegalArgumentException("Damping ratio must be non-negative")
80             }
81             field = value
82         }
83 
84     /** ********************* Below are private APIs */
85     fun getAcceleration(lastDisplacement: Float, lastVelocity: Float): Float {
86         val adjustedDisplacement = lastDisplacement - finalPosition
87 
88         val k = naturalFreq * naturalFreq
89         val c = 2.0 * naturalFreq * dampingRatio
90 
91         return (-k * adjustedDisplacement - c * lastVelocity).toFloat()
92     }
93 
94     /**
95      * Internal only call for Spring to calculate the spring position/velocity using an analytical
96      * approach.
97      */
98     internal fun updateValues(
99         lastDisplacement: Float,
100         lastVelocity: Float,
101         timeElapsed: Long
102     ): Motion {
103         val adjustedDisplacement = lastDisplacement - finalPosition
104         val deltaT = timeElapsed / 1000.0 // unit: seconds
105         val dampingRatioSquared = dampingRatio * dampingRatio.toDouble()
106         val r = -dampingRatio * naturalFreq
107 
108         val displacement: Double
109         val currentVelocity: Double
110 
111         if (dampingRatio > 1) {
112             // Over damping
113             val s = naturalFreq * sqrt(dampingRatioSquared - 1)
114             val gammaPlus = r + s
115             val gammaMinus = r - s
116 
117             // Overdamped
118             val coeffB =
119                 (gammaMinus * adjustedDisplacement - lastVelocity) / (gammaMinus - gammaPlus)
120             val coeffA = adjustedDisplacement - coeffB
121             displacement = (coeffA * exp(gammaMinus * deltaT) + coeffB * exp(gammaPlus * deltaT))
122             currentVelocity =
123                 (coeffA * gammaMinus * exp(gammaMinus * deltaT) +
124                     coeffB * gammaPlus * exp(gammaPlus * deltaT))
125         } else if (dampingRatio == 1.0f) {
126             // Critically damped
127             val coeffA = adjustedDisplacement
128             val coeffB = lastVelocity + naturalFreq * adjustedDisplacement
129             val nFdT = -naturalFreq * deltaT
130             displacement = (coeffA + coeffB * deltaT) * exp(nFdT)
131             currentVelocity =
132                 (((coeffA + coeffB * deltaT) * exp(nFdT) * (-naturalFreq)) + coeffB * exp(nFdT))
133         } else {
134             val dampedFreq = naturalFreq * sqrt(1 - dampingRatioSquared)
135             // Underdamped
136             val cosCoeff = adjustedDisplacement
137             val sinCoeff = ((1 / dampedFreq) * (((-r * adjustedDisplacement) + lastVelocity)))
138             val dFdT = dampedFreq * deltaT
139             displacement = (exp(r * deltaT) * ((cosCoeff * cos(dFdT) + sinCoeff * sin(dFdT))))
140             currentVelocity =
141                 (displacement * r +
142                     (exp(r * deltaT) *
143                         ((-dampedFreq * cosCoeff * sin(dFdT) + dampedFreq * sinCoeff * cos(dFdT)))))
144         }
145 
146         val newValue = (displacement + finalPosition).toFloat()
147         val newVelocity = currentVelocity.toFloat()
148 
149         return Motion(newValue, newVelocity)
150     }
151 }
152