William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 1 | // 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 Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 7 | #include <algorithm> |
punithnayak | b367adb | 2024-02-20 22:27:41 | [diff] [blame] | 8 | #include <numbers> |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 9 | #include <vector> |
| 10 | |
| 11 | #include "base/logging.h" |
| 12 | #include "base/notreached.h" |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 13 | #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 Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 19 | // - 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 22 | // 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 | |
| 27 | namespace content { |
| 28 | |
| 29 | namespace { |
| 30 | |
| 31 | // The tolerance value for which two floats are consider equal. |
| 32 | constexpr float kFloatTolerance = 0.001f; |
| 33 | |
| 34 | // Used to replace NaN and Inf. |
| 35 | constexpr 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. |
| 42 | constexpr int kSpringResponse = 708; |
| 43 | |
| 44 | // How much the spring overshoots. Smaller the value, more bouncy the spring. |
| 45 | constexpr float kSpringDampingRatio = 0.81f; |
| 46 | |
| 47 | // The size of the spring location history. |
| 48 | constexpr int kSpringHistorySize = 10; |
| 49 | |
| 50 | // A spring is considered at rest if it has used at least |
| 51 | // `kSpringAtRestThreshold`*`kSpringHistorySize` amount of energy. |
| 52 | constexpr int kSpringAtRestThreshold = 10; |
| 53 | |
| 54 | // Physics model. |
| 55 | // |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 56 | // 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. |
| 59 | constexpr int kPhysicsModelHistorySize = 10; |
| 60 | |
William Liu | b26568da | 2024-02-21 20:46:14 | [diff] [blame] | 61 | bool IsValidVelocity(float velocity) { |
| 62 | return !base::IsApproximatelyEqual(velocity, kInvalidVelocity, |
| 63 | kFloatTolerance); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 64 | } |
| 65 | |
| 66 | // Solves `positions`=`slope`*`timestamps`+ displacement(not calculated). |
| 67 | // |
Alison Gale | 770f3fc | 2024-04-27 00:39:58 | [diff] [blame] | 68 | // TODO(crbug.com/40945408): The native least square might not give us |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 69 | // the desired velocity. |
| 70 | void 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 | |
| 113 | class 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 = |
punithnayak | b367adb | 2024-02-20 22:27:41 | [diff] [blame] | 136 | std::pow(2 * std::numbers::pi_v<float> / frequency_response_, 2) * |
| 137 | mass_; |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 138 | 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 | |
| 228 | PhysicsModel::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 | |
| 244 | PhysicsModel::~PhysicsModel() = default; |
| 245 | |
| 246 | PhysicsModel::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 Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 262 | viewport_width_ * kTargetCommitPendingRatio - |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 263 | animation_start_offset_viewport_, |
| 264 | raf_since_start); |
William Liu | 86212af | 2024-02-28 17:01:28 | [diff] [blame] | 265 | // Prevent overshoot the right edge. |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 266 | foreground_offset_viewport_ = std::min( |
William Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 267 | viewport_width_, viewport_width_ * kTargetCommitPendingRatio - |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 268 | spring_position.equilibrium_offset_viewport); |
William Liu | 86212af | 2024-02-28 17:01:28 | [diff] [blame] | 269 | // 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 272 | break; |
| 273 | } |
| 274 | case Driver::kSpringInvoke: { |
| 275 | spring_position = spring_invoke_->GetPosition( |
| 276 | viewport_width_ - animation_start_offset_viewport_, raf_since_start); |
William Liu | 86212af | 2024-02-28 17:01:28 | [diff] [blame] | 277 | // Prevent overshoot the right edge. |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 278 | foreground_offset_viewport_ = std::min( |
| 279 | viewport_width_, |
| 280 | viewport_width_ - spring_position.equilibrium_offset_viewport); |
William Liu | 86212af | 2024-02-28 17:01:28 | [diff] [blame] | 281 | // 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 284 | break; |
| 285 | } |
| 286 | case Driver::kSpringCancel: { |
| 287 | spring_position = spring_cancel_->GetPosition( |
| 288 | animation_start_offset_viewport_, raf_since_start); |
William Liu | 86212af | 2024-02-28 17:01:28 | [diff] [blame] | 289 | // Prevent overshoot the left edge. |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 290 | foreground_offset_viewport_ = |
| 291 | std::max(spring_position.equilibrium_offset_viewport, 0.f); |
William Liu | 86212af | 2024-02-28 17:01:28 | [diff] [blame] | 292 | // 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 296 | break; |
| 297 | } |
| 298 | case Driver::kDragCurve: { |
Peter Boström | 01ab59a | 2024-08-15 02:39:49 | [diff] [blame] | 299 | NOTREACHED(); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 300 | } |
| 301 | } |
| 302 | |
| 303 | foreground_has_reached_target_commit_pending_ |= |
William Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 304 | foreground_offset_viewport_ >= |
| 305 | kTargetCommitPendingRatio * viewport_width_; |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 306 | |
| 307 | last_request_animation_frame_ = request_animation_frame; |
| 308 | |
| 309 | return Result{ |
| 310 | .foreground_offset_physical = |
Aldo Culquicondor | a5b5864 | 2025-08-11 17:39:14 | [diff] [blame] | 311 | std::round(foreground_offset_viewport_ * device_scale_factor_), |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 312 | .background_offset_physical = |
Aldo Culquicondor | a5b5864 | 2025-08-11 17:39:14 | [diff] [blame] | 313 | std::round(ForegroundToBackGroundOffset(foreground_offset_viewport_) * |
| 314 | device_scale_factor_), |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 315 | // 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_`. |
| 325 | PhysicsModel::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 Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 350 | void PhysicsModel::SwitchSpringForReason(SwitchSpringReason reason) { |
| 351 | switch (reason) { |
| 352 | case kGestureCancelled: |
| 353 | case kGestureInvoked: { |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 354 | // 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 358 | |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 359 | if (reason == kGestureCancelled) { |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 360 | navigation_state_ = NavigationState::kCancelled; |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 361 | } |
| 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 Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 370 | 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 377 | |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 378 | navigation_state_ = NavigationState::kBeforeUnloadShown; |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 379 | // On next `OnAnimate()`, `animation_driver_` will switch to |
| 380 | // `kSpringCancel`. |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 381 | break; |
| 382 | } |
| 383 | case kBeforeUnloadAckProceed: { |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 384 | CHECK_EQ(navigation_state_, NavigationState::kBeforeUnloadShown); |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 385 | navigation_state_ = NavigationState::kBeforeUnloadAckedProceed; |
| 386 | // On next `OnAnimate()`, `animation_driver_` will switch to |
| 387 | // `kSpringCommitPending`. |
| 388 | break; |
| 389 | } |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 390 | case kCancelledBeforeStart: { |
| 391 | navigation_state_ = NavigationState::kCancelled; |
| 392 | } |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 393 | } |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 394 | } |
| 395 | |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 396 | void PhysicsModel::OnNavigationFinished(bool committed) { |
| 397 | switch (navigation_state_) { |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 398 | // 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 Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 404 | case NavigationState::kBeforeUnloadAckedProceed: { |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 405 | break; |
| 406 | } |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 407 | // A navigation needs to start first. |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 408 | case NavigationState::kNotStarted: |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 409 | // 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 Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 415 | case NavigationState::kCommitted: { |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 416 | NOTREACHED(); |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 417 | } |
| 418 | } |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 419 | |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 420 | navigation_state_ = |
| 421 | committed ? NavigationState::kCommitted : NavigationState::kCancelled; |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 422 | } |
| 423 | |
William Liu | 4db01f1 | 2024-10-30 14:39:31 | [diff] [blame] | 424 | bool PhysicsModel::ReachedCommitPending() const { |
| 425 | return animation_driver_ == Driver::kSpringCommitPending && |
| 426 | foreground_has_reached_target_commit_pending_; |
| 427 | } |
| 428 | |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 429 | void PhysicsModel::StartAnimating(base::TimeTicks time) { |
| 430 | animation_start_time_ = time; |
| 431 | animation_start_offset_viewport_ = foreground_offset_viewport_; |
| 432 | } |
| 433 | |
William Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 434 | float PhysicsModel::ForegroundToBackGroundOffset(float fg_offset_viewport) { |
| 435 | float bg_offset_viewport = 0.f; |
| 436 | |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 437 | 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 Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 442 | // spring or the invoke spring. |
| 443 | return bg_offset_viewport; |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 444 | } |
William Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 445 | |
| 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 Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 462 | } |
| 463 | |
| 464 | float PhysicsModel::FingerDragCurve(float movement_viewport) { |
William Liu | e4393bd | 2024-03-15 20:16:36 | [diff] [blame] | 465 | return foreground_offset_viewport_ + |
| 466 | kTargetCommitPendingRatio * movement_viewport; |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 467 | } |
| 468 | |
Aldo Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 469 | float PhysicsModel::CalculateVelocity(base::TimeTicks time) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 470 | 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 Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 477 | timestamps.push_back((time - p.timestamp).InMillisecondsF()); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 478 | positions.push_back(p.position_viewport); |
| 479 | } |
| 480 | SolveLeastSquare(timestamps, positions, &velocity); |
Aldo Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 481 | return velocity; |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 482 | } |
| 483 | |
| 484 | void PhysicsModel::RecordCommitPendingAccelerationStartIfNeeded( |
| 485 | base::TimeTicks request_animation_frame) { |
| 486 | if (animation_driver_ == Driver::kSpringCommitPending && |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 487 | navigation_state_ == NavigationState::kCommitted) { |
William Liu | b26568da | 2024-02-21 20:46:14 | [diff] [blame] | 488 | float vel = spring_commit_pending_->ComputeVelocity(); |
| 489 | if (IsValidVelocity(vel) && vel > kFloatTolerance) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 490 | // 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 | |
| 507 | void 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 Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 514 | float finger_vel = CalculateVelocity(request_animation_frame); |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 515 | if (navigation_state_ == NavigationState::kCancelled || |
| 516 | navigation_state_ == NavigationState::kBeforeUnloadShown) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 517 | animation_driver_ = Driver::kSpringCancel; |
Aldo Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 518 | // 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 Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 521 | } else if (navigation_state_ == NavigationState::kCommitted) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 522 | animation_driver_ = Driver::kSpringInvoke; |
| 523 | spring_invoke_->set_initial_velocity(finger_vel); |
| 524 | } else { |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 525 | CHECK(navigation_state_ == NavigationState::kStarted || |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 526 | navigation_state_ == NavigationState::kBeforeUnloadDispatched || |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 527 | // 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. |
punithbnayak | 326d20b | 2024-05-16 14:13:58 | [diff] [blame] | 532 | navigation_state_ == NavigationState::kBeforeUnloadAckedProceed); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 533 | 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 Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 545 | navigation_state_ == NavigationState::kCommitted) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 546 | // 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 Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 554 | } else if (navigation_state_ == NavigationState::kCancelled || |
| 555 | navigation_state_ == NavigationState::kBeforeUnloadShown) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 556 | StartAnimating(start_animating_raf); |
| 557 | animation_driver_ = Driver::kSpringCancel; |
Alison Gale | 770f3fc | 2024-04-27 00:39:58 | [diff] [blame] | 558 | // TODO(crbug.com/40945408): Ditto. |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 559 | 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 Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 566 | // - If the browser has asked the renderer to run the BeforeUnload |
| 567 | // handler but the renderer hasn't ack'ed the message. |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 568 | const bool commit_pending_being_accelerated = |
| 569 | (!last_request_animation_frame_.is_null() && |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 570 | navigation_state_ == NavigationState::kCommitted); |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 571 | const bool nav_started = |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 572 | navigation_state_ == NavigationState::kStarted || |
| 573 | navigation_state_ == NavigationState::kBeforeUnloadAckedProceed; |
William Liu | 7b0751a7 | 2024-10-23 16:08:52 | [diff] [blame] | 574 | const bool nav_requested = |
| 575 | navigation_state_ == NavigationState::kBeforeUnloadDispatched; |
| 576 | CHECK(commit_pending_being_accelerated || nav_started || nav_requested); |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 577 | } |
| 578 | break; |
| 579 | } |
| 580 | case Driver::kSpringCancel: { |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 581 | 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 Liu | 688864f | 2024-05-08 21:48:54 | [diff] [blame] | 595 | // 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); |
punithbnayak | 326d20b | 2024-05-16 14:13:58 | [diff] [blame] | 600 | } else if (navigation_state_ == NavigationState::kCommitted) |
| 601 | [[unlikely]] { |
William Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 602 | // 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 Liu | 688864f | 2024-05-08 21:48:54 | [diff] [blame] | 608 | spring_invoke_->set_initial_velocity(0.f); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 609 | } |
| 610 | break; |
| 611 | } |
| 612 | // Shouldn't switch from the terminal states. |
| 613 | case Driver::kSpringInvoke: |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 614 | return; |
| 615 | } |
| 616 | |
William Liu | b26568da | 2024-02-21 20:46:14 | [diff] [blame] | 617 | if (!IsValidVelocity(spring_invoke_->initial_velocity())) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 618 | spring_invoke_->set_initial_velocity(-2.0); |
| 619 | } |
William Liu | b26568da | 2024-02-21 20:46:14 | [diff] [blame] | 620 | if (!IsValidVelocity(spring_commit_pending_->initial_velocity())) { |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 621 | spring_commit_pending_->set_initial_velocity(0.f); |
| 622 | } |
William Liu | b26568da | 2024-02-21 20:46:14 | [diff] [blame] | 623 | if (!IsValidVelocity(spring_cancel_->initial_velocity())) { |
Aldo Culquicondor | 195c75d | 2025-01-09 22:22:33 | [diff] [blame] | 624 | spring_cancel_->set_initial_velocity(-1.f); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 625 | } |
| 626 | } |
| 627 | |
| 628 | base::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 Liu | c6cc30ec | 2024-03-14 16:54:16 | [diff] [blame] | 638 | CHECK_EQ(navigation_state_, NavigationState::kCommitted); |
William Liu | 8e6e6ba | 2023-12-08 16:21:07 | [diff] [blame] | 639 | 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 |