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