/* * Copyright (c) 2013, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ package com.marshalchen.common.uimodule.rebound; /** * Classical spring implementing Hooke's law with configurable friction and tension. */ public class Spring { // unique incrementer id for springs private static int ID = 0; // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS) private static final double MAX_DELTA_TIME_SEC = 0.064; // fixed timestep to use in the physics solver in seconds private static final double SOLVER_TIMESTEP_SEC = 0.001; private SpringConfig mSpringConfig; private boolean mOvershootClampingEnabled; // storage for the current and prior physics state while integration is occurring private static class PhysicsState { double position; double velocity; } // unique id for the spring in the system private final String mId; // all physics simulation objects are final and reused in each processing pass private final PhysicsState mCurrentState = new PhysicsState(); private final PhysicsState mPreviousState = new PhysicsState(); private final PhysicsState mTempState = new PhysicsState(); private double mStartValue; private double mEndValue; private boolean mWasAtRest = true; // thresholds for determining when the spring is at rest private double mRestSpeedThreshold = 0.005; private double mDisplacementFromRestThreshold = 0.005; private ReentrantCallback<SpringListener> mListeners = new ReentrantCallback<SpringListener>(); private double mTimeAccumulator = 0; private final BaseSpringSystem mSpringSystem; /** * create a new spring */ Spring(BaseSpringSystem springSystem) { if (springSystem == null) { throw new IllegalArgumentException("Spring cannot be created outside of a BaseSpringSystem"); } mSpringSystem = springSystem; mId = "spring:" + ID++; setSpringConfig(SpringConfig.defaultConfig); } /** * Destroys this Spring, meaning that it will be deregistered from its BaseSpringSystem so it won't be * iterated anymore and will clear its set of listeners. Do not use the Spring after calling this, * doing so may just cause an exception to be thrown. */ public void destroy() { mListeners.clear(); mSpringSystem.deregisterSpring(this); } /** * get the unique id for this spring * * @return the unique id */ public String getId() { return mId; } /** * set the config class * * @param springConfig config class for the spring * @return this Spring instance for chaining */ public Spring setSpringConfig(SpringConfig springConfig) { if (springConfig == null) { throw new IllegalArgumentException("springConfig is required"); } mSpringConfig = springConfig; return this; } /** * retrieve the spring config for this spring * * @return the SpringConfig applied to this spring */ public SpringConfig getSpringConfig() { return mSpringConfig; } /** * Set the displaced value to determine the displacement for the spring from the rest value. * This value is retained and used to calculate the displacement ratio. * This also updates the start value of the Spring. * * @param currentValue the new start and current value for the spring * @return the spring for chaining */ public Spring setCurrentValue(double currentValue) { mStartValue = currentValue; mCurrentState.position = currentValue; for (SpringListener listener : mListeners) { listener.onSpringUpdate(this); } return this; } /** * Get the displacement value from the last time setCurrentValue was called. * * @return displacement value */ public double getStartValue() { return mStartValue; } /** * Get the current * * @return current value */ public double getCurrentValue() { return mCurrentState.position; } /** * get the displacement of the springs current value from its rest value. * * @return the distance displaced by */ public double getCurrentDisplacementDistance() { return getDisplacementDistanceForState(mCurrentState); } /** * get the displacement from rest for a given physics state * * @param state the state to measure from * @return the distance displaced by */ private double getDisplacementDistanceForState(PhysicsState state) { return Math.abs(mEndValue - state.position); } /** * set the rest value to determine the displacement for the spring * * @param endValue the endValue for the spring * @return the spring for chaining */ public Spring setEndValue(double endValue) { if (mEndValue == endValue && isAtRest()) { return this; } mStartValue = getCurrentValue(); mEndValue = endValue; mSpringSystem.activateSpring(this.getId()); for (SpringListener listener : mListeners) { listener.onSpringEndStateChange(this); } return this; } /** * get the rest value used for determining the displacement of the spring * * @return the rest value for the spring */ public double getEndValue() { return mEndValue; } /** * set the velocity on the spring in pixels per second * * @return the spring for chaining */ public Spring setVelocity(double velocity) { mCurrentState.velocity = velocity; return this; } /** * get the velocity of the spring * * @return the current velocity */ public double getVelocity() { return mCurrentState.velocity; } /** * Sets the speed at which the spring should be considered at rest. * * @param restSpeedThreshold speed pixels per second * @return the spring for chaining */ public Spring setRestSpeedThreshold(double restSpeedThreshold) { mRestSpeedThreshold = restSpeedThreshold; return this; } /** * Returns the speed at which the spring should be considered at rest in pixels per second * * @return speed in pixels per second */ public double getRestSpeedThreshold() { return mRestSpeedThreshold; } /** * set the threshold of displacement from rest below which the spring should be considered at rest * * @param displacementFromRestThreshold displacement to consider resting below * @return the spring for chaining */ public Spring setRestDisplacementThreshold(double displacementFromRestThreshold) { mDisplacementFromRestThreshold = displacementFromRestThreshold; return this; } /** * get the threshold of displacement from rest below which the spring should be considered at rest * * @return displacement to consider resting below */ public double getRestDisplacementThreshold() { return mDisplacementFromRestThreshold; } /** * Force the spring to clamp at its end value to avoid overshooting the target value. * * @param overshootClampingEnabled whether or not to enable overshoot clamping * @return the spring for chaining */ public Spring setOvershootClampingEnabled(boolean overshootClampingEnabled) { mOvershootClampingEnabled = overshootClampingEnabled; return this; } /** * Check if overshoot clamping is enabled. * * @return is overshoot clamping enabled */ public boolean isOvershootClampingEnabled() { return mOvershootClampingEnabled; } /** * Check if the spring is overshooting beyond its target. * * @return true if the spring is overshooting its target */ public boolean isOvershooting() { return (mStartValue < mEndValue && getCurrentValue() > mEndValue) || (mStartValue > mEndValue && getCurrentValue() < mEndValue); } /** * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required * realTimeDelta. * The math is inlined inside the loop since it made a huge performance impact when there are * several springs being advanced. * * @param time clock time * @param realDeltaTime clock drift */ void advance(double time, double realDeltaTime) { boolean isAtRest = isAtRest(); if (isAtRest && mWasAtRest) { /* begin debug Log.d(TAG, "bailing out because we are at rest:" + getName()); end debug */ 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. double adjustedDeltaTime = realDeltaTime; if (realDeltaTime > MAX_DELTA_TIME_SEC) { adjustedDeltaTime = MAX_DELTA_TIME_SEC; } /* begin debug long startTime = System.currentTimeMillis(); int iterations = 0; end debug */ mTimeAccumulator += adjustedDeltaTime; double tension = mSpringConfig.tension; double friction = mSpringConfig.friction; double position = mCurrentState.position; double velocity = mCurrentState.velocity; double tempPosition = mTempState.position; double tempVelocity = mTempState.velocity; double aVelocity, aAcceleration; double bVelocity, bAcceleration; double cVelocity, cAcceleration; double dVelocity, dAcceleration; double dxdt, dvdt; // iterate over the true time while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) { /* begin debug iterations++; end debug */ mTimeAccumulator -= SOLVER_TIMESTEP_SEC; if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) { // This will be the last iteration. Remember the previous state in case we need to // interpolate mPreviousState.position = position; mPreviousState.velocity = velocity; } // Perform an RK4 integration to provide better detection of the acceleration curve via // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation // of the next and taking a weighted sum of the 4 derivatives as the final output. // This math was inlined since it made for big performance improvements when advancing several // springs in one pass of the BaseSpringSystem. // The initial derivative is based on the current velocity and the calculated acceleration aVelocity = velocity; aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity; // Calculate the next derivatives starting with the last derivative and integrating over the // timestep tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5; tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5; bVelocity = tempVelocity; bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5; tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5; cVelocity = tempVelocity; cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC; tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC; dVelocity = tempVelocity; dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; // Take the weighted sum of the 4 derivatives as the final output. dxdt = 1.0 / 6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity); dvdt = 1.0 / 6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration); position += dxdt * SOLVER_TIMESTEP_SEC; velocity += dvdt * SOLVER_TIMESTEP_SEC; } mTempState.position = tempPosition; mTempState.velocity = tempVelocity; mCurrentState.position = position; mCurrentState.velocity = velocity; if (mTimeAccumulator > 0) { interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC); } // 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() || (mOvershootClampingEnabled && isOvershooting())) { // Don't call setCurrentValue because that forces a call to onSpringUpdate mStartValue = mEndValue; mCurrentState.position = mEndValue; setVelocity(0); isAtRest = true; } /* begin debug long endTime = System.currentTimeMillis(); long elapsedMillis = endTime - startTime; Log.d(TAG, "iterations:" + iterations + " iterationTime:" + elapsedMillis + " position:" + mCurrentState.position + " velocity:" + mCurrentState.velocity + " realDeltaTime:" + realDeltaTime + " adjustedDeltaTime:" + adjustedDeltaTime + " isAtRest:" + isAtRest + " wasAtRest:" + mWasAtRest); end debug */ // NB: do these checks outside the loop so all listeners are properly notified of the state // transition boolean notifyActivate = false; if (mWasAtRest) { mWasAtRest = false; notifyActivate = true; } boolean notifyAtRest = false; if (isAtRest) { mWasAtRest = true; notifyAtRest = true; } for (SpringListener listener : mListeners) { // starting to move if (notifyActivate) { listener.onSpringActivate(this); } // updated listener.onSpringUpdate(this); // coming to rest if (notifyAtRest) { listener.onSpringAtRest(this); } } } /** * Check if this spring should be advanced by the system. * The rule is if the spring is * currently at rest and it was at rest in the previous advance, the system can skip this spring * * @return should the system process this spring */ public boolean systemShouldAdvance() { return !isAtRest() || !wasAtRest(); } /** * Check if the spring was at rest in the prior iteration. This is used for ensuring the ending * callbacks are fired as the spring comes to a rest. * * @return true if the spring was at rest in the prior iteration */ public boolean wasAtRest() { return mWasAtRest; } /** * check if the current state is at rest * * @return is the spring at rest */ public boolean isAtRest() { return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold && getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold; } /** * Set the spring to be at rest by making its end value equal to its current value and setting * velocity to 0. */ public Spring setAtRest() { mEndValue = mCurrentState.position; mTempState.position = mCurrentState.position; mCurrentState.velocity = 0; return this; } /** * linear interpolation between the previous and current physics state based on the amount of * timestep remaining after processing the rendering delta time in timestep sized chunks. * * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state */ private void interpolate(double alpha) { mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position * (1 - alpha); mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity * (1 - alpha); } /** listeners **/ /** * add a listener * * @param newListener to add * @return the spring for chaining */ public Spring addListener(SpringListener newListener) { if (newListener == null) { throw new IllegalArgumentException("newListener is required"); } mListeners.addListener(newListener); return this; } /** * remove a listener * * @param listenerToRemove to remove * @return the spring for chaining */ public Spring removeListener(SpringListener listenerToRemove) { if (listenerToRemove == null) { throw new IllegalArgumentException("listenerToRemove is required"); } mListeners.removeListener(listenerToRemove); return this; } /** * remove all of the listeners * * @return the spring for chaining */ public Spring removeAllListeners() { mListeners.clear(); return this; } /** * This method checks to see that the current spring displacement value is equal to the input, * accounting for the spring's rest displacement threshold. * * @param value The value to compare the spring value to * @return Whether the displacement value from the spring is within the bounds of the compare * value, accounting for threshold */ public boolean currentValueIsApproximately(double value) { return Math.abs(getCurrentValue() - value) <= getRestDisplacementThreshold(); } }