Skip to content

Commit 69b58f3

Browse files
generatedunixname1563563004708334meta-codesync[bot]
authored andcommitted
xplat/js/react-native-github/packages/react-native/ReactCommon/react/renderer/animated/drivers/DecayAnimationDriver.cpp (#57104)
Summary: Pull Request resolved: #57104 Reviewed By: javache Differential Revision: D107634657 fbshipit-source-id: 4f5afc57f461a9ab53c1e0c0d6565304bd6b71af
1 parent 2ff3b81 commit 69b58f3

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
#include "AnimationTestsBase.h"
9+
10+
#include <react/renderer/animated/drivers/AnimationDriverUtils.h>
11+
#include <react/renderer/core/ReactRootViewTagGenerator.h>
12+
13+
#include <cmath>
14+
#include <limits>
15+
16+
namespace facebook::react {
17+
18+
class DecayAnimationDriverTest : public AnimationTestsBase {
19+
protected:
20+
// Closed-form analytical value of the decay curve, used to derive expected
21+
// values for assertions instead of hard-coding constants. Mirrors the
22+
// formula implemented by DecayAnimationDriver::getValueAndVelocityForTime,
23+
// which is:
24+
// value = fromValue + velocity / (1 - deceleration)
25+
// * (1 - exp(-(1 - deceleration) * (1000 * time)))
26+
// The driver is called with timeDeltaMs / 1000.0, so 1000 * time collapses
27+
// back to timeMs.
28+
static double expectedDecayValue(
29+
double fromValue,
30+
double velocity,
31+
double deceleration,
32+
double timeMs) {
33+
return fromValue +
34+
velocity / (1 - deceleration) *
35+
(1 - std::exp(-(1 - deceleration) * timeMs));
36+
}
37+
38+
// Create a ValueAnimatedNode with the given initial value and zero offset.
39+
Tag createValueNode(double initialValue) {
40+
auto tag = ++rootTag_;
41+
nodesManager_->createAnimatedNode(
42+
tag,
43+
folly::dynamic::object("type", "value")("value", initialValue)(
44+
"offset", 0));
45+
return tag;
46+
}
47+
48+
void startDecay(
49+
int animationId,
50+
Tag valueNodeTag,
51+
double velocity,
52+
double deceleration,
53+
int iterations = 1) {
54+
nodesManager_->startAnimatingNode(
55+
animationId,
56+
valueNodeTag,
57+
folly::dynamic::object("type", "decay")("velocity", velocity)(
58+
"deceleration", deceleration)("iterations", iterations),
59+
std::nullopt);
60+
}
61+
62+
Tag rootTag_{getNextRootViewTag()};
63+
};
64+
65+
TEST_F(DecayAnimationDriverTest, decayProducesAnalyticalCurveValues) {
66+
// Drives a decay animation and checks the produced values match the
67+
// closed-form decay formula at multiple points along the curve. This
68+
// guards the formula in getValueAndVelocityForTime against regressions
69+
// (e.g. sign flips, swapped operands, exp(+x) vs exp(-x)).
70+
initNodesManager();
71+
const auto valueNodeTag = createValueNode(0);
72+
const auto animationId = 1;
73+
const double velocity = 1.0;
74+
const double deceleration = 0.998;
75+
startDecay(animationId, valueNodeTag, velocity, deceleration);
76+
77+
const double startTimeInTick = 10000;
78+
79+
// First frame: the timeDelta is 0, so the produced value must equal the
80+
// node's starting value (fromValue is captured from the node here).
81+
runAnimationFrame(startTimeInTick);
82+
EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), 0.0, 1e-6);
83+
84+
// Sample the curve at several non-trivial points and compare to the
85+
// analytical expectation. We feed the same wall-clock delta the driver
86+
// sees (frameTimeMs - startFrameTimeMs_, which equals the offset we add
87+
// to startTimeInTick).
88+
for (const double dtMs : {100.0, 500.0, 1000.0}) {
89+
runAnimationFrame(startTimeInTick + dtMs);
90+
const auto expected = expectedDecayValue(0.0, velocity, deceleration, dtMs);
91+
EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), expected, 1e-3);
92+
}
93+
}
94+
95+
TEST_F(DecayAnimationDriverTest, decayUsesNodeStartingValueAsOrigin) {
96+
// The driver must capture the node's current value as fromValue on the
97+
// first update — not assume 0. With a non-zero starting value the entire
98+
// curve is shifted by that offset; verifying this catches a regression
99+
// where fromValue defaults to 0 (a likely off-by-one mistake).
100+
initNodesManager();
101+
const double startingValue = 50.0;
102+
const auto valueNodeTag = createValueNode(startingValue);
103+
const auto animationId = 1;
104+
const double velocity = 2.0;
105+
const double deceleration = 0.99;
106+
startDecay(animationId, valueNodeTag, velocity, deceleration);
107+
108+
const double startTimeInTick = 10000;
109+
runAnimationFrame(startTimeInTick);
110+
EXPECT_NEAR(
111+
nodesManager_->getValue(valueNodeTag).value(), startingValue, 1e-6);
112+
113+
const double dtMs = 200.0;
114+
runAnimationFrame(startTimeInTick + dtMs);
115+
const auto expected =
116+
expectedDecayValue(startingValue, velocity, deceleration, dtMs);
117+
EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), expected, 1e-3);
118+
}
119+
120+
TEST_F(DecayAnimationDriverTest, decayWithNegativeVelocityDecreasesValue) {
121+
// A negative velocity must produce a monotonically decreasing curve that
122+
// approaches the asymptote fromValue + velocity/(1-deceleration) from
123+
// above. This catches sign errors in the value computation.
124+
initNodesManager();
125+
const double startingValue = 100.0;
126+
const auto valueNodeTag = createValueNode(startingValue);
127+
const auto animationId = 1;
128+
const double velocity = -1.0;
129+
const double deceleration = 0.998;
130+
startDecay(animationId, valueNodeTag, velocity, deceleration);
131+
132+
const double t = 10000;
133+
runAnimationFrame(t);
134+
const auto v0 = nodesManager_->getValue(valueNodeTag).value();
135+
EXPECT_NEAR(v0, startingValue, 1e-6);
136+
137+
runAnimationFrame(t + 100);
138+
const auto v1 = nodesManager_->getValue(valueNodeTag).value();
139+
runAnimationFrame(t + 500);
140+
const auto v2 = nodesManager_->getValue(valueNodeTag).value();
141+
142+
// Strictly decreasing in time.
143+
EXPECT_LT(v1, v0);
144+
EXPECT_LT(v2, v1);
145+
146+
// And matches the analytical curve at a sampled point.
147+
const auto expected = expectedDecayValue(startingValue, velocity, 0.998, 500);
148+
EXPECT_NEAR(v2, expected, 1e-3);
149+
}
150+
151+
TEST_F(DecayAnimationDriverTest, decayCompletesWhenValueStabilizes) {
152+
// The driver reports completion when the change between successive frames
153+
// drops below 0.1. Once complete and not running additional iterations,
154+
// the node must hold a final value close to the asymptote
155+
// fromValue + velocity / (1 - deceleration) and must stop updating.
156+
initNodesManager();
157+
const auto valueNodeTag = createValueNode(0);
158+
const auto animationId = 1;
159+
const double velocity = 1.0;
160+
const double deceleration = 0.998;
161+
startDecay(animationId, valueNodeTag, velocity, deceleration);
162+
163+
const double startTimeInTick = 10000;
164+
165+
// Drive enough frames to let the decay settle. The per-frame delta drops
166+
// below 0.1 well before this many frames at 60Hz.
167+
const int totalFrames = 1500;
168+
for (int i = 0; i <= totalFrames; ++i) {
169+
runAnimationFrame(startTimeInTick + i * SingleFrameIntervalMs);
170+
}
171+
172+
const auto asymptote = velocity / (1 - deceleration); // == 500
173+
const auto finalValue = nodesManager_->getValue(valueNodeTag).value();
174+
// The completion threshold is 0.1 per frame; the settled value lands a
175+
// few units short of the asymptote. A generous bound that still rules
176+
// out the unsettled case (where finalValue would be far below it).
177+
EXPECT_LT(std::abs(finalValue - asymptote), 10.0);
178+
// Sanity check the opposite direction — the value must not have overshot
179+
// the asymptote (the formula is strictly increasing toward it for
180+
// positive velocity).
181+
EXPECT_LE(finalValue, asymptote);
182+
183+
// Driving more frames after completion must not change the value: once
184+
// the driver reports completion, subsequent frames are short-circuited
185+
// by AnimationDriver::runAnimationStep and the node value is unchanged.
186+
runAnimationFrame(
187+
startTimeInTick + (totalFrames + 100) * SingleFrameIntervalMs);
188+
EXPECT_NEAR(nodesManager_->getValue(valueNodeTag).value(), finalValue, 1e-6);
189+
}
190+
191+
TEST_F(DecayAnimationDriverTest, decayIterationsResetValueToOrigin) {
192+
// When the animation has additional iterations remaining, each iteration
193+
// after the first must restart from fromValue (the value captured on the
194+
// very first update), not from wherever the previous iteration ended.
195+
// This exercises the `else` branch in update() that resets the node via
196+
// setRawValue when restarting a subsequent iteration. We use iterations
197+
// = -1 (infinite) so the driver is guaranteed to restart instead of
198+
// terminating.
199+
initNodesManager();
200+
const double startingValue = 0.0;
201+
const auto valueNodeTag = createValueNode(startingValue);
202+
const auto animationId = 1;
203+
const double velocity = 1.0;
204+
const double deceleration = 0.998;
205+
startDecay(
206+
animationId, valueNodeTag, velocity, deceleration, /*iterations=*/-1);
207+
208+
const double startTimeInTick = 10000;
209+
210+
// First frame anchors fromValue at startingValue and emits it.
211+
runAnimationFrame(startTimeInTick);
212+
ASSERT_NEAR(
213+
nodesManager_->getValue(valueNodeTag).value(), startingValue, 1e-6);
214+
215+
// Walk frame-by-frame until we observe a value drop from one frame to
216+
// the next. Decay with positive velocity is strictly monotone, so the
217+
// only way the observed value can decrease is if the driver completed
218+
// an iteration and reset the node back to fromValue at the start of
219+
// the next one. With velocity=1, deceleration=0.998, fromValue=0 the
220+
// asymptote is 500, so any reset produces a multi-hundred-unit drop —
221+
// not a fragile near-equality.
222+
double previousValue = nodesManager_->getValue(valueNodeTag).value();
223+
bool sawReset = false;
224+
double resetValue = std::numeric_limits<double>::quiet_NaN();
225+
// Bound the loop generously — the completion threshold (per-frame delta
226+
// < 0.1) is reached in a couple hundred frames for these parameters.
227+
constexpr int kMaxFrames = 600;
228+
for (int i = 1; i <= kMaxFrames; ++i) {
229+
runAnimationFrame(startTimeInTick + i * SingleFrameIntervalMs);
230+
const auto current = nodesManager_->getValue(valueNodeTag).value();
231+
if (current < previousValue) {
232+
sawReset = true;
233+
resetValue = current;
234+
break;
235+
}
236+
previousValue = current;
237+
}
238+
239+
ASSERT_TRUE(sawReset);
240+
// After the reset, the very next frame of the new iteration emits the
241+
// starting value (timeDelta=0 ⇒ value=fromValue).
242+
EXPECT_NEAR(resetValue, startingValue, 1e-6);
243+
}
244+
245+
} // namespace facebook::react

0 commit comments

Comments
 (0)