| // Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file | 
 | // for details. All rights reserved. Use of this source code is governed by a | 
 | // BSD-style license that can be found in the LICENSE file. | 
 |  | 
 | // @dart = 2.9 | 
 |  | 
 | part of touch; | 
 |  | 
 | /** | 
 |  * Implementations can be used to simulate the deceleration of an element within | 
 |  * a certain region. To use this behavior you need to provide an initial | 
 |  * velocity that is meant to represent the gesture that is initiating this | 
 |  * deceleration. You also provide the bounds of the region that the element | 
 |  * exists in, and the current offset of the element within that region. The | 
 |  * transitions will have the element decelerate to rest, or stretch past the | 
 |  * offset boundaries and then come to rest. | 
 |  * | 
 |  * This is primarily designed to solve the problem of slow scrolling in mobile | 
 |  * safari. You can use this along with the [Scroller] behavior to make a | 
 |  * scrollable area scroll the same way it would in a native application. | 
 |  * | 
 |  * Implementations of this interface do not maintain any references to HTML | 
 |  * elements, and therefore cannot do any redrawing of elements. They only | 
 |  * calculates where the element should be on an interval. It is the delegate's | 
 |  * responsibility to redraw the element when the onDecelerate callback is | 
 |  * invoked. It is recommended that you move the element with a hardware | 
 |  * accelerated method such as using 'translate3d' on the element's | 
 |  * -webkit-transform style property. | 
 |  */ | 
 | abstract class Momentum { | 
 |   factory Momentum(MomentumDelegate delegate, | 
 |           [num defaultDecelerationFactor = 1]) => | 
 |       new TimeoutMomentum(delegate, defaultDecelerationFactor); | 
 |  | 
 |   bool get decelerating; | 
 |  | 
 |   num get decelerationFactor; | 
 |  | 
 |   /** | 
 |   * Transition end handler. This function must be invoked after any transition | 
 |   * that occurred as a result of a call to the delegate's onDecelerate callback. | 
 |   */ | 
 |   void onTransitionEnd(); | 
 |  | 
 |   /** | 
 |    * Start decelerating. | 
 |    * The [velocity] passed should be in terms of number of pixels / millisecond. | 
 |    * [minCoord] and [maxCoord] specify the content's scrollable boundary. | 
 |    * The current offset of the element within its boundaries is specified by | 
 |    * [initialOffset]. | 
 |    * Returns true if deceleration has been initiated. | 
 |    */ | 
 |   bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord, | 
 |       Coordinate initialOffset, | 
 |       [num decelerationFactor]); | 
 |  | 
 |   /** | 
 |    * Calculate the velocity required to transition between coordinates [start] | 
 |    * and [target] optionally specifying a custom [decelerationFactor]. | 
 |    */ | 
 |   Coordinate calculateVelocity(Coordinate start, Coordinate target, | 
 |       [num decelerationFactor]); | 
 |  | 
 |   /** Stop decelerating and return the current velocity. */ | 
 |   Coordinate stop(); | 
 |  | 
 |   /** Aborts decelerating without dispatching any notification events. */ | 
 |   void abort(); | 
 |  | 
 |   /** null if no transition is in progress. */ | 
 |   Coordinate get destination; | 
 | } | 
 |  | 
 | /** | 
 |  * Momentum Delegate interface. | 
 |  * You are required to implement this interface in order to use the | 
 |  * Momentum behavior. | 
 |  */ | 
 | abstract class MomentumDelegate { | 
 |   /** | 
 |    * Callback for a deceleration step. The delegate is responsible for redrawing | 
 |    * the element in its new position specified in px. | 
 |    */ | 
 |   void onDecelerate(num x, num y); | 
 |  | 
 |   /** | 
 |    * Callback for end of deceleration. | 
 |    */ | 
 |   void onDecelerationEnd(); | 
 | } | 
 |  | 
 | class BouncingState { | 
 |   static const NOT_BOUNCING = 0; | 
 |   static const BOUNCING_AWAY = 1; | 
 |   static const BOUNCING_BACK = 2; | 
 | } | 
 |  | 
 | class _Move { | 
 |   final num x; | 
 |   final num y; | 
 |   final num vx; | 
 |   final num vy; | 
 |   final num time; | 
 |  | 
 |   _Move(this.x, this.y, this.vx, this.vy, this.time); | 
 | } | 
 |  | 
 | /** | 
 |  * Secant method root solver helper class. | 
 |  * We use http://en.wikipedia.org/wiki/Secant_method | 
 |  * falling back to the http://en.wikipedia.org/wiki/Bisection_method | 
 |  * if it doesn't appear we are converging properlty. | 
 |  * TODO(jacobr): simplify the code so we don't have to use this solver | 
 |  * class at all. | 
 |  */ | 
 | class Solver { | 
 |   static num solve(num Function(num) fn, num targetY, num startX, | 
 |       [int maxIterations = 50]) { | 
 |     num lastX = 0; | 
 |     num lastY = fn(lastX); | 
 |     num deltaX; | 
 |     num deltaY; | 
 |     num minX = null; | 
 |     num maxX = null; | 
 |     num x = startX; | 
 |     num delta = startX; | 
 |     for (int i = 0; i < maxIterations; i++) { | 
 |       num y = fn(x); | 
 |       if (y.round() == targetY.round()) { | 
 |         return x; | 
 |       } | 
 |       if (y > targetY) { | 
 |         maxX = x; | 
 |       } else { | 
 |         minX = x; | 
 |       } | 
 |  | 
 |       num errorY = targetY - y; | 
 |       deltaX = x - lastX; | 
 |       deltaY = y - lastY; | 
 |       lastX = x; | 
 |       lastY = y; | 
 |       // Avoid divide by zero and as a hack just repeat the previous delta. | 
 |       // Obviously this is a little dangerous and we might not converge. | 
 |       if (deltaY != 0) { | 
 |         delta = errorY * deltaX / deltaY; | 
 |       } | 
 |       x += delta; | 
 |       if (minX != null && maxX != null && (x > minX || x < maxX)) { | 
 |         // Fall back to binary search. | 
 |         x = (minX + maxX) / 2; | 
 |       } | 
 |     } | 
 |     window.console.warn('''Could not find an exact solution. LastY=${lastY}, | 
 |         targetY=${targetY} lastX=$lastX delta=$delta  deltaX=$deltaX | 
 |         deltaY=$deltaY'''); | 
 |     return x; | 
 |   } | 
 | } | 
 |  | 
 | /** | 
 |  * Helper class modeling the physics of a throwable scrollable area along a | 
 |  * single dimension. | 
 |  */ | 
 | class SingleDimensionPhysics { | 
 |   /** The number of frames per second the animation should run at. */ | 
 |   static const _FRAMES_PER_SECOND = 60; | 
 |  | 
 |   /** | 
 |    * The spring coefficient for when the element has passed a boundary and is | 
 |    * decelerating to change direction and bounce back. Each frame, the velocity | 
 |    * will be changed by x times this coefficient, where x is the current stretch | 
 |    * value of the element from its boundary. This will end when velocity reaches | 
 |    * zero. | 
 |    */ | 
 |   static const _PRE_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND; | 
 |  | 
 |   /** | 
 |    * The spring coefficient for when the element is bouncing back from a | 
 |    * stretched offset to a min or max position. Each frame, the velocity will | 
 |    * be changed to x times this coefficient, where x is the current stretch | 
 |    * value of the element from its boundary. This will end when the stretch | 
 |    * value reaches 0. | 
 |    */ | 
 |   static const _POST_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND; | 
 |  | 
 |   /** | 
 |    * The number of milliseconds per animation frame. | 
 |    */ | 
 |   static const _MS_PER_FRAME = 1000.0 / _FRAMES_PER_SECOND; | 
 |  | 
 |   /** | 
 |    * The constant factor applied to velocity at each frame to simulate | 
 |    * deceleration. | 
 |    */ | 
 |   static const _DECELERATION_FACTOR = 0.97; | 
 |  | 
 |   static const _MAX_VELOCITY_STATIC_FRICTION = 0.08 * _MS_PER_FRAME; | 
 |   static const _DECELERATION_FACTOR_STATIC_FRICTION = 0.92; | 
 |  | 
 |   /** | 
 |    * Minimum velocity required to start or continue deceleration, in | 
 |    * pixels/frame. This is equivalent to 0.25 px/ms. | 
 |    */ | 
 |   static const _MIN_VELOCITY = 0.25 * _MS_PER_FRAME; | 
 |  | 
 |   /** | 
 |    * Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01 | 
 |    * px/ms. | 
 |    */ | 
 |   static const _MIN_STEP_VELOCITY = 0.01 * _MS_PER_FRAME; | 
 |  | 
 |   /** | 
 |    * Boost the initial velocity by a certain factor before applying momentum. | 
 |    * This just gives the momentum a better feel. | 
 |    */ | 
 |   static const _INITIAL_VELOCITY_BOOST_FACTOR = 1.25; | 
 |  | 
 |   /** | 
 |    * Additional deceleration factor to apply for the current move only.  This | 
 |    * is helpful for cases such as scroll wheel scrolling where the default | 
 |    * amount of deceleration is inadequate. | 
 |    */ | 
 |   num customDecelerationFactor = 1; | 
 |   num _minCoord; | 
 |   num _maxCoord; | 
 |  | 
 |   /** The bouncing state. */ | 
 |   int _bouncingState; | 
 |  | 
 |   num velocity; | 
 |   num _currentOffset; | 
 |  | 
 |   /** | 
 |    * constant used when guessing at the velocity required to throw to a specific | 
 |    * location. Chosen arbitrarily. All that really matters is that the velocity | 
 |    * is large enough that a throw gesture will occur. | 
 |    */ | 
 |   static const _VELOCITY_GUESS = 20; | 
 |  | 
 |   SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING {} | 
 |  | 
 |   void configure(num minCoord, num maxCoord, num initialOffset, | 
 |       num customDecelerationFactor_, num velocity_) { | 
 |     _bouncingState = BouncingState.NOT_BOUNCING; | 
 |     _minCoord = minCoord; | 
 |     _maxCoord = maxCoord; | 
 |     _currentOffset = initialOffset; | 
 |     this.customDecelerationFactor = customDecelerationFactor_; | 
 |     _adjustInitialVelocityAndBouncingState(velocity_); | 
 |   } | 
 |  | 
 |   num solve( | 
 |       num initialOffset, num targetOffset, num customDecelerationFactor_) { | 
 |     initialOffset = initialOffset.round(); | 
 |     targetOffset = targetOffset.round(); | 
 |     if (initialOffset == targetOffset) { | 
 |       return 0; | 
 |     } | 
 |     return Solver.solve((num velocity_) { | 
 |       // Don't specify min and max coordinates as we don't need to bother | 
 |       // with the simulating bouncing off the edges. | 
 |       configure(null, null, initialOffset.round(), customDecelerationFactor_, | 
 |           velocity_); | 
 |       stepAll(); | 
 |       return _currentOffset; | 
 |     }, targetOffset, | 
 |         targetOffset > initialOffset ? _VELOCITY_GUESS : -_VELOCITY_GUESS); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Helper method to calculate initial velocity. | 
 |    * The [velocity] passed here should be in terms of number of | 
 |    * pixels / millisecond. Returns the adjusted x and y velocities. | 
 |    */ | 
 |   void _adjustInitialVelocityAndBouncingState(num v) { | 
 |     velocity = v * _MS_PER_FRAME * _INITIAL_VELOCITY_BOOST_FACTOR; | 
 |  | 
 |     if (velocity.abs() < _MIN_VELOCITY) { | 
 |       if (_minCoord != null && _currentOffset < _minCoord) { | 
 |         velocity = (_minCoord - _currentOffset) * _POST_BOUNCE_COEFFICIENT; | 
 |         velocity = Math.max(velocity, _MIN_STEP_VELOCITY); | 
 |         _bouncingState = BouncingState.BOUNCING_BACK; | 
 |       } else if (_maxCoord != null && _currentOffset > _maxCoord) { | 
 |         velocity = (_currentOffset - _maxCoord) * _POST_BOUNCE_COEFFICIENT; | 
 |         velocity = -Math.max(velocity, _MIN_STEP_VELOCITY); | 
 |         _bouncingState = BouncingState.BOUNCING_BACK; | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Apply deceleration. | 
 |    */ | 
 |   void _adjustVelocity() { | 
 |     num speed = velocity.abs(); | 
 |     velocity *= _DECELERATION_FACTOR; | 
 |     if (customDecelerationFactor != null) { | 
 |       velocity *= customDecelerationFactor; | 
 |     } | 
 |     // This isn't really how static friction works but it is a plausible | 
 |     // approximation. | 
 |     if (speed < _MAX_VELOCITY_STATIC_FRICTION) { | 
 |       velocity *= _DECELERATION_FACTOR_STATIC_FRICTION; | 
 |     } | 
 |  | 
 |     num stretchDistance; | 
 |     if (_minCoord != null && _currentOffset < _minCoord) { | 
 |       stretchDistance = _minCoord - _currentOffset; | 
 |     } else { | 
 |       if (_maxCoord != null && _currentOffset > _maxCoord) { | 
 |         stretchDistance = _maxCoord - _currentOffset; | 
 |       } | 
 |     } | 
 |     if (stretchDistance != null) { | 
 |       if (stretchDistance * velocity < 0) { | 
 |         _bouncingState = _bouncingState == BouncingState.BOUNCING_BACK | 
 |             ? BouncingState.NOT_BOUNCING | 
 |             : BouncingState.BOUNCING_AWAY; | 
 |         velocity += stretchDistance * _PRE_BOUNCE_COEFFICIENT; | 
 |       } else { | 
 |         _bouncingState = BouncingState.BOUNCING_BACK; | 
 |         velocity = stretchDistance > 0 | 
 |             ? Math.max( | 
 |                 stretchDistance * _POST_BOUNCE_COEFFICIENT, _MIN_STEP_VELOCITY) | 
 |             : Math.min(stretchDistance * _POST_BOUNCE_COEFFICIENT, | 
 |                 -_MIN_STEP_VELOCITY); | 
 |       } | 
 |     } else { | 
 |       _bouncingState = BouncingState.NOT_BOUNCING; | 
 |     } | 
 |   } | 
 |  | 
 |   void step() { | 
 |     // It is common for scrolling to be disabled so in these cases we want to | 
 |     // avoid needless calculations. | 
 |     if (velocity != null) { | 
 |       _currentOffset += velocity; | 
 |       _adjustVelocity(); | 
 |     } | 
 |   } | 
 |  | 
 |   void stepAll() { | 
 |     while (!isDone()) { | 
 |       step(); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Whether or not the current velocity is above the threshold required to | 
 |    * continue decelerating. | 
 |    */ | 
 |   bool isVelocityAboveThreshold(num threshold) { | 
 |     return velocity.abs() >= threshold; | 
 |   } | 
 |  | 
 |   bool isDone() { | 
 |     return _bouncingState == BouncingState.NOT_BOUNCING && | 
 |         !isVelocityAboveThreshold(_MIN_STEP_VELOCITY); | 
 |   } | 
 | } | 
 |  | 
 | /** | 
 |  * Implementation of a momentum strategy using webkit-transforms | 
 |  * and timeouts. | 
 |  */ | 
 | class TimeoutMomentum implements Momentum { | 
 |   SingleDimensionPhysics physicsX; | 
 |   SingleDimensionPhysics physicsY; | 
 |   Coordinate _previousOffset; | 
 |   Queue<_Move> _moves; | 
 |   num _stepTimeout; | 
 |   bool _decelerating; | 
 |   MomentumDelegate _delegate; | 
 |   int _nextY; | 
 |   int _nextX; | 
 |   Coordinate _minCoord; | 
 |   Coordinate _maxCoord; | 
 |   num _customDecelerationFactor; | 
 |   num _defaultDecelerationFactor; | 
 |  | 
 |   TimeoutMomentum(this._delegate, [num defaultDecelerationFactor = 1]) | 
 |       : _defaultDecelerationFactor = defaultDecelerationFactor, | 
 |         _decelerating = false, | 
 |         _moves = new Queue<_Move>(), | 
 |         physicsX = new SingleDimensionPhysics(), | 
 |         physicsY = new SingleDimensionPhysics(); | 
 |  | 
 |   /** | 
 |    * Calculate and return the moves for the deceleration motion. | 
 |    */ | 
 |   void _calculateMoves() { | 
 |     _moves.clear(); | 
 |     num time = TimeUtil.now(); | 
 |     while (!physicsX.isDone() || !physicsY.isDone()) { | 
 |       _stepWithoutAnimation(); | 
 |       time += SingleDimensionPhysics._MS_PER_FRAME; | 
 |       if (_isStepNecessary()) { | 
 |         _moves.add(new _Move( | 
 |             _nextX, _nextY, physicsX.velocity, physicsY.velocity, time)); | 
 |         _previousOffset.y = _nextY; | 
 |         _previousOffset.x = _nextX; | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   bool get decelerating => _decelerating; | 
 |   num get decelerationFactor => _customDecelerationFactor; | 
 |  | 
 |   /** | 
 |    * Checks whether or not an animation step is necessary or not. Animations | 
 |    * steps are not necessary when the velocity gets so low that in several | 
 |    * frames the offset is the same. | 
 |    * Returns true if there is movement to be done in the next frame. | 
 |    */ | 
 |   bool _isStepNecessary() { | 
 |     return _nextY != _previousOffset.y || _nextX != _previousOffset.x; | 
 |   } | 
 |  | 
 |   /** | 
 |    * The [TouchHandler] requires this function but we don't need to do | 
 |    * anything here. | 
 |    */ | 
 |   void onTransitionEnd() {} | 
 |  | 
 |   Coordinate calculateVelocity(Coordinate start_, Coordinate target, | 
 |       [num decelerationFactor = null]) { | 
 |     return new Coordinate( | 
 |         physicsX.solve(start_.x, target.x, decelerationFactor), | 
 |         physicsY.solve(start_.y, target.y, decelerationFactor)); | 
 |   } | 
 |  | 
 |   bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord, | 
 |       Coordinate initialOffset, | 
 |       [num decelerationFactor = null]) { | 
 |     _customDecelerationFactor = _defaultDecelerationFactor; | 
 |     if (decelerationFactor != null) { | 
 |       _customDecelerationFactor = decelerationFactor; | 
 |     } | 
 |  | 
 |     if (_stepTimeout != null) { | 
 |       Env.cancelRequestAnimationFrame(_stepTimeout); | 
 |       _stepTimeout = null; | 
 |     } | 
 |  | 
 |     assert(_stepTimeout == null); | 
 |     assert(minCoord.x <= maxCoord.x); | 
 |     assert(minCoord.y <= maxCoord.y); | 
 |     _previousOffset = initialOffset.clone(); | 
 |     physicsX.configure(minCoord.x, maxCoord.x, initialOffset.x, | 
 |         _customDecelerationFactor, velocity.x); | 
 |     physicsY.configure(minCoord.y, maxCoord.y, initialOffset.y, | 
 |         _customDecelerationFactor, velocity.y); | 
 |     if (!physicsX.isDone() || !physicsY.isDone()) { | 
 |       _calculateMoves(); | 
 |       if (!_moves.isEmpty) { | 
 |         num firstTime = _moves.first.time; | 
 |         _stepTimeout = Env.requestAnimationFrame(_step, null, firstTime); | 
 |         _decelerating = true; | 
 |         return true; | 
 |       } | 
 |     } | 
 |     _decelerating = false; | 
 |     return false; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Update the x, y values of the element offset without actually moving the | 
 |    * element. This is done because we store decimal values for x, y for | 
 |    * precision, but moving is only required when the offset is changed by at | 
 |    * least a whole integer. | 
 |    */ | 
 |   void _stepWithoutAnimation() { | 
 |     physicsX.step(); | 
 |     physicsY.step(); | 
 |     _nextX = physicsX._currentOffset.round(); | 
 |     _nextY = physicsY._currentOffset.round(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Calculate the next offset of the element and animate it to that position. | 
 |    */ | 
 |   void _step(num timestamp) { | 
 |     _stepTimeout = null; | 
 |  | 
 |     // Prune moves that are more than 1 frame behind when we have more | 
 |     // available moves. | 
 |     num lastEpoch = timestamp - SingleDimensionPhysics._MS_PER_FRAME; | 
 |     while (!_moves.isEmpty && | 
 |         !identical(_moves.first, _moves.last) && | 
 |         _moves.first.time < lastEpoch) { | 
 |       _moves.removeFirst(); | 
 |     } | 
 |  | 
 |     if (!_moves.isEmpty) { | 
 |       final move = _moves.removeFirst(); | 
 |       _delegate.onDecelerate(move.x, move.y); | 
 |       if (!_moves.isEmpty) { | 
 |         num nextTime = _moves.first.time; | 
 |         assert(_stepTimeout == null); | 
 |         _stepTimeout = Env.requestAnimationFrame(_step, null, nextTime); | 
 |       } else { | 
 |         stop(); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   void abort() { | 
 |     _decelerating = false; | 
 |     _moves.clear(); | 
 |     if (_stepTimeout != null) { | 
 |       Env.cancelRequestAnimationFrame(_stepTimeout); | 
 |       _stepTimeout = null; | 
 |     } | 
 |   } | 
 |  | 
 |   Coordinate stop() { | 
 |     final wasDecelerating = _decelerating; | 
 |     _decelerating = false; | 
 |     Coordinate velocity; | 
 |     if (!_moves.isEmpty) { | 
 |       final move = _moves.first; | 
 |       // This is a workaround for the ugly hacks that get applied when a user | 
 |       // passed a velocity in to this Momentum implementation. | 
 |       num velocityScale = SingleDimensionPhysics._MS_PER_FRAME * | 
 |           SingleDimensionPhysics._INITIAL_VELOCITY_BOOST_FACTOR; | 
 |       velocity = | 
 |           new Coordinate(move.vx / velocityScale, move.vy / velocityScale); | 
 |     } else { | 
 |       velocity = new Coordinate(0, 0); | 
 |     } | 
 |     _moves.clear(); | 
 |     if (_stepTimeout != null) { | 
 |       Env.cancelRequestAnimationFrame(_stepTimeout); | 
 |       _stepTimeout = null; | 
 |     } | 
 |     if (wasDecelerating) { | 
 |       _delegate.onDecelerationEnd(); | 
 |     } | 
 |     return velocity; | 
 |   } | 
 |  | 
 |   Coordinate get destination { | 
 |     if (!_moves.isEmpty) { | 
 |       final lastMove = _moves.last; | 
 |       return new Coordinate(lastMove.x, lastMove.y); | 
 |     } else { | 
 |       return null; | 
 |     } | 
 |   } | 
 | } |