// 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. import 'dart:ui' show Point, Offset; import 'lsq_solver.dart'; export 'dart:ui' show Point, Offset; 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; @override String toString() { return 'Estimate(xCoefficients: $xCoefficients, ' 'yCoefficients: $yCoefficients, ' 'time: $time, ' 'degree: $degree, ' 'confidence: $confidence)'; } } abstract class _VelocityTrackerStrategy { void addMovement(Duration timeStamp, Point position); _Estimate getEstimate(); void clear(); } enum _Weighting { weightingNone, weightingDelta, weightingCentral, weightingRecent } class _Movement { Duration eventTime = Duration.ZERO; Point position = Point.origin; } typedef double _WeightChooser(int index); class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy { static const int kHistorySize = 20; static const int kHorizonMilliseconds = 100; _LeastSquaresVelocityTrackerStrategy(this.degree, _Weighting weighting) : _index = 0, _movements = new List<_Movement>(kHistorySize) { switch (weighting) { case _Weighting.weightingNone: _chooseWeight = null; break; case _Weighting.weightingDelta: _chooseWeight = _weightDelta; break; case _Weighting.weightingCentral: _chooseWeight = _weightCentral; break; case _Weighting.weightingRecent: _chooseWeight = _weightRecent; break; } } final int degree; final List<_Movement> _movements; _WeightChooser _chooseWeight; int _index; @override void addMovement(Duration timeStamp, Point position) { _index += 1; if (_index == kHistorySize) _index = 0; _Movement movement = _getMovement(_index); movement.eventTime = timeStamp; movement.position = position; } @override _Estimate getEstimate() { // 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; _Movement newestMovement = _getMovement(index); do { _Movement movement = _getMovement(index); double age = (newestMovement.eventTime - movement.eventTime).inMilliseconds.toDouble(); if (age > kHorizonMilliseconds) break; Point position = movement.position; x.add(position.x); y.add(position.y); w.add(_chooseWeight != null ? _chooseWeight(index) : 1.0); time.add(-age); index = (index == 0 ? kHistorySize : index) - 1; m += 1; } while (m < kHistorySize); if (m == 0) // because we broke out of the loop above after age > kHorizonMilliseconds return null; // no data // 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) { return new _Estimate( xCoefficients: xFit.coefficients, yCoefficients: yFit.coefficients, time: newestMovement.eventTime, degree: n, confidence: xFit.confidence * yFit.confidence ); } } } // No velocity data available for this pointer, but we do have its current // position. return new _Estimate( xCoefficients: <double>[ x[0] ], yCoefficients: <double>[ y[0] ], time: newestMovement.eventTime, degree: 0, confidence: 1.0 ); } @override void clear() { _index = -1; } double _weightDelta(int index) { // Weight points based on how much time elapsed between them and the next // point so that points that "cover" a shorter time span are weighed less. // delta 0ms: 0.5 // delta 10ms: 1.0 if (index == _index) return 1.0; int nextIndex = (index + 1) % kHistorySize; int deltaMilliseconds = (_movements[nextIndex].eventTime - _movements[index].eventTime).inMilliseconds; if (deltaMilliseconds < 0) return 0.5; if (deltaMilliseconds < 10) return 0.5 + deltaMilliseconds * 0.05; return 1.0; } double _weightCentral(int index) { // Weight points based on their age, weighing very recent and very old // points less. // age 0ms: 0.5 // age 10ms: 1.0 // age 50ms: 1.0 // age 60ms: 0.5 int ageMilliseconds = (_movements[_index].eventTime - _movements[index].eventTime).inMilliseconds; if (ageMilliseconds < 0) return 0.5; if (ageMilliseconds < 10) return 0.5 + ageMilliseconds * 0.05; if (ageMilliseconds < 50) return 1.0; if (ageMilliseconds < 60) return 0.5 + (60 - ageMilliseconds) * 0.05; return 0.5; } double _weightRecent(int index) { // Weight points based on their age, weighing older points less. // age 0ms: 1.0 // age 50ms: 1.0 // age 100ms: 0.5 int ageMilliseconds = (_movements[_index].eventTime - _movements[index].eventTime).inMilliseconds; if (ageMilliseconds < 50) return 1.0; if (ageMilliseconds < 100) return 0.5 + (100 - ageMilliseconds) * 0.01; return 0.5; } _Movement _getMovement(int i) { _Movement result = _movements[i]; if (result == null) { result = new _Movement(); _movements[i] = result; } return result; } } /// A velocity in two dimensions. class Velocity { 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; Velocity operator -() => new Velocity(pixelsPerSecond: -pixelsPerSecond); Velocity operator -(Velocity other) { return new Velocity( pixelsPerSecond: pixelsPerSecond - other.pixelsPerSecond); } Velocity operator +(Velocity other) { return new Velocity( pixelsPerSecond: pixelsPerSecond + other.pixelsPerSecond); } @override bool operator ==(dynamic other) { if (other is! Velocity) return false; final Velocity typedOther = other; return pixelsPerSecond == typedOther.pixelsPerSecond; } @override int get hashCode => pixelsPerSecond.hashCode; @override String toString() => 'Velocity(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})'; } /// 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. class VelocityTracker { /// The maximum length of time between two move events to allow /// before assuming the pointer stopped. static const Duration kAssumePointerMoveStoppedTime = const Duration(milliseconds: 40); VelocityTracker() : _strategy = _createStrategy(); Duration _lastTimeStamp = const Duration(); _VelocityTrackerStrategy _strategy; /// Add a given position corresponding to a specific time. /// /// If [kAssumePointerMoveStoppedTime] has elapsed since the last /// call, then earlier data will be discarded. void addPosition(Duration timeStamp, Point position) { if (timeStamp - _lastTimeStamp >= kAssumePointerMoveStoppedTime) _strategy.clear(); _lastTimeStamp = timeStamp; _strategy.addMovement(timeStamp, position); } /// 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. Velocity getVelocity() { _Estimate estimate = _strategy.getEstimate(); if (estimate != null && estimate.degree >= 1) { return new Velocity( pixelsPerSecond: new Offset( // convert from pixels/ms to pixels/s estimate.xCoefficients[1] * 1000, estimate.yCoefficients[1] * 1000 ) ); } return null; } static _VelocityTrackerStrategy _createStrategy() { return new _LeastSquaresVelocityTrackerStrategy(2, _Weighting.weightingNone); } }