// 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, }) { var guess = initialGuess; for (var 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)})'; }