201 lines
7.1 KiB
Dart
201 lines
7.1 KiB
Dart
// 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)})';
|
|
}
|