1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import <ABI47_0_0React/ABI47_0_0RCTSpringAnimation.h>
9
10#import <UIKit/UIKit.h>
11
12#import <ABI47_0_0React/ABI47_0_0RCTConvert.h>
13#import <ABI47_0_0React/ABI47_0_0RCTDefines.h>
14
15#import <ABI47_0_0React/ABI47_0_0RCTAnimationUtils.h>
16#import <ABI47_0_0React/ABI47_0_0RCTValueAnimatedNode.h>
17
18@interface ABI47_0_0RCTSpringAnimation ()
19
20@property (nonatomic, strong) NSNumber *animationId;
21@property (nonatomic, strong) ABI47_0_0RCTValueAnimatedNode *valueNode;
22@property (nonatomic, assign) BOOL animationHasBegun;
23@property (nonatomic, assign) BOOL animationHasFinished;
24
25@end
26
27const NSTimeInterval ABI47_0_0MAX_DELTA_TIME = 0.064;
28
29@implementation ABI47_0_0RCTSpringAnimation
30{
31  CGFloat _toValue;
32  CGFloat _fromValue;
33  BOOL _overshootClamping;
34  CGFloat _restDisplacementThreshold;
35  CGFloat _restSpeedThreshold;
36  CGFloat _stiffness;
37  CGFloat _damping;
38  CGFloat _mass;
39  CGFloat _initialVelocity;
40  NSTimeInterval _animationStartTime;
41  NSTimeInterval _animationCurrentTime;
42  ABI47_0_0RCTResponseSenderBlock _callback;
43
44  CGFloat _lastPosition;
45  CGFloat _lastVelocity;
46
47  NSInteger _iterations;
48  NSInteger _currentLoop;
49
50  NSTimeInterval _t; // Current time (startTime + dt)
51}
52
53- (instancetype)initWithId:(NSNumber *)animationId
54                    config:(NSDictionary *)config
55                   forNode:(ABI47_0_0RCTValueAnimatedNode *)valueNode
56                  callBack:(nullable ABI47_0_0RCTResponseSenderBlock)callback
57{
58  if ((self = [super init])) {
59    _animationId = animationId;
60    _lastPosition = valueNode.value;
61    _valueNode = valueNode;
62    _lastVelocity = [ABI47_0_0RCTConvert CGFloat:config[@"initialVelocity"]];
63    _callback = [callback copy];
64    [self resetAnimationConfig:config];
65  }
66  return self;
67}
68
69- (void)resetAnimationConfig:(NSDictionary *)config
70{
71  NSNumber *iterations = [ABI47_0_0RCTConvert NSNumber:config[@"iterations"]] ?: @1;
72  _toValue = [ABI47_0_0RCTConvert CGFloat:config[@"toValue"]];
73  _overshootClamping = [ABI47_0_0RCTConvert BOOL:config[@"overshootClamping"]];
74  _restDisplacementThreshold = [ABI47_0_0RCTConvert CGFloat:config[@"restDisplacementThreshold"]];
75  _restSpeedThreshold = [ABI47_0_0RCTConvert CGFloat:config[@"restSpeedThreshold"]];
76  _stiffness = [ABI47_0_0RCTConvert CGFloat:config[@"stiffness"]];
77  _damping = [ABI47_0_0RCTConvert CGFloat:config[@"damping"]];
78  _mass = [ABI47_0_0RCTConvert CGFloat:config[@"mass"]];
79  _initialVelocity = _lastVelocity;
80  _fromValue = _lastPosition;
81  _fromValue = _lastPosition;
82  _lastVelocity = _initialVelocity;
83  _animationHasFinished = iterations.integerValue == 0;
84  _iterations = iterations.integerValue;
85  _currentLoop = 1;
86  _animationStartTime = _animationCurrentTime = -1;
87  _animationHasBegun = YES;
88}
89
90ABI47_0_0RCT_NOT_IMPLEMENTED(- (instancetype)init)
91
92- (void)startAnimation
93{
94  _animationStartTime = _animationCurrentTime = -1;
95  _animationHasBegun = YES;
96}
97
98- (void)stopAnimation
99{
100  _valueNode = nil;
101  if (_callback) {
102    _callback(@[@{
103      @"finished": @(_animationHasFinished)
104    }]);
105  }
106}
107
108- (void)stepAnimationWithTime:(NSTimeInterval)currentTime
109{
110  if (!_animationHasBegun || _animationHasFinished) {
111    // Animation has not begun or animation has already finished.
112    return;
113  }
114
115  // calculate delta time
116  if(_animationStartTime == -1) {
117    _t = 0.0;
118    _animationStartTime = currentTime;
119  } else {
120    // Handle frame drops, and only advance dt by a max of ABI47_0_0MAX_DELTA_TIME
121    NSTimeInterval deltaTime = MIN(ABI47_0_0MAX_DELTA_TIME, currentTime - _animationCurrentTime);
122    _t = _t + deltaTime / ABI47_0_0RCTAnimationDragCoefficient();
123  }
124
125  // store the timestamp
126  _animationCurrentTime = currentTime;
127
128  CGFloat c = _damping;
129  CGFloat m = _mass;
130  CGFloat k = _stiffness;
131  CGFloat v0 = -_initialVelocity;
132
133  CGFloat zeta = c / (2 * sqrtf(k * m));
134  CGFloat omega0 = sqrtf(k / m);
135  CGFloat omega1 = omega0 * sqrtf(1.0 - (zeta * zeta));
136  CGFloat x0 = _toValue - _fromValue;
137
138  CGFloat position;
139  CGFloat velocity;
140  if (zeta < 1) {
141    // Under damped
142    CGFloat envelope = expf(-zeta * omega0 * _t);
143    position =
144      _toValue -
145      envelope *
146      ((v0 + zeta * omega0 * x0) / omega1 * sinf(omega1 * _t) +
147        x0 * cosf(omega1 * _t));
148    // This looks crazy -- it's actually just the derivative of the
149    // oscillation function
150    velocity =
151      zeta *
152        omega0 *
153        envelope *
154        (sinf(omega1 * _t) * (v0 + zeta * omega0 * x0) / omega1 +
155          x0 * cosf(omega1 * _t)) -
156      envelope *
157        (cosf(omega1 * _t) * (v0 + zeta * omega0 * x0) -
158          omega1 * x0 * sinf(omega1 * _t));
159  } else {
160    CGFloat envelope = expf(-omega0 * _t);
161    position = _toValue - envelope * (x0 + (v0 + omega0 * x0) * _t);
162    velocity =
163      envelope * (v0 * (_t * omega0 - 1) + _t * x0 * (omega0 * omega0));
164  }
165
166  _lastPosition = position;
167  _lastVelocity = velocity;
168
169  [self onUpdate:position];
170
171  // Conditions for stopping the spring animation
172  BOOL isOvershooting = NO;
173  if (_overshootClamping && _stiffness != 0) {
174    if (_fromValue < _toValue) {
175      isOvershooting = position > _toValue;
176    } else {
177      isOvershooting = position < _toValue;
178    }
179  }
180  BOOL isVelocity = ABS(velocity) <= _restSpeedThreshold;
181  BOOL isDisplacement = YES;
182  if (_stiffness != 0) {
183    isDisplacement = ABS(_toValue - position) <= _restDisplacementThreshold;
184  }
185
186  if (isOvershooting || (isVelocity && isDisplacement)) {
187    if (_stiffness != 0) {
188      // Ensure that we end up with a round value
189      if (_animationHasFinished) {
190        return;
191      }
192      [self onUpdate:_toValue];
193    }
194
195    if (_iterations == -1 || _currentLoop < _iterations) {
196      _lastPosition = _fromValue;
197      _lastVelocity = _initialVelocity;
198      // Set _animationStartTime to -1 to reset instance variables on the next animation step.
199      _animationStartTime = -1;
200      _currentLoop++;
201      [self onUpdate:_fromValue];
202    } else {
203      _animationHasFinished = YES;
204    }
205  }
206}
207
208- (void)onUpdate:(CGFloat)outputValue
209{
210  _valueNode.value = outputValue;
211  [_valueNode setNeedsUpdate];
212}
213
214@end
215