blob: 07599dfeaf2340f1c6c9f6eded4a561958422e26 [file] [log] [blame]
William Liu8e6e6ba2023-12-08 16:21:071// Copyright 2023 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "content/browser/navigation_transitions/physics_model.h"
6
Aldo Culquicondor195c75d2025-01-09 22:22:337#include <algorithm>
punithnayakb367adb2024-02-20 22:27:418#include <numbers>
William Liu8e6e6ba2023-12-08 16:21:079#include <vector>
10
11#include "base/logging.h"
12#include "base/notreached.h"
William Liu8e6e6ba2023-12-08 16:21:0713#include "base/numerics/ranges.h"
14#include "base/time/time.h"
15
16// TODO(liuwilliam): The velocity and positions should have the same direction.
17//
18// Notes:
Aldo Culquicondor195c75d2025-01-09 22:22:3319// - Directions: for offsets/positions and the velocity of the cancel spring,
20// the right edge direction is "+" and the left is "-"; for commit pending and
21// invoke spring velocities, the right edge direction for is "-" and the left
William Liu8e6e6ba2023-12-08 16:21:0722// is "+".
23// - The physics model internally operates in the normalized viewport space
24// while takes/returns physical pixel values as input/output. The spacial
25// variables are suffixed with `_viewport` or `_physical` to avoid confusion.
26
27namespace content {
28
29namespace {
30
31// The tolerance value for which two floats are consider equal.
32constexpr float kFloatTolerance = 0.001f;
33
34// Used to replace NaN and Inf.
35constexpr float kInvalidVelocity = 999.f;
36
37// Springs.
38//
39// Determines when the spring is stabilized (the damped amplitude no longer
40// changes significantly). Larger the value, the longer the spring takes to
41// stabilize, but the spring amplitude is damped more gently.
42constexpr int kSpringResponse = 708;
43
44// How much the spring overshoots. Smaller the value, more bouncy the spring.
45constexpr float kSpringDampingRatio = 0.81f;
46
47// The size of the spring location history.
48constexpr int kSpringHistorySize = 10;
49
50// A spring is considered at rest if it has used at least
51// `kSpringAtRestThreshold`*`kSpringHistorySize` amount of energy.
52constexpr int kSpringAtRestThreshold = 10;
53
54// Physics model.
55//
William Liu8e6e6ba2023-12-08 16:21:0756// The size of the touch points store in `PhysicsModel`. Used to interpolate
57// the finger's terminal velocity when the model switches from the finger drag
58// curve driven to spring driven.
59constexpr int kPhysicsModelHistorySize = 10;
60
William Liub26568da2024-02-21 20:46:1461bool IsValidVelocity(float velocity) {
62 return !base::IsApproximatelyEqual(velocity, kInvalidVelocity,
63 kFloatTolerance);
William Liu8e6e6ba2023-12-08 16:21:0764}
65
66// Solves `positions`=`slope`*`timestamps`+ displacement(not calculated).
67//
Alison Gale770f3fc2024-04-27 00:39:5868// TODO(crbug.com/40945408): The native least square might not give us
William Liu8e6e6ba2023-12-08 16:21:0769// the desired velocity.
70void SolveLeastSquare(const std::vector<float>& timestamps,
71 const std::vector<float>& positions,
72 float* slope) {
73 CHECK_EQ(timestamps.size(), positions.size());
74
75 const size_t num_pts = timestamps.size();
76 if (num_pts <= 1) {
77 LOG(ERROR) << "Interpolating velocity with " << num_pts << " points";
78 if (slope) {
79 *slope = kInvalidVelocity;
80 }
81 return;
82 }
83
84 float sum_timestamps = 0;
85 float sum_positions = 0;
86 float sum_times_positions = 0;
87 float sum_timestamps_sq = 0;
88
89 for (size_t i = 0; i < num_pts; ++i) {
90 float t = timestamps[i];
91 float p = positions[i];
92 sum_timestamps += t;
93 sum_positions += p;
94 sum_times_positions += t * p;
95 sum_timestamps_sq += t * t;
96 }
97
98 if (slope) {
99 float denominator =
100 (sum_timestamps_sq - sum_timestamps * sum_timestamps / num_pts);
101 if (base::IsApproximatelyEqual(denominator, 0.f, kFloatTolerance)) {
102 *slope = kInvalidVelocity;
103 } else {
104 *slope =
105 (sum_times_positions - sum_timestamps * sum_positions / num_pts) /
106 denominator;
107 }
108 }
109}
110
111} // namespace
112
113class Spring {
114 public:
115 struct Position {
116 // Calculated offset of the spring's position w.r.t. its equilibrium.
117 float equilibrium_offset_viewport;
118
119 // The amount of time delta since the spring is released (i.e., the start of
120 // the animation).
121 base::TimeDelta timestamp;
122
123 // If the spring is at rest then it won't bounce anymore. A spring is at
124 // rest if it has lost enough energe, or it is <= 1 pixel away from its
125 // equilibrium.
126 bool at_rest;
127 };
128
129 Spring(int frequency_response,
130 float damping_ratio,
131 float device_scaling_factor)
132 : frequency_response_(frequency_response),
133 damping_ratio_(damping_ratio),
134 device_scale_factor_(device_scaling_factor) {
135 float stiffness =
punithnayakb367adb2024-02-20 22:27:41136 std::pow(2 * std::numbers::pi_v<float> / frequency_response_, 2) *
137 mass_;
William Liu8e6e6ba2023-12-08 16:21:07138 undamped_natural_frequency_ = std::sqrt(stiffness / mass_);
139 damped_natural_frequency_ =
140 undamped_natural_frequency_ *
141 std::sqrt(std::abs(1 - std::pow(damping_ratio_, 2)));
142 // `damped_natural_frequency_` will be used as a denominator. It shouldn't
143 // be zero.
144 CHECK(!base::IsApproximatelyEqual(damped_natural_frequency_, 0.f,
145 kFloatTolerance));
146 CHECK(!base::IsApproximatelyEqual(device_scale_factor_, 0.f,
147 kFloatTolerance));
148 }
149 Spring(const Spring&) = delete;
150 Spring& operator=(const Spring&) = delete;
151 ~Spring() = default;
152
153 Position GetPosition(float start_offset, base::TimeDelta time) {
154 // The general solution to a damped oscillator.
155 const float a = undamped_natural_frequency_ * damping_ratio_;
156 const float c =
157 (initial_velocity_ + a * start_offset) / damped_natural_frequency_;
158 const float ms = time.InMillisecondsF();
159 const float offset =
160 std::exp(-a * ms) *
161 (c * std::sin(damped_natural_frequency_ * ms) +
162 start_offset * std::cos(damped_natural_frequency_ * ms));
163
164 spring_position_history_.push_back({.equilibrium_offset_viewport = offset,
165 .timestamp = time,
166 .at_rest = false});
167
168 if (spring_position_history_.size() > kSpringHistorySize) {
169 spring_position_history_.pop_front();
170 float energy = 0;
171 for (const auto& p : spring_position_history_) {
172 // Energy is proportional to the square of the amplitude.
173 energy += p.equilibrium_offset_viewport * p.equilibrium_offset_viewport;
174 }
175 // If the spring has used `kSpringAtRestThreshold * kSpringHistorySize`
176 // amount energy in the last `kSpringHistorySize` locations, consider it
177 // is at rest.
178 spring_position_history_.back().at_rest |=
179 energy < kSpringAtRestThreshold * kSpringHistorySize;
180 }
181
182 // Less than 1pixel from its equilibrium.
183 spring_position_history_.back().at_rest |=
184 offset <= 1.f / device_scale_factor_;
185
186 return spring_position_history_.back();
187 }
188
189 float ComputeVelocity() {
190 std::vector<float> timestamps;
191 std::vector<float> positions;
192 timestamps.reserve(spring_position_history_.size());
193 positions.reserve(spring_position_history_.size());
194
195 for (const auto& p : spring_position_history_) {
196 timestamps.push_back(p.timestamp.InMillisecondsF());
197 positions.push_back(p.equilibrium_offset_viewport);
198 }
199
200 float velocity = 0;
201 SolveLeastSquare(timestamps, positions, &velocity);
202
203 return velocity;
204 }
205
206 float initial_velocity() const { return initial_velocity_; }
207 void set_initial_velocity(float velocity) { initial_velocity_ = velocity; }
208
209 private:
210 // Intrinsic properties of the spring.
211 const int frequency_response_;
212 const float damping_ratio_;
213 const float device_scale_factor_;
214 float undamped_natural_frequency_;
215 float damped_natural_frequency_;
216 const float mass_ = 1.f;
217
218 // The initial velocity might not be zero: to enture the smooth animation
219 // hand-off from spring A to spring B, we might set B's initial velocity to
220 // A's terminal velocity.
221 float initial_velocity_ = 0.f;
222
223 // The last few positions of the spring. Used to interpolate the velocity. It
224 // has a max size of `kSpringHistorySize`.
225 std::deque<Position> spring_position_history_;
226};
227
228PhysicsModel::PhysicsModel(int screen_width_physical, float device_scale_factor)
229 : viewport_width_(screen_width_physical / device_scale_factor),
230 device_scale_factor_(device_scale_factor) {
231 spring_cancel_ = std::make_unique<Spring>(
232 /*frequency_response=*/200,
233 /*damping_ratio=*/0.9,
234 /*device_scaling_factor=*/device_scale_factor_);
235 spring_commit_pending_ = std::make_unique<Spring>(
236 /*frequency_response=*/kSpringResponse,
237 /*damping_ratio=*/kSpringDampingRatio,
238 /*device_scaling_factor=*/device_scale_factor_);
239 spring_invoke_ = std::make_unique<Spring>(
240 /*frequency_response=*/200,
241 /*damping_ratio=*/0.95, /*device_scaling_factor=*/device_scale_factor_);
242}
243
244PhysicsModel::~PhysicsModel() = default;
245
246PhysicsModel::Result PhysicsModel::OnAnimate(
247 base::TimeTicks request_animation_frame) {
248 // `commit_pending_acceleration_start_` needs to be recorded before we switch
249 // to the next driver.
250 RecordCommitPendingAccelerationStartIfNeeded(request_animation_frame);
251
252 AdvanceToNextAnimationDriver(request_animation_frame);
253
254 base::TimeDelta raf_since_start =
255 CalculateRequestAnimationFrameSinceStart(request_animation_frame);
256
257 // Ask the animation driver for the offset of the next frame.
258 Spring::Position spring_position;
259 switch (animation_driver_) {
260 case Driver::kSpringCommitPending: {
261 spring_position = spring_commit_pending_->GetPosition(
William Liue4393bd2024-03-15 20:16:36262 viewport_width_ * kTargetCommitPendingRatio -
William Liu8e6e6ba2023-12-08 16:21:07263 animation_start_offset_viewport_,
264 raf_since_start);
William Liu86212af2024-02-28 17:01:28265 // Prevent overshoot the right edge.
William Liu8e6e6ba2023-12-08 16:21:07266 foreground_offset_viewport_ = std::min(
William Liue4393bd2024-03-15 20:16:36267 viewport_width_, viewport_width_ * kTargetCommitPendingRatio -
William Liu8e6e6ba2023-12-08 16:21:07268 spring_position.equilibrium_offset_viewport);
William Liu86212af2024-02-28 17:01:28269 // https://crbug.com/326850774: The commit-pending spring can also
270 // overshoot the left edge.
271 foreground_offset_viewport_ = std::max(0.f, foreground_offset_viewport_);
William Liu8e6e6ba2023-12-08 16:21:07272 break;
273 }
274 case Driver::kSpringInvoke: {
275 spring_position = spring_invoke_->GetPosition(
276 viewport_width_ - animation_start_offset_viewport_, raf_since_start);
William Liu86212af2024-02-28 17:01:28277 // Prevent overshoot the right edge.
William Liu8e6e6ba2023-12-08 16:21:07278 foreground_offset_viewport_ = std::min(
279 viewport_width_,
280 viewport_width_ - spring_position.equilibrium_offset_viewport);
William Liu86212af2024-02-28 17:01:28281 // https://crbug.com/326850774: The invoke spring can also overshoot the
282 // left edge.
283 foreground_offset_viewport_ = std::max(0.f, foreground_offset_viewport_);
William Liu8e6e6ba2023-12-08 16:21:07284 break;
285 }
286 case Driver::kSpringCancel: {
287 spring_position = spring_cancel_->GetPosition(
288 animation_start_offset_viewport_, raf_since_start);
William Liu86212af2024-02-28 17:01:28289 // Prevent overshoot the left edge.
William Liu8e6e6ba2023-12-08 16:21:07290 foreground_offset_viewport_ =
291 std::max(spring_position.equilibrium_offset_viewport, 0.f);
William Liu86212af2024-02-28 17:01:28292 // https://crbug.com/326850774: The cancel spring can also overshoot the
293 // right edge.
294 foreground_offset_viewport_ =
295 std::min(viewport_width_, foreground_offset_viewport_);
William Liu8e6e6ba2023-12-08 16:21:07296 break;
297 }
298 case Driver::kDragCurve: {
Peter Boström01ab59a2024-08-15 02:39:49299 NOTREACHED();
William Liu8e6e6ba2023-12-08 16:21:07300 }
301 }
302
303 foreground_has_reached_target_commit_pending_ |=
William Liue4393bd2024-03-15 20:16:36304 foreground_offset_viewport_ >=
305 kTargetCommitPendingRatio * viewport_width_;
William Liu8e6e6ba2023-12-08 16:21:07306
307 last_request_animation_frame_ = request_animation_frame;
308
309 return Result{
310 .foreground_offset_physical =
Aldo Culquicondora5b58642025-08-11 17:39:14311 std::round(foreground_offset_viewport_ * device_scale_factor_),
William Liu8e6e6ba2023-12-08 16:21:07312 .background_offset_physical =
Aldo Culquicondora5b58642025-08-11 17:39:14313 std::round(ForegroundToBackGroundOffset(foreground_offset_viewport_) *
314 device_scale_factor_),
William Liu8e6e6ba2023-12-08 16:21:07315 // Done only if we have finished playing the terminal animations.
316 .done = (spring_position.at_rest &&
317 (animation_driver_ == Driver::kSpringInvoke ||
318 animation_driver_ == Driver::kSpringCancel)),
319 };
320}
321
322// Note: we don't call `StartAnimating()` with the drag curve because
323// `timestamp` for the drag curve is not from the wallclock. The non-wallclock
324// time shouldn't be stored as `animation_start_time_`.
325PhysicsModel::Result PhysicsModel::OnGestureProgressed(
326 float movement_physical,
327 base::TimeTicks timestamp) {
328 CHECK_EQ(animation_driver_, Driver::kDragCurve);
329 const float movement_viewport = movement_physical / device_scale_factor_;
330
331 foreground_offset_viewport_ =
332 std::max(0.f, FingerDragCurve(movement_viewport));
333 touch_points_history_.push_back(TouchEvent{
334 .position_viewport = foreground_offset_viewport_,
335 .timestamp = timestamp,
336 });
337 if (touch_points_history_.size() > kPhysicsModelHistorySize) {
338 touch_points_history_.pop_front();
339 }
340 return Result{
341 .foreground_offset_physical =
342 foreground_offset_viewport_ * device_scale_factor_,
343 .background_offset_physical =
344 ForegroundToBackGroundOffset(foreground_offset_viewport_) *
345 device_scale_factor_,
346 .done = false,
347 };
348}
349
William Liuc6cc30ec2024-03-14 16:54:16350void PhysicsModel::SwitchSpringForReason(SwitchSpringReason reason) {
351 switch (reason) {
352 case kGestureCancelled:
353 case kGestureInvoked: {
William Liuc6cc30ec2024-03-14 16:54:16354 // The navigation just started by the caller in the same atomic callstack
355 // if the user decides to start the navigation. The navigation hasn't
356 // committed or been cancelled yet.
357 CHECK_EQ(navigation_state_, NavigationState::kNotStarted);
William Liu8e6e6ba2023-12-08 16:21:07358
William Liuc6cc30ec2024-03-14 16:54:16359 if (reason == kGestureCancelled) {
William Liu7b0751a72024-10-23 16:08:52360 navigation_state_ = NavigationState::kCancelled;
William Liuc6cc30ec2024-03-14 16:54:16361 }
362 if (reason == kGestureInvoked) {
363 navigation_state_ = NavigationState::kStarted;
364 }
365 // Next `OnAnimate()` call will switch to `kSpringCancel` or
366 // `kSpringCommitPending`.
367 break;
368 }
369 case kBeforeUnloadDispatched: {
William Liu7b0751a72024-10-23 16:08:52370 navigation_state_ = NavigationState::kBeforeUnloadDispatched;
371 // On next `OnAnimate()`, `animation_driver_` will switch to
372 // `kSpringCommitPending`.
373 break;
374 }
375 case kBeforeUnloadShown: {
376 CHECK_EQ(navigation_state_, NavigationState::kBeforeUnloadDispatched);
William Liu8e6e6ba2023-12-08 16:21:07377
William Liu7b0751a72024-10-23 16:08:52378 navigation_state_ = NavigationState::kBeforeUnloadShown;
William Liuc6cc30ec2024-03-14 16:54:16379 // On next `OnAnimate()`, `animation_driver_` will switch to
380 // `kSpringCancel`.
William Liuc6cc30ec2024-03-14 16:54:16381 break;
382 }
383 case kBeforeUnloadAckProceed: {
William Liu7b0751a72024-10-23 16:08:52384 CHECK_EQ(navigation_state_, NavigationState::kBeforeUnloadShown);
William Liuc6cc30ec2024-03-14 16:54:16385 navigation_state_ = NavigationState::kBeforeUnloadAckedProceed;
386 // On next `OnAnimate()`, `animation_driver_` will switch to
387 // `kSpringCommitPending`.
388 break;
389 }
William Liu7b0751a72024-10-23 16:08:52390 case kCancelledBeforeStart: {
391 navigation_state_ = NavigationState::kCancelled;
392 }
William Liuc6cc30ec2024-03-14 16:54:16393 }
William Liu8e6e6ba2023-12-08 16:21:07394}
395
William Liuc6cc30ec2024-03-14 16:54:16396void PhysicsModel::OnNavigationFinished(bool committed) {
397 switch (navigation_state_) {
William Liu7b0751a72024-10-23 16:08:52398 // For a gesture navigation that doesn't have a BeforeUnload handler.
399 case NavigationState::kStarted:
400 // It's possible the navigation commits so fast that the commit-pending
401 // spring hasn't played a single frame.
402 case NavigationState::kBeforeUnloadDispatched:
403 // A navigation starts after running the BeforeUnload handler.
William Liuc6cc30ec2024-03-14 16:54:16404 case NavigationState::kBeforeUnloadAckedProceed: {
William Liuc6cc30ec2024-03-14 16:54:16405 break;
406 }
William Liu7b0751a72024-10-23 16:08:52407 // A navigation needs to start first.
William Liuc6cc30ec2024-03-14 16:54:16408 case NavigationState::kNotStarted:
William Liu7b0751a72024-10-23 16:08:52409 // Not reachable because the browser is waiting for the ack from the
410 // renderer.
411 case NavigationState::kBeforeUnloadShown:
412 // A cancelled navigation should never commit.
413 case NavigationState::kCancelled:
414 // A navigation can only commit (finish) once.
William Liuc6cc30ec2024-03-14 16:54:16415 case NavigationState::kCommitted: {
William Liu7b0751a72024-10-23 16:08:52416 NOTREACHED();
William Liuc6cc30ec2024-03-14 16:54:16417 }
418 }
William Liu8e6e6ba2023-12-08 16:21:07419
William Liuc6cc30ec2024-03-14 16:54:16420 navigation_state_ =
421 committed ? NavigationState::kCommitted : NavigationState::kCancelled;
William Liu8e6e6ba2023-12-08 16:21:07422}
423
William Liu4db01f12024-10-30 14:39:31424bool PhysicsModel::ReachedCommitPending() const {
425 return animation_driver_ == Driver::kSpringCommitPending &&
426 foreground_has_reached_target_commit_pending_;
427}
428
William Liu8e6e6ba2023-12-08 16:21:07429void PhysicsModel::StartAnimating(base::TimeTicks time) {
430 animation_start_time_ = time;
431 animation_start_offset_viewport_ = foreground_offset_viewport_;
432}
433
William Liue4393bd2024-03-15 20:16:36434float PhysicsModel::ForegroundToBackGroundOffset(float fg_offset_viewport) {
435 float bg_offset_viewport = 0.f;
436
William Liu8e6e6ba2023-12-08 16:21:07437 if ((animation_driver_ == Driver::kSpringCommitPending ||
438 animation_driver_ == Driver::kSpringInvoke) &&
439 foreground_has_reached_target_commit_pending_) {
440 // Do not bounce the background page when the foreground page has reached
441 // the commit-pending point, once we have switched to the commit-pending
William Liue4393bd2024-03-15 20:16:36442 // spring or the invoke spring.
443 return bg_offset_viewport;
William Liu8e6e6ba2023-12-08 16:21:07444 }
William Liue4393bd2024-03-15 20:16:36445
446 // Maps:
447 // fg_offset_viewport 0 -> 0.85W -> W
448 // To:
449 // bg_offset_viewport -0.25W -> 0 -> 0
450 const float fg_commit_position_viewport =
451 viewport_width_ * kTargetCommitPendingRatio;
452 // If the foreground has passed the commit position, the background should be
453 // at origin.
454 if (fg_offset_viewport > fg_commit_position_viewport) {
455 return bg_offset_viewport;
456 }
457 const float fg_progress_to_commit_position =
458 fg_offset_viewport / fg_commit_position_viewport;
459 bg_offset_viewport = (1 - fg_progress_to_commit_position) *
460 kScreenshotInitialPositionRatio * viewport_width_;
461 return bg_offset_viewport;
William Liu8e6e6ba2023-12-08 16:21:07462}
463
464float PhysicsModel::FingerDragCurve(float movement_viewport) {
William Liue4393bd2024-03-15 20:16:36465 return foreground_offset_viewport_ +
466 kTargetCommitPendingRatio * movement_viewport;
William Liu8e6e6ba2023-12-08 16:21:07467}
468
Aldo Culquicondor195c75d2025-01-09 22:22:33469float PhysicsModel::CalculateVelocity(base::TimeTicks time) {
William Liu8e6e6ba2023-12-08 16:21:07470 float velocity = 0;
471
472 std::vector<float> timestamps;
473 std::vector<float> positions;
474 timestamps.reserve(touch_points_history_.size());
475 positions.reserve(touch_points_history_.size());
476 for (const auto& p : touch_points_history_) {
Aldo Culquicondor195c75d2025-01-09 22:22:33477 timestamps.push_back((time - p.timestamp).InMillisecondsF());
William Liu8e6e6ba2023-12-08 16:21:07478 positions.push_back(p.position_viewport);
479 }
480 SolveLeastSquare(timestamps, positions, &velocity);
Aldo Culquicondor195c75d2025-01-09 22:22:33481 return velocity;
William Liu8e6e6ba2023-12-08 16:21:07482}
483
484void PhysicsModel::RecordCommitPendingAccelerationStartIfNeeded(
485 base::TimeTicks request_animation_frame) {
486 if (animation_driver_ == Driver::kSpringCommitPending &&
William Liuc6cc30ec2024-03-14 16:54:16487 navigation_state_ == NavigationState::kCommitted) {
William Liub26568da2024-02-21 20:46:14488 float vel = spring_commit_pending_->ComputeVelocity();
489 if (IsValidVelocity(vel) && vel > kFloatTolerance) {
William Liu8e6e6ba2023-12-08 16:21:07490 // If the navigation is committed and `spring_commit_pending_` is moving
491 // at the opposite direction of the invoke animation, record the first
492 // requested frame's timestamp. This timestamp will be used to speed up
493 // the opposite-moving animation of the commit-pending spring. Since the
494 // navigation is committed, we should display the invoke animation as soon
495 // as possible.
496 if (commit_pending_acceleration_start_.is_null()) {
497 commit_pending_acceleration_start_ = request_animation_frame;
498 }
499 } else {
500 // `spring_commit_pending_` moves in the same direction as the invoke
501 // animation. Reset `commit_pending_acceleration_start_`.
502 commit_pending_acceleration_start_ = base::TimeTicks();
503 }
504 }
505}
506
507void PhysicsModel::AdvanceToNextAnimationDriver(
508 base::TimeTicks request_animation_frame) {
509 switch (animation_driver_) {
510 case Driver::kDragCurve: {
511 // We can only reach here for once, and once only.
512 CHECK(last_request_animation_frame_.is_null());
513 StartAnimating(request_animation_frame);
Aldo Culquicondor195c75d2025-01-09 22:22:33514 float finger_vel = CalculateVelocity(request_animation_frame);
William Liu7b0751a72024-10-23 16:08:52515 if (navigation_state_ == NavigationState::kCancelled ||
516 navigation_state_ == NavigationState::kBeforeUnloadShown) {
William Liu8e6e6ba2023-12-08 16:21:07517 animation_driver_ = Driver::kSpringCancel;
Aldo Culquicondor195c75d2025-01-09 22:22:33518 // The sign of the velocities from the drag curve and the cancel spring
519 // have opposite semantics, so we need to multiply by -1.
520 spring_cancel_->set_initial_velocity(-finger_vel);
William Liuc6cc30ec2024-03-14 16:54:16521 } else if (navigation_state_ == NavigationState::kCommitted) {
William Liu8e6e6ba2023-12-08 16:21:07522 animation_driver_ = Driver::kSpringInvoke;
523 spring_invoke_->set_initial_velocity(finger_vel);
524 } else {
William Liuc6cc30ec2024-03-14 16:54:16525 CHECK(navigation_state_ == NavigationState::kStarted ||
William Liu7b0751a72024-10-23 16:08:52526 navigation_state_ == NavigationState::kBeforeUnloadDispatched ||
William Liuc6cc30ec2024-03-14 16:54:16527 // This can happen when the renderer sends the BeforeUnload ack
528 // back to the browser so fast, that the cancel spring hasn't
529 // played a single frame thus we are still at
530 //`Driver::kDragCurve`. Typically this happens when the renderer
531 // doesn't have a sticky UserActivation.
punithbnayak326d20b2024-05-16 14:13:58532 navigation_state_ == NavigationState::kBeforeUnloadAckedProceed);
William Liu8e6e6ba2023-12-08 16:21:07533 animation_driver_ = Driver::kSpringCommitPending;
534 spring_commit_pending_->set_initial_velocity(finger_vel);
535 }
536 break;
537 }
538 case Driver::kSpringCommitPending: {
539 // It is rare but possible that we haven't played a single frame with
540 // commit-pending spring, where `last_request_animation_frame_` is null.
541 auto start_animating_raf = !last_request_animation_frame_.is_null()
542 ? last_request_animation_frame_
543 : request_animation_frame;
544 if (commit_pending_acceleration_start_.is_null() &&
William Liuc6cc30ec2024-03-14 16:54:16545 navigation_state_ == NavigationState::kCommitted) {
William Liu8e6e6ba2023-12-08 16:21:07546 // Only switch from commit-pending spring to the invoke spring when:
547 // - The commit-pending is moving in the same direction as the invoke
548 // animation, for which `commit_pending_acceleration_start_` is null.
549 // - The navigation is committed.
550 StartAnimating(start_animating_raf);
551 animation_driver_ = Driver::kSpringInvoke;
552 spring_invoke_->set_initial_velocity(
553 spring_commit_pending_->ComputeVelocity());
William Liu7b0751a72024-10-23 16:08:52554 } else if (navigation_state_ == NavigationState::kCancelled ||
555 navigation_state_ == NavigationState::kBeforeUnloadShown) {
William Liu8e6e6ba2023-12-08 16:21:07556 StartAnimating(start_animating_raf);
557 animation_driver_ = Driver::kSpringCancel;
Alison Gale770f3fc2024-04-27 00:39:58558 // TODO(crbug.com/40945408): Ditto.
William Liu8e6e6ba2023-12-08 16:21:07559 spring_cancel_->set_initial_velocity(1.f);
560 } else {
561 // Keep running the commit-pending animation if:
562 // - The commit-pending animation is being accelerated, for which
563 // `last_request_animation_frame_` is non-null.
564 // - The on-going navigation hasn't reached its final state
565 // (`OnDidFinishNavigation()` not yet called).
William Liu7b0751a72024-10-23 16:08:52566 // - If the browser has asked the renderer to run the BeforeUnload
567 // handler but the renderer hasn't ack'ed the message.
William Liu8e6e6ba2023-12-08 16:21:07568 const bool commit_pending_being_accelerated =
569 (!last_request_animation_frame_.is_null() &&
William Liuc6cc30ec2024-03-14 16:54:16570 navigation_state_ == NavigationState::kCommitted);
William Liu7b0751a72024-10-23 16:08:52571 const bool nav_started =
William Liuc6cc30ec2024-03-14 16:54:16572 navigation_state_ == NavigationState::kStarted ||
573 navigation_state_ == NavigationState::kBeforeUnloadAckedProceed;
William Liu7b0751a72024-10-23 16:08:52574 const bool nav_requested =
575 navigation_state_ == NavigationState::kBeforeUnloadDispatched;
576 CHECK(commit_pending_being_accelerated || nav_started || nav_requested);
William Liuc6cc30ec2024-03-14 16:54:16577 }
578 break;
579 }
580 case Driver::kSpringCancel: {
William Liuc6cc30ec2024-03-14 16:54:16581 if (navigation_state_ == NavigationState::kBeforeUnloadAckedProceed) {
582 // We only switch away from `kSpringCancel` when the renderer has acked
583 // the BeforeUnload message and navigation should proceed. When the
584 // BeforeUnload message is dispatched to the renderer, `kSpringCancel`
585 // drives the animation. When the renderer acks to proceed the
586 // navigation, we switch from `kSpringCancel` to `kSpringCommitPending`.
587
588 // It's visually incorrect to use `last_request_animation_frame_` here.
589 // Say the last frame is animated by `kSpringCancel` at time T and the
590 // user interacts with the BeforeUnload prompt at T+10s to begin the
591 // navigation. It's incorrect to tell `kSpringCommitPending` to animate
592 // the first frame as if it has been 10 seconds since the last frame.
593 StartAnimating(request_animation_frame);
594 animation_driver_ = Driver::kSpringCommitPending;
William Liu688864f2024-05-08 21:48:54595 // Set the initial velocity to zero because the commit-pending (or
596 // invoke) spring will move the active page across the entire viewport.
597 // A high velocity would make the animation look like it's skipping
598 // frames.
599 spring_commit_pending_->set_initial_velocity(0.f);
punithbnayak326d20b2024-05-16 14:13:58600 } else if (navigation_state_ == NavigationState::kCommitted)
601 [[unlikely]] {
William Liuc6cc30ec2024-03-14 16:54:16602 // Also rare but possible (e.g., in tests) for the navigation to commit
603 // so fast that the commit-pending spring hasn't played a single frame,
604 // after BeforeUnload is executed with "proceed". Directly switch to the
605 // invoke spring in this case.
606 StartAnimating(request_animation_frame);
607 animation_driver_ = Driver::kSpringInvoke;
William Liu688864f2024-05-08 21:48:54608 spring_invoke_->set_initial_velocity(0.f);
William Liu8e6e6ba2023-12-08 16:21:07609 }
610 break;
611 }
612 // Shouldn't switch from the terminal states.
613 case Driver::kSpringInvoke:
William Liu8e6e6ba2023-12-08 16:21:07614 return;
615 }
616
William Liub26568da2024-02-21 20:46:14617 if (!IsValidVelocity(spring_invoke_->initial_velocity())) {
William Liu8e6e6ba2023-12-08 16:21:07618 spring_invoke_->set_initial_velocity(-2.0);
619 }
William Liub26568da2024-02-21 20:46:14620 if (!IsValidVelocity(spring_commit_pending_->initial_velocity())) {
William Liu8e6e6ba2023-12-08 16:21:07621 spring_commit_pending_->set_initial_velocity(0.f);
622 }
William Liub26568da2024-02-21 20:46:14623 if (!IsValidVelocity(spring_cancel_->initial_velocity())) {
Aldo Culquicondor195c75d2025-01-09 22:22:33624 spring_cancel_->set_initial_velocity(-1.f);
William Liu8e6e6ba2023-12-08 16:21:07625 }
626}
627
628base::TimeDelta PhysicsModel::CalculateRequestAnimationFrameSinceStart(
629 base::TimeTicks request_animation_frame) {
630 // Shouldn't be called for the drag curve animation.
631 CHECK_NE(animation_driver_, Driver::kDragCurve);
632
633 base::TimeDelta raf_since_start =
634 request_animation_frame - animation_start_time_;
635
636 // Accelerate the commit-pending animation if necessary.
637 if (!commit_pending_acceleration_start_.is_null()) {
William Liuc6cc30ec2024-03-14 16:54:16638 CHECK_EQ(navigation_state_, NavigationState::kCommitted);
William Liu8e6e6ba2023-12-08 16:21:07639 CHECK_EQ(animation_driver_, Driver::kSpringCommitPending);
640 // Add a delta to all the left-moving frames. This is to "speed up" the
641 // spring animation, so it can start to move to the right sooner, to display
642 // the invoke animation.
643 //
644 // Ex:
645 // - request animation frame timeline: [37, 39, 41, 43, 45 ...]
646 // - raf timeline with the delta: [37, 41, 45, 49, 53 ...]
647 //
648 // So the net effect is the animation is sped up twice.
649 raf_since_start +=
650 (request_animation_frame - commit_pending_acceleration_start_);
651 }
652
653 return raf_since_start;
654}
655
656} // namespace content