velocity_tracker.dart 6.71 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Ian Hickson's avatar
Ian Hickson committed
5
import 'dart:ui' show Point, Offset;
6 7 8

import 'lsq_solver.dart';

Ian Hickson's avatar
Ian Hickson committed
9
export 'dart:ui' show Point, Offset;
10

11 12 13 14 15 16 17 18
class _Estimate {
  const _Estimate({ this.xCoefficients, this.yCoefficients, this.time, this.degree, this.confidence });

  final List<double> xCoefficients;
  final List<double> yCoefficients;
  final Duration time;
  final int degree;
  final double confidence;
19 20
}

21
abstract class _VelocityTrackerStrategy {
Ian Hickson's avatar
Ian Hickson committed
22
  void addMovement(Duration timeStamp, Point position);
23
  _Estimate getEstimate();
24 25 26
  void clear();
}

27
class _Movement {
Ian Hickson's avatar
Ian Hickson committed
28 29
  Duration eventTime = Duration.ZERO;
  Point position = Point.origin;
30 31
}

32
class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
33
  static const int kHistorySize = 20;
34
  static const int kHorizonMilliseconds = 100;
35

36
  _LeastSquaresVelocityTrackerStrategy(this.degree);
37 38

  final int degree;
39 40
  final List<_Movement> _movements = new List<_Movement>(kHistorySize);
  int _index = 0;
41

42
  @override
Ian Hickson's avatar
Ian Hickson committed
43 44 45
  void addMovement(Duration timeStamp, Point position) {
    _index += 1;
    if (_index == kHistorySize)
46
      _index = 0;
47 48
    _Movement movement = _getMovement(_index);
    movement.eventTime = timeStamp;
Ian Hickson's avatar
Ian Hickson committed
49
    movement.position = position;
50 51
  }

52
  @override
53
  _Estimate getEstimate() {
54 55 56 57 58 59 60
    // Iterate over movement samples in reverse time order and collect samples.
    List<double> x = new List<double>();
    List<double> y = new List<double>();
    List<double> w = new List<double>();
    List<double> time = new List<double>();
    int m = 0;
    int index = _index;
61
    _Movement newestMovement = _getMovement(index);
62
    do {
63
      _Movement movement = _getMovement(index);
64

65
      double age = (newestMovement.eventTime - movement.eventTime).inMilliseconds.toDouble();
66
      if (age > kHorizonMilliseconds)
67 68
        break;

Ian Hickson's avatar
Ian Hickson committed
69
      Point position = movement.position;
70 71
      x.add(position.x);
      y.add(position.y);
72
      w.add(1.0);
73 74 75
      time.add(-age);
      index = (index == 0 ? kHistorySize : index) - 1;

Ian Hickson's avatar
Ian Hickson committed
76 77 78 79
      m += 1;
    } while (m < kHistorySize);

    if (m == 0) // because we broke out of the loop above after age > kHorizonMilliseconds
80
      return null; // no data
81 82 83 84 85 86 87 88 89 90 91 92 93

    // Calculate a least squares polynomial fit.
    int n = degree;
    if (n > m - 1)
      n = m - 1;

    if (n >= 1) {
      LeastSquaresSolver xSolver = new LeastSquaresSolver(time, x, w);
      PolynomialFit xFit = xSolver.solve(n);
      if (xFit != null) {
        LeastSquaresSolver ySolver = new LeastSquaresSolver(time, y, w);
        PolynomialFit yFit = ySolver.solve(n);
        if (yFit != null) {
94 95 96 97 98 99 100
          return new _Estimate(
            xCoefficients: xFit.coefficients,
            yCoefficients: yFit.coefficients,
            time: newestMovement.eventTime,
            degree: n,
            confidence: xFit.confidence * yFit.confidence
          );
101 102 103 104 105 106
        }
      }
    }

    // No velocity data available for this pointer, but we do have its current
    // position.
107 108 109 110 111 112 113
    return new _Estimate(
      xCoefficients: <double>[ x[0] ],
      yCoefficients: <double>[ y[0] ],
      time: newestMovement.eventTime,
      degree: 0,
      confidence: 1.0
    );
114 115
  }

116
  @override
117 118 119 120
  void clear() {
    _index = -1;
  }

