• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.android.mechanics.spring
18 
19 import androidx.compose.ui.util.packFloats
20 import androidx.compose.ui.util.unpackFloat1
21 import androidx.compose.ui.util.unpackFloat2
22 import kotlin.math.cos
23 import kotlin.math.exp
24 import kotlin.math.sin
25 import kotlin.math.sqrt
26 
27 /**
28  * Describes the motion state of a spring.
29  *
30  * @see calculateUpdatedState to simulate the springs movement
31  * @see SpringState function to create this value.
32  */
33 @JvmInline
34 value class SpringState(val packedValue: Long) {
35     val displacement: Float
36         get() = unpackFloat1(packedValue)
37 
38     val velocity: Float
39         get() = unpackFloat2(packedValue)
40 
41     /**
42      * Whether the state is considered stable.
43      *
44      * The amplitude of the remaining movement, for a spring with [parameters] is less than
45      * [stableThreshold]
46      */
isStablenull47     fun isStable(parameters: SpringParameters, stableThreshold: Float): Boolean {
48         if (this == AtRest) return true
49         val currentEnergy = parameters.stiffness * displacement * displacement + velocity * velocity
50         val maxStableEnergy = parameters.stiffness * stableThreshold * stableThreshold
51         return currentEnergy <= maxStableEnergy
52     }
53 
54     /** Adds the specified [displacementDelta] and [velocityDelta] to the returned state. */
nudgenull55     fun nudge(displacementDelta: Float = 0f, velocityDelta: Float = 0f): SpringState {
56         return SpringState(displacement + displacementDelta, velocity + velocityDelta)
57     }
58 
toStringnull59     override fun toString(): String {
60         return "MechanicsSpringState(displacement=$displacement, velocity=$velocity)"
61     }
62 
63     companion object {
64         /** Spring at rest. */
65         val AtRest = SpringState(displacement = 0f, velocity = 0f)
66     }
67 }
68 
69 /** Creates a [SpringState] given [displacement] and [velocity] */
SpringStatenull70 fun SpringState(displacement: Float, velocity: Float = 0f) =
71     SpringState(packFloats(displacement, velocity))
72 
73 /**
74  * Computes the updated [SpringState], after letting the spring with the specified [parameters]
75  * settle for [elapsedNanos].
76  *
77  * This implementation is based on Compose's [SpringSimulation].
78  */
79 fun SpringState.calculateUpdatedState(
80     elapsedNanos: Long,
81     parameters: SpringParameters,
82 ): SpringState {
83     if (parameters.isSnapSpring || this == SpringState.AtRest) {
84         return SpringState.AtRest
85     }
86 
87     val stiffness = parameters.stiffness.toDouble()
88     val naturalFreq = sqrt(stiffness)
89 
90     val dampingRatio = parameters.dampingRatio
91     val displacement = displacement
92     val velocity = velocity
93     val deltaT = elapsedNanos / 1_000_000_000.0 // unit: seconds
94     val dampingRatioSquared = dampingRatio * dampingRatio.toDouble()
95     val r = -dampingRatio * naturalFreq
96 
97     val currentDisplacement: Double
98     val currentVelocity: Double
99 
100     if (dampingRatio > 1) {
101         // Over damping
102         val s = naturalFreq * sqrt(dampingRatioSquared - 1)
103         val gammaPlus = r + s
104         val gammaMinus = r - s
105 
106         val coeffB = (gammaMinus * displacement - velocity) / (gammaMinus - gammaPlus)
107         val coeffA = displacement - coeffB
108         currentDisplacement = (coeffA * exp(gammaMinus * deltaT) + coeffB * exp(gammaPlus * deltaT))
109         currentVelocity =
110             (coeffA * gammaMinus * exp(gammaMinus * deltaT) +
111                 coeffB * gammaPlus * exp(gammaPlus * deltaT))
112     } else if (dampingRatio == 1.0f) {
113         // Critically damped
114         val coeffA = displacement
115         val coeffB = velocity + naturalFreq * displacement
116         val nFdT = -naturalFreq * deltaT
117         currentDisplacement = (coeffA + coeffB * deltaT) * exp(nFdT)
118         currentVelocity =
119             (((coeffA + coeffB * deltaT) * exp(nFdT) * (-naturalFreq)) + coeffB * exp(nFdT))
120     } else {
121         // Underdamped
122         val dampedFreq = naturalFreq * sqrt(1 - dampingRatioSquared)
123         val cosCoeff = displacement
124         val sinCoeff = ((1 / dampedFreq) * (((-r * displacement) + velocity)))
125         val dFdT = dampedFreq * deltaT
126         currentDisplacement = (exp(r * deltaT) * ((cosCoeff * cos(dFdT) + sinCoeff * sin(dFdT))))
127         currentVelocity =
128             (currentDisplacement * r +
129                 (exp(r * deltaT) *
130                     ((-dampedFreq * cosCoeff * sin(dFdT) + dampedFreq * sinCoeff * cos(dFdT)))))
131     }
132 
133     return SpringState(currentDisplacement.toFloat(), currentVelocity.toFloat())
134 }
135