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