UNPKG

animated

Version:

Declarative Animations Library for React and React Native

225 lines (194 loc) 7.58 kB
/** * Copyright (c) 2015-present, 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. * * @flow */ 'use strict'; var Animation = require('./Animation'); var AnimatedValue = require('./AnimatedValue'); var RequestAnimationFrame = require('./injectable/RequestAnimationFrame'); var CancelAnimationFrame = require('./injectable/CancelAnimationFrame'); var invariant = require('invariant'); var SpringConfig = require('./SpringConfig'); import type { AnimationConfig, EndCallback } from './Animation'; type SpringAnimationConfigSingle = AnimationConfig & { toValue: number | AnimatedValue; overshootClamping?: bool; restDisplacementThreshold?: number; restSpeedThreshold?: number; velocity?: number; bounciness?: number; speed?: number; tension?: number; friction?: number; }; function withDefault<T>(value: ?T, defaultValue: T): T { if (value === undefined || value === null) { return defaultValue; } return value; } class SpringAnimation extends Animation { _overshootClamping: bool; _restDisplacementThreshold: number; _restSpeedThreshold: number; _initialVelocity: ?number; _lastVelocity: number; _startPosition: number; _lastPosition: number; _fromValue: number; _toValue: any; _tension: number; _friction: number; _lastTime: number; _onUpdate: (value: number) => void; _animationFrame: any; constructor( config: SpringAnimationConfigSingle, ) { super(); this._overshootClamping = withDefault(config.overshootClamping, false); this._restDisplacementThreshold = withDefault(config.restDisplacementThreshold, 0.001); this._restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001); this._initialVelocity = config.velocity; this._lastVelocity = withDefault(config.velocity, 0); this._toValue = config.toValue; this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; var springConfig; if (config.bounciness !== undefined || config.speed !== undefined) { invariant( config.tension === undefined && config.friction === undefined, 'You can only define bounciness/speed or tension/friction but not both', ); springConfig = SpringConfig.fromBouncinessAndSpeed( withDefault(config.bounciness, 8), withDefault(config.speed, 12), ); } else { springConfig = SpringConfig.fromOrigamiTensionAndFriction( withDefault(config.tension, 40), withDefault(config.friction, 7), ); } this._tension = springConfig.tension; this._friction = springConfig.friction; } start( fromValue: number, onUpdate: (value: number) => void, onEnd: ?EndCallback, previousAnimation: ?Animation, ): void { this.__active = true; this._startPosition = fromValue; this._lastPosition = this._startPosition; this._onUpdate = onUpdate; this.__onEnd = onEnd; this._lastTime = Date.now(); if (previousAnimation instanceof SpringAnimation) { var internalState = previousAnimation.getInternalState(); this._lastPosition = internalState.lastPosition; this._lastVelocity = internalState.lastVelocity; this._lastTime = internalState.lastTime; } if (this._initialVelocity !== undefined && this._initialVelocity !== null) { this._lastVelocity = this._initialVelocity; } this.onUpdate(); } getInternalState(): Object { return { lastPosition: this._lastPosition, lastVelocity: this._lastVelocity, lastTime: this._lastTime, }; } onUpdate(): void { var position = this._lastPosition; var velocity = this._lastVelocity; var tempPosition = this._lastPosition; var tempVelocity = this._lastVelocity; // If for some reason we lost a lot of frames (e.g. process large payload or // stopped in the debugger), we only advance by 4 frames worth of // computation and will continue on the next frame. It's better to have it // running at faster speed than jumping to the end. var MAX_STEPS = 64; var now = Date.now(); if (now > this._lastTime + MAX_STEPS) { now = this._lastTime + MAX_STEPS; } // We are using a fixed time step and a maximum number of iterations. // The following post provides a lot of thoughts into how to build this // loop: http://gafferongames.com/game-physics/fix-your-timestep/ var TIMESTEP_MSEC = 1; var numSteps = Math.floor((now - this._lastTime) / TIMESTEP_MSEC); for (var i = 0; i < numSteps; ++i) { // Velocity is based on seconds instead of milliseconds var step = TIMESTEP_MSEC / 1000; // This is using RK4. A good blog post to understand how it works: // http://gafferongames.com/game-physics/integration-basics/ var aVelocity = velocity; var aAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity; var tempPosition = position + aVelocity * step / 2; var tempVelocity = velocity + aAcceleration * step / 2; var bVelocity = tempVelocity; var bAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity; tempPosition = position + bVelocity * step / 2; tempVelocity = velocity + bAcceleration * step / 2; var cVelocity = tempVelocity; var cAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity; tempPosition = position + cVelocity * step / 2; tempVelocity = velocity + cAcceleration * step / 2; var dVelocity = tempVelocity; var dAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity; tempPosition = position + cVelocity * step / 2; tempVelocity = velocity + cAcceleration * step / 2; var dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6; var dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6; position += dxdt * step; velocity += dvdt * step; } this._lastTime = now; this._lastPosition = position; this._lastVelocity = velocity; this._onUpdate(position); if (!this.__active) { // a listener might have stopped us in _onUpdate return; } // Conditions for stopping the spring animation var isOvershooting = false; if (this._overshootClamping && this._tension !== 0) { if (this._startPosition < this._toValue) { isOvershooting = position > this._toValue; } else { isOvershooting = position < this._toValue; } } var isVelocity = Math.abs(velocity) <= this._restSpeedThreshold; var isDisplacement = true; if (this._tension !== 0) { isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold; } if (isOvershooting || (isVelocity && isDisplacement)) { if (this._tension !== 0) { // Ensure that we end up with a round value this._onUpdate(this._toValue); } this.__debouncedOnEnd({finished: true}); return; } this._animationFrame = RequestAnimationFrame.current(this.onUpdate.bind(this)); } stop(): void { this.__active = false; CancelAnimationFrame.current(this._animationFrame); this.__debouncedOnEnd({finished: false}); } } module.exports = SpringAnimation;