blob: 4bfc4b3adb6221b5e7d525c7008f1eb80c54aad3 [file] [log] [blame]
// 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.
/// @docImport 'package:flutter/widgets.dart';
library;
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'simulation.dart';
import 'utils.dart';
export 'tolerance.dart' show Tolerance;
// Examples can assume:
// late AnimationController _controller;
/// Structure that describes a spring's constants.
///
/// Used to configure a [SpringSimulation].
class SpringDescription {
/// Creates a spring given the mass, stiffness, and the damping coefficient.
///
/// See [mass], [stiffness], and [damping] for the units of the arguments.
const SpringDescription({required this.mass, required this.stiffness, required this.damping});
/// Creates a spring given the mass (m), stiffness (k), and damping ratio (ζ).
/// The damping ratio describes a gradual reduction in a spring oscillation.
/// By using the damping ratio, you can define how rapidly the oscillations
/// decay from one bounce to the next.
///
/// The damping ratio is especially useful when trying to determining 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 [stiffness] for the units for those arguments. The damping
/// ratio is unitless.
SpringDescription.withDampingRatio({
required this.mass,
required this.stiffness,
double ratio = 1.0,
}) : damping = ratio * 2.0 * math.sqrt(mass * stiffness);
/// The mass of the spring (m).
///
/// The units are arbitrary, but all springs within a system should use
/// the same mass units.
///
/// The greater the mass, the larger the amplitude of oscillation,
/// and the longer the time to return to the equilibrium position.
final double mass;
/// The spring constant (k).
///
/// The units of stiffness 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].
///
/// Stiffness defines the spring constant, which measures the strength of
/// the spring. A stiff spring applies more force to the object that is
/// attached for some deviation from the rest position.
final double stiffness;
/// The damping coefficient (c).
///
/// It is a pure number without physical meaning and describes the oscillation
/// and decay of a system after being disturbed. The larger the damping,
/// the fewer oscillations and smaller the amplitude of the elastic motion.
///
/// Do not confuse the damping _coefficient_ (c) with the damping _ratio_ (ζ).
/// To create a [SpringDescription] with a damping ratio, use the [
/// 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;
@override
String toString() =>
'${objectRuntimeType(this, 'SpringDescription')}(mass: ${mass.toStringAsFixed(1)}, stiffness: ${stiffness.toStringAsFixed(1)}, damping: ${damping.toStringAsFixed(1)})';
}
/// 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.
///
/// {@tool snippet}
///
/// This method triggers an [AnimationController] (a previously constructed
/// `_controller` field) to simulate a spring oscillation.
///
/// ```dart
/// void _startSpringMotion() {
/// _controller.animateWith(SpringSimulation(
/// const SpringDescription(
/// mass: 1.0,
/// stiffness: 300.0,
/// damping: 15.0,
/// ),
/// 0.0, // starting position
/// 1.0, // ending position
/// 0.0, // starting velocity
/// ));
/// }
/// ```
/// {@end-tool}
///
/// This [AnimationController] could be used with an [AnimatedBuilder] to
/// animate the position of a child as if it were attached to a spring.
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].
///
/// If `snapToEnd` is true, [x] will be set to `end` and [dx] to 0 when
/// [isDone] returns true. This is useful for transitions that require the
/// simulation to stop exactly at the end value, since the spring may not
/// naturally reach the target precisely. Defaults to false.
SpringSimulation(
SpringDescription spring,
double start,
double end,
double velocity, {
bool snapToEnd = false,
super.tolerance,
}) : _endPosition = end,
_solution = _SpringSolution(spring, start - end, velocity),
_snapToEnd = snapToEnd;
final double _endPosition;
final _SpringSolution _solution;
final bool _snapToEnd;
/// The kind of spring being simulated, for debugging purposes.
///
/// This is derived from the [SpringDescription] provided to the [
/// SpringSimulation] constructor.
SpringType get type => _solution.type;
@override
double x(double time) {
if (_snapToEnd && isDone(time)) {
return _endPosition;
} else {
return _endPosition + _solution.x(time);
}
}
@override
double dx(double time) {
if (_snapToEnd && isDone(time)) {
return 0;
} else {
return _solution.dx(time);
}
}
@override
bool isDone(double time) {
return nearZero(_solution.x(time), tolerance.distance) &&
nearZero(_solution.dx(time), tolerance.velocity);
}
@override
String toString() =>
'${objectRuntimeType(this, 'SpringSimulation')}(end: ${_endPosition.toStringAsFixed(1)}, $type)';
}
/// 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 [SpringSimulation.new] constructor on the superclass for a
/// discussion of the arguments' units.
ScrollSpringSimulation(super.spring, super.start, super.end, super.velocity, {super.tolerance});
@override
double x(double time) => isDone(time) ? _endPosition : super.x(time);
}
// SPRING IMPLEMENTATIONS
abstract class _SpringSolution {
factory _SpringSolution(
SpringDescription spring,
double initialPosition,
double initialVelocity,
) {
return switch (spring.damping * spring.damping - 4 * spring.mass * spring.stiffness) {
> 0.0 => _OverdampedSolution(spring, initialPosition, initialVelocity),
< 0.0 => _UnderdampedSolution(spring, initialPosition, initialVelocity),
_ => _CriticalSolution(spring, initialPosition, initialVelocity),
};
}
double x(double time);
double dx(double time);
SpringType get type;
}
class _CriticalSolution implements _SpringSolution {
factory _CriticalSolution(SpringDescription spring, double distance, double velocity) {
final double r = -spring.damping / (2.0 * spring.mass);
final double c1 = distance;
final double c2 = velocity - (r * distance);
return _CriticalSolution.withArgs(r, c1, c2);
}
_CriticalSolution.withArgs(double r, double c1, double c2) : _r = r, _c1 = c1, _c2 = c2;
final double _r, _c1, _c2;
@override
double x(double time) {
return (_c1 + _c2 * time) * math.pow(math.e, _r * time);
}
@override
double dx(double time) {
final double power = math.pow(math.e, _r * time) as double;
return _r * (_c1 + _c2 * time) * power + _c2 * power;
}
@override
SpringType get type => SpringType.criticallyDamped;
}
class _OverdampedSolution implements _SpringSolution {
factory _OverdampedSolution(SpringDescription spring, double distance, double velocity) {
final double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.stiffness;
final double r1 = (-spring.damping - math.sqrt(cmk)) / (2.0 * spring.mass);
final double r2 = (-spring.damping + math.sqrt(cmk)) / (2.0 * spring.mass);
final double c2 = (velocity - r1 * distance) / (r2 - r1);
final double c1 = distance - c2;
return _OverdampedSolution.withArgs(r1, r2, c1, c2);
}
_OverdampedSolution.withArgs(double r1, double r2, double c1, double c2)
: _r1 = r1,
_r2 = r2,
_c1 = c1,
_c2 = c2;
final double _r1, _r2, _c1, _c2;
@override
double x(double time) {
return _c1 * math.pow(math.e, _r1 * time) + _c2 * math.pow(math.e, _r2 * time);
}
@override
double dx(double time) {
return _c1 * _r1 * math.pow(math.e, _r1 * time) + _c2 * _r2 * math.pow(math.e, _r2 * time);
}
@override
SpringType get type => SpringType.overDamped;
}
class _UnderdampedSolution implements _SpringSolution {
factory _UnderdampedSolution(SpringDescription spring, double distance, double velocity) {
final double w =
math.sqrt(4.0 * spring.mass * spring.stiffness - spring.damping * spring.damping) /
(2.0 * spring.mass);
final double r = -(spring.damping / 2.0 * spring.mass);
final double c1 = distance;
final double c2 = (velocity - r * distance) / w;
return _UnderdampedSolution.withArgs(w, r, c1, c2);
}
_UnderdampedSolution.withArgs(double w, double r, double c1, double c2)
: _w = w,
_r = r,
_c1 = c1,
_c2 = c2;
final double _w, _r, _c1, _c2;
@override
double x(double time) {
return (math.pow(math.e, _r * time) as double) *
(_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time));
}
@override
double dx(double time) {
final double power = math.pow(math.e, _r * time) as double;
final double cosine = math.cos(_w * time);
final double sine = math.sin(_w * time);
return power * (_c2 * _w * cosine - _c1 * _w * sine) + _r * power * (_c2 * sine + _c1 * cosine);
}
@override
SpringType get type => SpringType.underDamped;
}