/** * 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. */ #import "RCTSpringAnimation.h" #import #import #import #import "RCTValueAnimatedNode.h" @interface RCTSpringAnimation () @property (nonatomic, strong) NSNumber *animationId; @property (nonatomic, strong) RCTValueAnimatedNode *valueNode; @property (nonatomic, assign) BOOL animationHasBegun; @property (nonatomic, assign) BOOL animationHasFinished; @end @implementation RCTSpringAnimation { CGFloat _toValue; CGFloat _fromValue; BOOL _overshootClamping; CGFloat _restDisplacementThreshold; CGFloat _restSpeedThreshold; CGFloat _tension; CGFloat _friction; CGFloat _initialVelocity; NSTimeInterval _animationStartTime; NSTimeInterval _animationCurrentTime; RCTResponseSenderBlock _callback; CGFloat _lastPosition; CGFloat _lastVelocity; NSInteger _iterations; NSInteger _currentLoop; } - (instancetype)initWithId:(NSNumber *)animationId config:(NSDictionary *)config forNode:(RCTValueAnimatedNode *)valueNode callBack:(nullable RCTResponseSenderBlock)callback { if ((self = [super init])) { NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; _animationId = animationId; _toValue = [RCTConvert CGFloat:config[@"toValue"]]; _fromValue = valueNode.value; _valueNode = valueNode; _overshootClamping = [RCTConvert BOOL:config[@"overshootClamping"]]; _restDisplacementThreshold = [RCTConvert CGFloat:config[@"restDisplacementThreshold"]]; _restSpeedThreshold = [RCTConvert CGFloat:config[@"restSpeedThreshold"]]; _tension = [RCTConvert CGFloat:config[@"tension"]]; _friction = [RCTConvert CGFloat:config[@"friction"]]; _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; _callback = [callback copy]; _lastPosition = _fromValue; _lastVelocity = _initialVelocity; _animationHasFinished = iterations.integerValue == 0; _iterations = iterations.integerValue; _currentLoop = 1; } return self; } RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)startAnimation { _animationStartTime = _animationCurrentTime = -1; _animationHasBegun = YES; } - (void)stopAnimation { _valueNode = nil; if (_callback) { _callback(@[@{ @"finished": @(_animationHasFinished) }]); } } - (void)stepAnimationWithTime:(NSTimeInterval)currentTime { if (!_animationHasBegun || _animationHasFinished) { // Animation has not begun or animation has already finished. return; } if (_animationStartTime == -1) { _animationStartTime = _animationCurrentTime = currentTime; } // 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/ CGFloat TIMESTEP_MSEC = 1; // Velocity is based on seconds instead of milliseconds CGFloat step = TIMESTEP_MSEC / 1000; NSInteger numSteps = floorf((currentTime - _animationCurrentTime) / step); _animationCurrentTime = currentTime; if (numSteps == 0) { return; } CGFloat position = _lastPosition; CGFloat velocity = _lastVelocity; CGFloat tempPosition = _lastPosition; CGFloat tempVelocity = _lastVelocity; for (NSInteger i = 0; i < numSteps; ++i) { // This is using RK4. A good blog post to understand how it works: // http://gafferongames.com/game-physics/integration-basics/ CGFloat aVelocity = velocity; CGFloat aAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity; tempPosition = position + aVelocity * step / 2; tempVelocity = velocity + aAcceleration * step / 2; CGFloat bVelocity = tempVelocity; CGFloat bAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity; tempPosition = position + bVelocity * step / 2; tempVelocity = velocity + bAcceleration * step / 2; CGFloat cVelocity = tempVelocity; CGFloat cAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity; tempPosition = position + cVelocity * step / 2; tempVelocity = velocity + cAcceleration * step / 2; CGFloat dVelocity = tempVelocity; CGFloat dAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity; tempPosition = position + cVelocity * step / 2; tempVelocity = velocity + cAcceleration * step / 2; CGFloat dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6; CGFloat dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6; position += dxdt * step; velocity += dvdt * step; } _lastPosition = position; _lastVelocity = velocity; [self onUpdate:position]; if (_animationHasFinished) { return; } // Conditions for stopping the spring animation BOOL isOvershooting = NO; if (_overshootClamping && _tension != 0) { if (_fromValue < _toValue) { isOvershooting = position > _toValue; } else { isOvershooting = position < _toValue; } } BOOL isVelocity = ABS(velocity) <= _restSpeedThreshold; BOOL isDisplacement = YES; if (_tension != 0) { isDisplacement = ABS(_toValue - position) <= _restDisplacementThreshold; } if (isOvershooting || (isVelocity && isDisplacement)) { if (_tension != 0) { // Ensure that we end up with a round value if (_animationHasFinished) { return; } [self onUpdate:_toValue]; } if (_iterations == -1 || _currentLoop < _iterations) { _lastPosition = _fromValue; _lastVelocity = _initialVelocity; _currentLoop++; [self onUpdate:_fromValue]; } else { _animationHasFinished = YES; } } } - (void)onUpdate:(CGFloat)outputValue { _valueNode.value = outputValue; [_valueNode setNeedsUpdate]; } @end