friction_simulation.dart 7.06 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math' as math;

7 8
import 'package:flutter/foundation.dart';

9
import 'simulation.dart';
10 11

export 'tolerance.dart' show Tolerance;
12

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
/// Numerically determine the input value which produces output value [target]
/// for a function [f], given its first-derivative [df].
double _newtonsMethod({
  required double initialGuess,
  required double target,
  required double Function(double) f,
  required double Function(double) df,
  required int iterations
}) {
  double guess = initialGuess;
  for (int i = 0; i < iterations; i++) {
    guess = guess - (f(guess) - target) / df(guess);
  }
  return guess;
}

Ian Hickson's avatar
Ian Hickson committed
29 30 31 32 33 34
/// A simulation that applies a drag to slow a particle down.
///
/// Models a particle affected by fluid drag, e.g. air resistance.
///
/// The simulation ends when the velocity of the particle drops to zero (within
/// the current velocity [tolerance]).
35
class FrictionSimulation extends Simulation {
Ian Hickson's avatar
Ian Hickson committed
36
  /// Creates a [FrictionSimulation] with the given arguments, namely: the fluid
37 38
  /// drag coefficient _cₓ_, a unitless value; the initial position _x₀_, in the same
  /// length units as used for [x]; and the initial velocity _dx₀_, in the same
Ian Hickson's avatar
Ian Hickson committed
39
  /// velocity units as used for [dx].
40 41 42 43
  FrictionSimulation(
    double drag,
    double position,
    double velocity, {
44
    super.tolerance,
45
    double constantDeceleration = 0
46 47 48
  }) : _drag = drag,
       _dragLog = math.log(drag),
       _x = position,
49 50 51 52 53 54 55 56 57 58
       _v = velocity,
       _constantDeceleration = constantDeceleration * velocity.sign {
      _finalTime = _newtonsMethod(
        initialGuess: 0,
        target: 0,
        f: dx,
        df: (double time) => (_v * math.pow(_drag, time) * _dragLog) - _constantDeceleration,
        iterations: 10
      );
    }
Ian Hickson's avatar
Ian Hickson committed
59

60
  /// Creates a new friction simulation with its fluid drag coefficient (_cₓ_) set so
Ian Hickson's avatar
Ian Hickson committed
61 62 63 64 65 66 67 68 69 70
  /// as to ensure that the simulation starts and ends at the specified
  /// positions and velocities.
  ///
  /// The positions must use the same units as expected from [x], and the
  /// velocities must use the same units as expected from [dx].
  ///
  /// The sign of the start and end velocities must be the same, the magnitude
  /// of the start velocity must be greater than the magnitude of the end
  /// velocity, and the velocities must be in the direction appropriate for the
  /// particle to start from the start position and reach the end position.
Ian Hickson's avatar
Ian Hickson committed
71
  factory FrictionSimulation.through(double startPosition, double endPosition, double startVelocity, double endVelocity) {
72
    assert(startVelocity == 0.0 || endVelocity == 0.0 || startVelocity.sign == endVelocity.sign);
Ian Hickson's avatar
Ian Hickson committed
73 74
    assert(startVelocity.abs() >= endVelocity.abs());
    assert((endPosition - startPosition).sign == startVelocity.sign);
75
    return FrictionSimulation(
Ian Hickson's avatar
Ian Hickson committed
76 77
      _dragFor(startPosition, endPosition, startVelocity, endVelocity),
      startPosition,
78
      startVelocity,
79
      tolerance: Tolerance(velocity: endVelocity.abs()),
80
    );
Ian Hickson's avatar
Ian Hickson committed
81 82
  }

83
  final double _drag;
84
  final double _dragLog;
85 86
  final double _x;
  final double _v;
87 88 89 90 91 92
  final double _constantDeceleration;
  // The time at which the simulation should be stopped.
  // This is needed when constantDeceleration is not zero (on Desktop), when
  // using the pure friction simulation, acceleration naturally reduces to zero
  // and creates a stopping point.
  double _finalTime = double.infinity; // needs to be infinity for newtonsMethod call in constructor.
93

94 95 96 97 98 99 100 101
  // Return the drag value for a FrictionSimulation whose x() and dx() values pass
  // through the specified start and end position/velocity values.
  //
  // Total time to reach endVelocity is just: (log(endVelocity) / log(startVelocity)) / log(_drag)
  // or (log(v1) - log(v0)) / log(D), given v = v0 * D^t per the dx() function below.
  // Solving for D given x(time) is trickier. Algebra courtesy of Wolfram Alpha:
  // x1 = x0 + (v0 * D^((log(v1) - log(v0)) / log(D))) / log(D) - v0 / log(D), find D
  static double _dragFor(double startPosition, double endPosition, double startVelocity, double endVelocity) {
102
    return math.pow(math.e, (startVelocity - endVelocity) / (startPosition - endPosition)) as double;
103 104
  }

105
  @override
106 107 108 109 110 111
  double x(double time) {
    if (time > _finalTime) {
      return finalX;
    }
    return _x + _v * math.pow(_drag, time) / _dragLog - _v / _dragLog - ((_constantDeceleration / 2) * time * time);
  }
112

113
  @override
114 115 116 117 118 119
  double dx(double time) {
    if (time > _finalTime) {
      return 0;
    }
    return _v * math.pow(_drag, time) - _constantDeceleration * time;
  }
120

121
  /// The value of [x] at `double.infinity`.
122 123 124 125 126 127
  double get finalX {
    if (_constantDeceleration == 0) {
      return _x - _v / _dragLog;
    }
    return x(_finalTime);
  }
128 129 130

