Skip to content

Commit

Permalink
Migrate SpringAnimation to Kotlin
Browse files Browse the repository at this point in the history
Differential Revision: D60347906
  • Loading branch information
rshest authored and facebook-github-bot committed Jul 30, 2024
1 parent 7d0a3cc commit 8c32ca7
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 207 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.animated

import com.facebook.react.bridge.ReadableMap
import kotlin.math.*

/**
* Implementation of [AnimationDriver] providing support for spring animations. The implementation
* has been copied from android implementation of Rebound library (see
* [http://facebook.github.io/rebound/](http://facebook.github.io/rebound/))
*/
internal class SpringAnimation(config: ReadableMap) : AnimationDriver() {
// storage for the current and prior physics state while integration is occurring
private data class PhysicsState(var position: Double = 0.0, var velocity: Double = 0.0)

private var lastTime: Long = 0
private var springStarted = false
// configuration
private var springStiffness = 0.0
private var springDamping = 0.0
private var springMass = 0.0
private var initialVelocity = 0.0
private var overshootClampingEnabled = false
// all physics simulation objects are final and reused in each processing pass
private val currentState = PhysicsState()
private var startValue = 0.0
private var endValue = 0.0
// thresholds for determining when the spring is at rest
private var restSpeedThreshold = 0.0
private var displacementFromRestThreshold = 0.0
private var timeAccumulator = 0.0
// for controlling loop
private var iterations = 0
private var currentLoop = 0
private var originalValue = 0.0

init {
currentState.velocity = config.getDouble("initialVelocity")
resetConfig(config)
}

override fun resetConfig(config: ReadableMap) {
springStiffness = config.getDouble("stiffness")
springDamping = config.getDouble("damping")
springMass = config.getDouble("mass")
initialVelocity = currentState.velocity
endValue = config.getDouble("toValue")
restSpeedThreshold = config.getDouble("restSpeedThreshold")
displacementFromRestThreshold = config.getDouble("restDisplacementThreshold")
overshootClampingEnabled = config.getBoolean("overshootClamping")
iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1
mHasFinished = iterations == 0
currentLoop = 0
timeAccumulator = 0.0
springStarted = false
}

override fun runAnimationStep(frameTimeNanos: Long) {
val frameTimeMillis = frameTimeNanos / 1_000_000
if (!springStarted) {
if (currentLoop == 0) {
originalValue = mAnimatedValue.nodeValue
currentLoop = 1
}
currentState.position = mAnimatedValue.nodeValue
startValue = currentState.position
lastTime = frameTimeMillis
timeAccumulator = 0.0
springStarted = true
}
advance((frameTimeMillis - lastTime) / 1000.0)
lastTime = frameTimeMillis
mAnimatedValue.nodeValue = currentState.position
if (isAtRest) {
if (iterations == -1 || currentLoop < iterations) { // looping animation, return to start
springStarted = false
mAnimatedValue.nodeValue = originalValue
currentLoop++
} else { // animation has completed
mHasFinished = true
}
}
}

/**
* get the displacement from rest for a given physics state
*
* @param state the state to measure from
* @return the distance displaced by
*/
private fun getDisplacementDistanceForState(state: PhysicsState): Double =
abs(endValue - state.position)

private val isAtRest: Boolean
/**
* check if the current state is at rest
*
* @return is the spring at rest
*/
get() =
(abs(currentState.velocity) <= restSpeedThreshold &&
(getDisplacementDistanceForState(currentState) <= displacementFromRestThreshold ||
springStiffness == 0.0))

/* Check if the spring is overshooting beyond its target. */
private val isOvershooting: Boolean
get() =
(springStiffness > 0 &&
(startValue < endValue && currentState.position > endValue ||
startValue > endValue && currentState.position < endValue))

private fun advance(realDeltaTime: Double) {
if (isAtRest) {
return
}

// clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able
// to catch up in a subsequent advance if necessary.
var adjustedDeltaTime = realDeltaTime
if (realDeltaTime > MAX_DELTA_TIME_SEC) {
adjustedDeltaTime = MAX_DELTA_TIME_SEC
}
timeAccumulator += adjustedDeltaTime
val c = springDamping
val m = springMass
val k = springStiffness
val v0 = -initialVelocity
val zeta = c / (2 * sqrt(k * m))
val omega0 = sqrt(k / m)
val omega1 = omega0 * sqrt(1.0 - zeta * zeta)
val x0 = endValue - startValue
val velocity: Double
val position: Double
val t = timeAccumulator
if (zeta < 1) {
// Under damped
val envelope = exp(-zeta * omega0 * t)
position =
(endValue -
envelope *
((v0 + zeta * omega0 * x0) / omega1 * sin(omega1 * t) + x0 * cos(omega1 * t)))
// This looks crazy -- it's actually just the derivative of the
// oscillation function
velocity =
((zeta *
omega0 *
envelope *
(sin(omega1 * t) * (v0 + zeta * omega0 * x0) / omega1 + x0 * cos(omega1 * t))) -
envelope *
(cos(omega1 * t) * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin(omega1 * t)))
} else {
// Critically damped spring
val envelope = exp(-omega0 * t)
position = endValue - envelope * (x0 + (v0 + omega0 * x0) * t)
velocity = envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0))
}
currentState.position = position
currentState.velocity = velocity

// End the spring immediately if it is overshooting and overshoot clamping is enabled.
// Also make sure that if the spring was considered within a resting threshold that it's now
// snapped to its end value.
if (isAtRest || overshootClampingEnabled && isOvershooting) {
// Don't call setCurrentValue because that forces a call to onSpringUpdate
if (springStiffness > 0) {
startValue = endValue
currentState.position = endValue
} else {
endValue = currentState.position
startValue = endValue
}
currentState.velocity = 0.0
}
}

companion object {
// maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS)
private const val MAX_DELTA_TIME_SEC = 0.064
}
}

0 comments on commit 8c32ca7

Please sign in to comment.