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