  /// The time at which the value of `x(time)` will equal [x].
  ///
131
  /// Returns `double.infinity` if the simulation will never reach [x].
132
  double timeAtX(double x) {
133
    if (x == _x) {
134
      return 0.0;
135 136
    }
    if (_v == 0.0 || (_v > 0 ? (x < _x || x > finalX) : (x > _x || x < finalX))) {
137
      return double.infinity;
138
    }
139 140 141 142 143 144 145
    return _newtonsMethod(
      target: x,
      initialGuess: 0,
      f: this.x,
      df: dx,
      iterations: 10
    );
146 147
  }

148
  @override
149 150 151
  bool isDone(double time) {
    return dx(time).abs() < tolerance.velocity;
  }
152 153 154

  @override
  String toString() => '${objectRuntimeType(this, 'FrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)})';
155
}
156

157
/// A [FrictionSimulation] that clamps the modeled particle to a specific range
Ian Hickson's avatar
Ian Hickson committed
158
/// of values.
159 160 161
///
/// Only the position is clamped. The velocity [dx] will continue to report
/// unbounded simulated velocities once the particle has reached the bounds.
162
class BoundedFrictionSimulation extends FrictionSimulation {
Ian Hickson's avatar
Ian Hickson committed
163
  /// Creates a [BoundedFrictionSimulation] with the given arguments, namely:
164 165
  /// the fluid drag coefficient _cₓ_, a unitless value; the initial position _x₀_, in the
  /// same length units as used for [x]; the initial velocity _dx₀_, in the same
Ian Hickson's avatar
Ian Hickson committed
166 167 168 169
  /// velocity units as used for [dx], the minimum value for the position, and
  /// the maximum value for the position. The minimum and maximum values must be
  /// in the same units as the initial position, and the initial position must
  /// be within the given range.
170
  BoundedFrictionSimulation(
171 172 173
    super.drag,
    super.position,
    super.velocity,
Hixie's avatar
Hixie committed
174
    this._minX,
175
    this._maxX,
176
  ) : assert(clampDouble(position, _minX, _maxX) == position);
177 178 179 180

  final double _minX;
  final double _maxX;

181
  @override
182
  double x(double time) {
183
    return clampDouble(super.x(time), _minX, _maxX);
184 185
  }

186
  @override
187 188 189 190 191
  bool isDone(double time) {
    return super.isDone(time) ||
      (x(time) - _minX).abs() < tolerance.distance ||
      (x(time) - _maxX).abs() < tolerance.distance;
  }
192 193 194

  @override
  String toString() => '${objectRuntimeType(this, 'BoundedFrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)}, x: ${_minX.toStringAsFixed(1)}..${_maxX.toStringAsFixed(1)})';
195
}