121
  _Movement _getMovement(int i) {
Ian Hickson's avatar
Ian Hickson committed
122 123 124 125 126 127
    _Movement result = _movements[i];
    if (result == null) {
      result = new _Movement();
      _movements[i] = result;
    }
    return result;
128 129 130 131
  }

}

132 133
/// A velocity in two dimensions.
class Velocity {
134 135 136
  /// Creates a velocity.
  ///
  /// The [pixelsPerSecond] argument must not be null.
137 138 139 140 141 142 143 144
  const Velocity({ this.pixelsPerSecond });

  /// A velocity that isn't moving at all.
  static const Velocity zero = const Velocity(pixelsPerSecond: Offset.zero);

  /// The number of pixels per second of velocity in the x and y directions.
  final Offset pixelsPerSecond;

145
  /// Return the negation of a velocity.
146
  Velocity operator -() => new Velocity(pixelsPerSecond: -pixelsPerSecond);
147 148

  /// Return the difference of two velocities.
149 150 151 152
  Velocity operator -(Velocity other) {
    return new Velocity(
        pixelsPerSecond: pixelsPerSecond - other.pixelsPerSecond);
  }
153 154

  /// Return the sum of two velocities.
155 156 157 158 159
  Velocity operator +(Velocity other) {
    return new Velocity(
        pixelsPerSecond: pixelsPerSecond + other.pixelsPerSecond);
  }

160
  @override
161 162 163 164 165 166 167
  bool operator ==(dynamic other) {
    if (other is! Velocity)
      return false;
    final Velocity typedOther = other;
    return pixelsPerSecond == typedOther.pixelsPerSecond;
  }

168
  @override
169 170
  int get hashCode => pixelsPerSecond.hashCode;

171
  @override
172 173 174
  String toString() => 'Velocity(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})';
}

175 176 177 178 179 180 181 182 183 184 185
/// Computes a pointer velocity based on data from PointerMove events.
///
/// The input data is provided by calling addPosition(). Adding data
/// is cheap.
///
/// To obtain a velocity, call getVelocity(). This will compute the
/// velocity based on the data added so far. Only call this when you
/// need to use the velocity, as it is comparatively expensive.
///
/// The quality of the velocity estimation will be better if more data
/// points have been received.
186
class VelocityTracker {
187 188 189 190

  /// The maximum length of time between two move events to allow
  /// before assuming the pointer stopped.
  static const Duration kAssumePointerMoveStoppedTime = const Duration(milliseconds: 40);
191

192
  /// Creates a velocity tracker.
193
  VelocityTracker() : _strategy = _createStrategy();
194

195
  Duration _lastTimeStamp = const Duration();
196
  _VelocityTrackerStrategy _strategy;
197

198 199 200 201
  /// Add a given position corresponding to a specific time.
  ///
  /// If [kAssumePointerMoveStoppedTime] has elapsed since the last
  /// call, then earlier data will be discarded.
Ian Hickson's avatar
Ian Hickson committed
202
  void addPosition(Duration timeStamp, Point position) {
203
    if (timeStamp - _lastTimeStamp >= kAssumePointerMoveStoppedTime)
204 205
      _strategy.clear();
    _lastTimeStamp = timeStamp;
Ian Hickson's avatar
Ian Hickson committed
206
    _strategy.addMovement(timeStamp, position);
207 208
  }

209 210 211 212 213 214 215
  /// Computes the velocity of the pointer at the time of the last
  /// provided data point.
  ///
  /// This can be expensive. Only call this when you need the velocity.
  ///
  /// getVelocity() will return null if no estimate is available or if
  /// the velocity is zero.
216
  Velocity getVelocity() {
217 218
    _Estimate estimate = _strategy.getEstimate();
    if (estimate != null && estimate.degree >= 1) {
219 220 221 222 223
      return new Velocity(
        pixelsPerSecond: new Offset( // convert from pixels/ms to pixels/s
          estimate.xCoefficients[1] * 1000,
          estimate.yCoefficients[1] * 1000
        )
224 225
      );
    }
Ian Hickson's avatar
Ian Hickson committed
226
    return null;
227 228
  }

229
  static _VelocityTrackerStrategy _createStrategy() {
230
    return new _LeastSquaresVelocityTrackerStrategy(2);
231 232
  }
}