Commit daa0d2df authored by Ian Hickson's avatar Ian Hickson

Document newton more.

parent 79cfe1e0
......@@ -20,8 +20,9 @@ import 'scroll_behavior.dart';
/// The accuracy to which scrolling is computed.
final Tolerance kPixelScrollTolerance = new Tolerance(
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
// TODO(ianh): Handle the case of the device pixel ratio changing.
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
typedef void ScrollListener(double scrollOffset);
......
......@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Simple Physics Simulations for Dart. Springs, friction, gravity, etc.
/// Simple one-dimensional physics simulations, such as springs, friction, and
/// gravity, for use in user interface animations.
library newton;
export 'src/clamped_simulation.dart';
......
......@@ -4,7 +4,23 @@
import 'simulation.dart';
/// A simulation that applies limits to another simulation.
///
/// The limits are only applied to the other simulation's outputs. For example,
/// if a maximum position was applied to a gravity simulation with the
/// particle's initial velocity being up, and the accerelation being down, and
/// the maximum position being between the initial position and the curve's
/// apogee, then the particle would return to its initial position in the same
/// amount of time as it would have if the maximum had not been applied; the
/// difference would just be that the position would be reported as pinned to
/// the maximum value for the times that it would otherwise have been reported
/// as higher.
class ClampedSimulation extends Simulation {
/// Creates a [ClampedSimulation] that clamps the given simulation.
///
/// The named arguments specify the ranges for the clamping behavior, as
/// applied to [x] and [dx].
ClampedSimulation(this.simulation, {
this.xMin: double.NEGATIVE_INFINITY,
this.xMax: double.INFINITY,
......@@ -16,10 +32,20 @@ class ClampedSimulation extends Simulation {
assert(dxMax >= dxMin);
}
/// The simulation being clamped. Calls to [x], [dx], and [isDone] are
/// forwarded to the simulation.
final Simulation simulation;
/// The minimum to apply to [x].
final double xMin;
/// The maximum to apply to [x].
final double xMax;
/// The minimum to apply to [dx].
final double dxMin;
/// The maximum to apply to [dx].
final double dxMax;
@override
......
......@@ -7,21 +7,43 @@ import 'dart:math' as math;
import 'simulation.dart';
import 'tolerance.dart';
/// 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, a unitless value; the initial position, in the same
/// length units as used for [x]; and the initial velocity, in the same
/// velocity units as used for [dx].
FrictionSimulation(double drag, double position, double velocity)
: _drag = drag,
_dragLog = math.log(drag),
_x = position,
_v = velocity;
// A friction simulation that starts and ends at the specified positions
// and velocities.
/// Creates a new friction simulation with its fluid drag coefficient 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.sign == endVelocity.sign);
assert(startVelocity.abs() >= endVelocity.abs());
assert((endPosition - startPosition).sign == startVelocity.sign);
return new FrictionSimulation(
_dragFor(startPosition, endPosition, startVelocity, endVelocity),
startPosition,
startVelocity)
.. tolerance = new Tolerance(velocity: endVelocity.abs());
startVelocity
)..tolerance = new Tolerance(velocity: endVelocity.abs());
}
final double _drag;
......@@ -50,14 +72,25 @@ class FrictionSimulation extends Simulation {
bool isDone(double time) => dx(time).abs() < tolerance.velocity;
}
/// A [FrictionSimulation] that clamps the modelled particle to a specific range
/// of values.
class BoundedFrictionSimulation extends FrictionSimulation {
/// Creates a [BoundedFrictionSimulation] with the given arguments, namely:
/// the fluid drag coefficient, a unitless value; the initial position, in the
/// same length units as used for [x]; the initial velocity, 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(
double drag,
double position,
double velocity,
this._minX,
this._maxX
) : super(drag, position, velocity);
) : super(drag, position, velocity) {
assert(position.clamp(_minX, _maxX) == position);
}
final double _minX;
final double _maxX;
......
......@@ -4,19 +4,46 @@
import 'simulation.dart';
/// A simulation that applies a constant accelerating force.
///
/// Models a particle that follows Newton's second law of motion. The simulation
/// ends when the position reaches a defined point.
class GravitySimulation extends Simulation {
/// Creates a [GravitySimulation] using the given arguments, which are,
/// respectively: an acceleration that is to be applied continually over time;
/// an initial position relative to an origin; the magnitude of the distance
/// from that origin beyond which (in either direction) to consider the
/// simulation to be "done", which must be positive; and an initial velocity.
///
/// The initial position and maximum distance are measured in arbitrary length
/// units L from an arbitrary origin. The units will match those used for [x].
///
/// The time unit T used for the arguments to [x], [dx], and [isDone],
/// combined with the aforementioned length unit, together determine the units
/// that must be used for the velocity and acceleration arguments: L/T and
/// L/T² respectively. The same units of velocity are used for the velocity
/// obtained from [dx].
GravitySimulation(
double acceleration,
double distance,
double endDistance,
double velocity
) : _a = acceleration,
_x = distance,
_v = velocity,
_end = endDistance {
assert(acceleration != null);
assert(distance != null);
assert(velocity != null);
assert(endDistance != null);
assert(endDistance >= 0);
}
final double _x;
final double _v;
final double _a;
final double _end;
GravitySimulation(
double acceleration, double distance, double endDistance, double velocity)
: _a = acceleration,
_x = distance,
_v = velocity,
_end = endDistance;
@override
double x(double time) => _x + _v * time + 0.5 * _a * time * time;
......
......@@ -7,10 +7,25 @@ import 'simulation_group.dart';
import 'simulation.dart';
import 'spring_simulation.dart';
/// Composite simulation for scrollable interfaces.
///
/// Simulates kinetic scrolling behavior between a leading and trailing
/// boundary. Friction is applied within the extends and a spring action applied
/// at the boundaries. This simulation can only step forward.
/// boundary. Friction is applied within the extents and a spring action is
/// applied at the boundaries. This simulation can only step forward.
class ScrollSimulation extends SimulationGroup {
/// Creates a [ScrollSimulation] with the given parameters.
///
/// The position and velocity arguments must use the same units as will be
/// expected from the [x] and [dx] methods respectively.
///
/// The leading and trailing extents must use the unit of length, the same
/// unit as used for the position argument and as expected from the [x]
/// method.
///
/// The units used with the provided [SpringDescription] must similarly be
/// consistent with the other arguments.
///
/// The final argument is the coefficient of friction, which is unitless.
ScrollSimulation(
double position,
double velocity,
......
......@@ -4,18 +4,47 @@
import 'tolerance.dart';
/// The base class for all simulations. The user is meant to instantiate an
/// instance of a simulation and query the same for the position and velocity
/// of the body at a given interval.
/// The base class for all simulations.
///
/// A simulation models an object, in a one-dimensional space, on which particular
/// forces are being applied, and exposes:
///
/// * The object's position, [x]
/// * The object's velocity, [dx]
/// * Whether the simulation is "done", [isDone]
///
/// A simulation is generally "done" if the object has, to a given [tolerance],
/// come to a complete rest.
///
/// The [x], [dx], and [isDone] functions take a time argument which specifies
/// the time for which they are to be evaluated. In principle, simulations can
/// be stateless, and thus can be queried with arbitrary times. In practice,
/// however, some simulations are not, and calling any of these functions will
/// advance the simulation to the given time.
///
/// As a general rule, therefore, a simulation should only be queried using
/// times that are equal to or greater than all times previously used for that
/// simulation.
///
/// Simulations do not specify units for distance, velocity, and time. Client
/// should establish a convention and use that convention consistently with all
/// related objects.
abstract class Simulation {
Tolerance tolerance = toleranceDefault;
/// The current position of the object in the simulation
/// The position of the object in the simulation at the given time.
double x(double time);
/// The current velocity of the object in the simulation
/// The velocity of the object in the simulation at the given time.
double dx(double time);
/// Returns if the simulation is done at a given time
/// Whether the simulation is "done" at the given time.
bool isDone(double time);
/// How close to the actual end of the simulation a value at a particular time
/// must be before [isDone] considers the simulation to be "done".
///
/// A simulation with an asymptotic curve would never technically be "done",
/// but once the difference from the value at a particular time and the
/// asymptote itself could not be seen, it would be pointless to continue. The
/// tolerance defines how to determine if the difference could not be seen.
Tolerance tolerance = Tolerance.defaultTolerance;
}
......@@ -6,25 +6,50 @@ import 'simulation.dart';
import 'tolerance.dart';
import 'utils.dart';
/// The abstract base class for all composite simulations. Concrete subclasses
/// must implement the appropriate methods to select the appropriate simulation
/// at a given time interval. The simulation group takes care to call the `step`
/// method at appropriate intervals. If more fine grained control over the the
/// step is necessary, subclasses may override the [x], [dx], and [isDone]
/// methods.
/// Base class for composite simulations.
///
/// Concrete subclasses must implement the [currentSimulation] getter, the
/// [currentIntervalOffset] getter, and the [step] function to select the
/// appropriate simulation at a given time interval. This class implements the
/// [x], [dx], and [isDone] functions by calling the [step] method if necessary
/// and then deferring to the [currentSimulation]'s methods with a time offset
/// by [currentIntervalOffset].
///
/// The tolerance of this simulation is pushed to the simulations that are used
/// by this group as they become active. This mean simulations should not be
/// shared among different groups that are active at the same time.
abstract class SimulationGroup extends Simulation {
/// The currently active simulation
/// The currently active simulation.
///
/// This getter should return the same value until [step] is called and
/// returns true.
Simulation get currentSimulation;
/// The time offset applied to the currently active simulation;
/// The time offset applied to the currently active simulation when deferring
/// [x], [dx], and [isDone] to it.
double get currentIntervalOffset;
/// Called when a significant change in the interval is detected. Subclasses
/// must decide if the the current simulation must be switched (or updated).
/// The result is whether the simulation was switched in this step.
/// must decide if the current simulation must be switched (or updated).
///
/// Must return true if the simulation was switched in this step, otherwise
/// false.
///
/// If this function returns true, then [currentSimulation] must start
/// returning a new value.
bool step(double time);
double _lastStep = -1.0;
void _stepIfNecessary(double time) {
if (nearEqual(_lastStep, time, Tolerance.defaultTolerance.time))
return;
_lastStep = time;
if (step(time))
currentSimulation.tolerance = tolerance;
}
@override
double x(double time) {
_stepIfNecessary(time);
......@@ -37,27 +62,15 @@ abstract class SimulationGroup extends Simulation {
return currentSimulation.dx(time - currentIntervalOffset);
}
@override
void set tolerance(Tolerance t) {
this.currentSimulation.tolerance = t;
super.tolerance = t;
}
@override
bool isDone(double time) {
_stepIfNecessary(time);
return currentSimulation.isDone(time - currentIntervalOffset);
}
double _lastStep = -1.0;
void _stepIfNecessary(double time) {
if (nearEqual(_lastStep, time, toleranceDefault.time)) {
return;
}
_lastStep = time;
if (step(time)) {
this.currentSimulation.tolerance = this.tolerance;
}
@override
void set tolerance(Tolerance t) {
currentSimulation.tolerance = t;
super.tolerance = t;
}
}
......@@ -7,7 +7,135 @@ import 'dart:math' as math;
import 'simulation.dart';
import 'utils.dart';
enum SpringType { unknown, criticallyDamped, underDamped, overDamped }
/// Structure that describes a spring's constants.
///
/// Used to configure a [SpringSimulation].
class SpringDescription {
/// Creates a spring given the mass, spring constant and the damping coefficient.
///
/// See [mass], [springConstant], and [damping] for the units of the arguments.
const SpringDescription({
this.mass,
this.springConstant,
this.damping
});
/// Creates a spring given the mass (m), spring constant (k), and damping
/// ratio (ζ). The damping ratio is especially useful trying to determing the
/// type of spring to create. A ratio of 1.0 creates a critically damped
/// spring, > 1.0 creates an overdamped spring and < 1.0 an underdamped one.
///
/// See [mass] and [springConstant] for the units for those arguments. The
/// damping ratio is unitless.
SpringDescription.withDampingRatio({
double mass,
double springConstant,
double ratio: 1.0
}) : mass = mass,
springConstant = springConstant,
damping = ratio * 2.0 * math.sqrt(mass * springConstant);
/// The mass of the spring (m). The units are arbitrary, but all springs
/// within a system should use the same mass units.
final double mass;
/// The spring constant (k). The units of the spring constant are M/T², where
/// M is the mass unit used for the value of the [mass] property, and T is the
/// time unit used for driving the [SpringSimulation].
final double springConstant;
/// The damping coefficient (c).
///
/// Do not confuse the damping _coefficient_ (c) with the damping _ratio_ (ζ).
/// To create a [SpringDescription] with a damping ratio, use the [new
/// SpringDescription.withDampingRatio] constructor.
///
/// The units of the damping coefficient are M/T, where M is the mass unit
/// used for the value of the [mass] property, and T is the time unit used for
/// driving the [SpringSimulation].
final double damping;
}
/// The kind of spring solution that the [SpringSimulation] is using to simulate the spring.
///
/// See [SpringSimulation.type].
enum SpringType {
/// A spring that does not bounce and returns to its rest position in the
/// shortest possible time.
criticallyDamped,
/// A spring that bounces.
underDamped,
/// A spring that does not bounce but takes longer to return to its rest
/// position than a [criticallyDamped] one.
overDamped,
}
/// A spring simulation.
///
/// Models a particle attached to a spring that follows Hooke's law.
class SpringSimulation extends Simulation {
/// Creates a spring simulation from the provided spring description, start
/// distance, end distance, and initial velocity.
///
/// The units for the start and end distance arguments are arbitrary, but must
/// be consistent with the units used for other lengths in the system.
///
/// The units for the velocity are L/T, where L is the aforementioned
/// arbitrary unit of length, and T is the time unit used for driving the
/// [SpringSimulation].
SpringSimulation(
SpringDescription desc,
double start,
double end,
double velocity
) : _endPosition = end,
_solution = new _SpringSolution(desc, start - end, velocity);
final double _endPosition;
final _SpringSolution _solution;
/// The kind of spring being simulated, for debugging purposes.
///
/// This is derived from the [SpringDescription] provided to the [new
/// SpringSimulation] constructor.
SpringType get type => _solution.type;
@override
double x(double time) => _endPosition + _solution.x(time);
@override
double dx(double time) => _solution.dx(time);
@override
bool isDone(double time) {
return nearZero(_solution.x(time), tolerance.distance) &&
nearZero(_solution.dx(time), tolerance.velocity);
}
}
/// A SpringSimulation where the value of [x] is guaranteed to have exactly the
/// end value when the simulation isDone().
class ScrollSpringSimulation extends SpringSimulation {
/// Creates a spring simulation from the provided spring description, start
/// distance, end distance, and initial velocity.
///
/// See the [new SpringSimulation] constructor on the superclass for a
/// discussion of the arguments' units.
ScrollSpringSimulation(
SpringDescription desc,
double start,
double end,
double velocity
) : super(desc, start, end, velocity);
@override
double x(double time) => isDone(time) ? _endPosition : super.x(time);
}
// SPRING IMPLEMENTATIONS
abstract class _SpringSolution {
factory _SpringSolution(
......@@ -15,6 +143,12 @@ abstract class _SpringSolution {
double initialPosition,
double initialVelocity
) {
assert(desc != null);
assert(desc.mass != null);
assert(desc.springConstant != null);
assert(desc.damping != null);
assert(initialPosition != null);
assert(initialVelocity != null);
double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
if (cmk == 0.0)
return new _CriticalSolution(desc, initialPosition, initialVelocity);
......@@ -140,83 +274,3 @@ class _UnderdampedSolution implements _SpringSolution {
@override
SpringType get type => SpringType.underDamped;
}
class SpringDescription {
SpringDescription({
this.mass,
this.springConstant,
this.damping
}) {
assert(mass != null);
assert(springConstant != null);
assert(damping != null);
}
/// Create a spring given the mass, spring constant and the damping ratio. The
/// damping ratio is especially useful trying to determing the type of spring
/// to create. A ratio of 1.0 creates a critically damped spring, > 1.0
/// creates an overdamped spring and < 1.0 an underdamped one.
SpringDescription.withDampingRatio({
double mass,
double springConstant,
double ratio: 1.0
}) : mass = mass,
springConstant = springConstant,
damping = ratio * 2.0 * math.sqrt(mass * springConstant);
/// The mass of the spring (m)
final double mass;
/// The spring constant (k)
final double springConstant;
/// The damping coefficient.
/// Not to be confused with the damping ratio. Use the separate
/// constructor provided for this purpose
final double damping;
}
/// Creates a spring simulation. Depending on the spring description, a
/// critically, under or overdamped spring will be created.
class SpringSimulation extends Simulation {
/// A spring description with the provided spring description, start distance,
/// end distance and velocity.
SpringSimulation(
SpringDescription desc,
double start,
double end,
double velocity
) : _endPosition = end,
_solution = new _SpringSolution(desc, start - end, velocity);
final double _endPosition;
final _SpringSolution _solution;
SpringType get type => _solution.type;
@override
double x(double time) => _endPosition + _solution.x(time);
@override
double dx(double time) => _solution.dx(time);
@override
bool isDone(double time) {
return nearZero(_solution.x(time), tolerance.distance) &&
nearZero(_solution.dx(time), tolerance.velocity);
}
}
/// A SpringSimulation where the value of x() is guaranteed to have exactly the
/// end value when the simulation isDone().
class ScrollSpringSimulation extends SpringSimulation {
ScrollSpringSimulation(
SpringDescription desc,
double start,
double end,
double velocity
) : super(desc, start, end, velocity);
@override
double x(double time) => isDone(time) ? _endPosition : super.x(time);
}
......@@ -2,17 +2,45 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Structure that specifies maximum allowable magnitudes for distances,
/// durations, and velocity differences to be considered equal.
class Tolerance {
/// Creates a [Tolerance] object. By default, the distance, time, and velocity
/// tolerances are all ±0.001; the constructor arguments override this.
///
/// The arguments should all be positive values.
const Tolerance({
this.distance: _kEpsilonDefault,
this.time: _kEpsilonDefault,
this.velocity: _kEpsilonDefault
});
static const double _kEpsilonDefault = 1e-3;
/// A default tolerance of 0.001 for all three values.
static const Tolerance defaultTolerance = const Tolerance();
/// The magnitude of the maximum distance between two points for them to be
/// considered within tolerance.
///
/// The units for the distance tolerance must be the same as the units used
/// for the distances that are to be compared to this tolerance.
final double distance;
/// The magnitude of the maximum duration between two times for them to be
/// considered within tolerance.
///
/// The units for the time tolerance must be the same as the units used
/// for the times that are to be compared to this tolerance.
final double time;
final double velocity;
const Tolerance({this.distance: epsilonDefault, this.time: epsilonDefault,
this.velocity: epsilonDefault});
/// The magnitude of the maximum difference between two velocities for them to
/// be considered within tolerance.
///
/// The units for the velocity tolerance must be the same as the units used
/// for the velocities that are to be compared to this tolerance.
final double velocity;
@override
String toString() => 'Tolerance(distance: $distance, time=$time, velocity: $velocity)';
String toString() => 'Tolerance(distance: ±$distance, time: ±$time, velocity: ±$velocity)';
}
const double epsilonDefault = 1e-3;
const Tolerance toleranceDefault = const Tolerance();
......@@ -2,9 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Whether two doubles are within a given distance of each other.
///
/// The epsilon argument must be positive.
bool nearEqual(double a, double b, double epsilon) {
assert(epsilon >= 0.0);
return (a > (b - epsilon)) && (a < (b + epsilon));
}
/// Whether a double is within a given distance of zero.
///
/// The epsilon argument must be positive.
bool nearZero(double a, double epsilon) => nearEqual(a, 0.0, epsilon);
......@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'near_equal_test.dart' as near_equal_test;
import 'newton_test.dart' as newton_test;
void main() {
near_equal_test.main();
newton_test.main();
}
// Copyright (c) 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 'package:test/test.dart';
import 'package:newton/newton.dart';
void main() {
test('test_friction', () {
expect(nearEqual(5.0, 6.0, 2.0), isTrue);
expect(nearEqual(6.0, 5.0, 2.0), isTrue);
expect(nearEqual(5.0, 6.0, 0.5), isFalse);
expect(nearEqual(6.0, 5.0, 0.5), isFalse);
});
}
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library simple_physics.test;
import 'package:test/test.dart';
import 'package:newton/newton.dart';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment