| // Copyright 2014 The Flutter 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:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'simulation.dart'; |
| |
| export 'tolerance.dart' show Tolerance; |
| |
| /// 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; |
| } |
| |
| /// 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]). |
| class FrictionSimulation extends Simulation { |
| /// Creates a [FrictionSimulation] with the given arguments, namely: the fluid |
| /// 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 |
| /// velocity units as used for [dx]. |
| FrictionSimulation( |
| double drag, |
| double position, |
| double velocity, { |
| super.tolerance, |
| double constantDeceleration = 0 |
| }) : _drag = drag, |
| _dragLog = math.log(drag), |
| _x = position, |
| _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 |
| ); |
| } |
| |
| /// Creates a new friction simulation with its fluid drag coefficient (_cₓ_) set so |
| /// 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. |
| factory FrictionSimulation.through(double startPosition, double endPosition, double startVelocity, double endVelocity) { |
| assert(startVelocity == 0.0 || endVelocity == 0.0 || startVelocity.sign == endVelocity.sign); |
| assert(startVelocity.abs() >= endVelocity.abs()); |
| assert((endPosition - startPosition).sign == startVelocity.sign); |
| return FrictionSimulation( |
| _dragFor(startPosition, endPosition, startVelocity, endVelocity), |
| startPosition, |
| startVelocity, |
| tolerance: Tolerance(velocity: endVelocity.abs()), |
| ); |
| } |
| |
| final double _drag; |
| final double _dragLog; |
| final double _x; |
| final double _v; |
| 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. |
| |
| // 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) { |
| return math.pow(math.e, (startVelocity - endVelocity) / (startPosition - endPosition)) as double; |
| } |
| |
| @override |
| double x(double time) { |
| if (time > _finalTime) { |
| return finalX; |
| } |
| return _x + _v * math.pow(_drag, time) / _dragLog - _v / _dragLog - ((_constantDeceleration / 2) * time * time); |
| } |
| |
| @override |
| double dx(double time) { |
| if (time > _finalTime) { |
| return 0; |
| } |
| return _v * math.pow(_drag, time) - _constantDeceleration * time; |
| } |
| |
| /// The value of [x] at `double.infinity`. |
| double get finalX { |
| if (_constantDeceleration == 0) { |
| return _x - _v / _dragLog; |
| } |
| return x(_finalTime); |
| } |
| |
| /// The time at which the value of `x(time)` will equal [x]. |
| /// |
| /// Returns `double.infinity` if the simulation will never reach [x]. |
| double timeAtX(double x) { |
| if (x == _x) { |
| return 0.0; |
| } |
| if (_v == 0.0 || (_v > 0 ? (x < _x || x > finalX) : (x > _x || x < finalX))) { |
| return double.infinity; |
| } |
| return _newtonsMethod( |
| target: x, |
| initialGuess: 0, |
| f: this.x, |
| df: dx, |
| iterations: 10 |
| ); |
| } |
| |
| @override |
| bool isDone(double time) { |
| return dx(time).abs() < tolerance.velocity; |
| } |
| |
| @override |
| String toString() => '${objectRuntimeType(this, 'FrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)})'; |
| } |
| |
| /// A [FrictionSimulation] that clamps the modeled particle to a specific range |
| /// of values. |
| /// |
| /// Only the position is clamped. The velocity [dx] will continue to report |
| /// unbounded simulated velocities once the particle has reached the bounds. |
| class BoundedFrictionSimulation extends FrictionSimulation { |
| /// Creates a [BoundedFrictionSimulation] with the given arguments, namely: |
| /// 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 |
| /// 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. |
| BoundedFrictionSimulation( |
| super.drag, |
| super.position, |
| super.velocity, |
| this._minX, |
| this._maxX, |
| ) : assert(clampDouble(position, _minX, _maxX) == position); |
| |
| final double _minX; |
| final double _maxX; |
| |
| @override |
| double x(double time) { |
| return clampDouble(super.x(time), _minX, _maxX); |
| } |
| |
| @override |
| bool isDone(double time) { |
| return super.isDone(time) || |
| (x(time) - _minX).abs() < tolerance.distance || |
| (x(time) - _maxX).abs() < tolerance.distance; |
| } |
| |
| @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)})'; |
| } |