spring_simulation.dart 8.65 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math' as math;

7 8
import 'package:flutter/foundation.dart';

9 10 11
import 'simulation.dart';
import 'utils.dart';

12 13
export 'tolerance.dart' show Tolerance;

Ian Hickson's avatar
Ian Hickson committed
14 15 16 17
/// Structure that describes a spring's constants.
///
/// Used to configure a [SpringSimulation].
class SpringDescription {
18
  /// Creates a spring given the mass, stiffness, and the damping coefficient.
Ian Hickson's avatar
Ian Hickson committed
19
  ///
20
  /// See [mass], [stiffness], and [damping] for the units of the arguments.
Ian Hickson's avatar
Ian Hickson committed
21
  const SpringDescription({
22 23 24
    required this.mass,
    required this.stiffness,
    required this.damping,
Ian Hickson's avatar
Ian Hickson committed
25 26
  });

27
  /// Creates a spring given the mass (m), stiffness (k), and damping ratio (ζ).
28
  /// The damping ratio is especially useful trying to determining the type of
29 30
  /// 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.
Ian Hickson's avatar
Ian Hickson committed
31
  ///
32 33
  /// See [mass] and [stiffness] for the units for those arguments. The damping
  /// ratio is unitless.
Ian Hickson's avatar
Ian Hickson committed
34
  SpringDescription.withDampingRatio({
35 36
    required this.mass,
    required this.stiffness,
37
    double ratio = 1.0,
38
  }) : damping = ratio * 2.0 * math.sqrt(mass * stiffness);
Ian Hickson's avatar
Ian Hickson committed
39 40 41 42 43

  /// 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;

44 45 46 47
  /// 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].
  final double stiffness;
Ian Hickson's avatar
Ian Hickson committed
48 49 50 51

  /// The damping coefficient (c).
  ///
  /// Do not confuse the damping _coefficient_ (c) with the damping _ratio_ (ζ).
52
  /// To create a [SpringDescription] with a damping ratio, use the [
Ian Hickson's avatar
Ian Hickson committed
53 54 55 56 57 58
  /// 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;
59 60

  @override
61
  String toString() => '${objectRuntimeType(this, 'SpringDescription')}(mass: ${mass.toStringAsFixed(1)}, stiffness: ${stiffness.toStringAsFixed(1)}, damping: ${damping.toStringAsFixed(1)})';
Ian Hickson's avatar
Ian Hickson committed
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
}

/// 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(
94
    SpringDescription spring,
Ian Hickson's avatar
Ian Hickson committed
95 96
    double start,
    double end,
97
    double velocity, {
98
    super.tolerance,
99
  }) : _endPosition = end,
100
       _solution = _SpringSolution(spring, start - end, velocity);
Ian Hickson's avatar
Ian Hickson committed
101 102 103 104 105 106

  final double _endPosition;
  final _SpringSolution _solution;

  /// The kind of spring being simulated, for debugging purposes.
  ///
107
  /// This is derived from the [SpringDescription] provided to the [
Ian Hickson's avatar
Ian Hickson committed
108 109 110 111 112 113 114 115 116 117 118 119 120 121
  /// 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);
  }
122 123

  @override
124
  String toString() => '${objectRuntimeType(this, 'SpringSimulation')}(end: ${_endPosition.toStringAsFixed(1)}, $type)';
Ian Hickson's avatar
Ian Hickson committed
125 126
}

127 128
/// A [SpringSimulation] where the value of [x] is guaranteed to have exactly the
/// end value when the simulation [isDone].
Ian Hickson's avatar
Ian Hickson committed
129 130 131 132
class ScrollSpringSimulation extends SpringSimulation {
  /// Creates a spring simulation from the provided spring description, start
  /// distance, end distance, and initial velocity.
  ///
133
  /// See the [SpringSimulation.new] constructor on the superclass for a
Ian Hickson's avatar
Ian Hickson committed
134 135
  /// discussion of the arguments' units.
  ScrollSpringSimulation(
136 137 138 139 140 141
    super.spring,
    super.start,
    super.end,
    super.velocity, {
    super.tolerance,
  });
Ian Hickson's avatar
Ian Hickson committed
142 143 144 145 146 147 148

  @override
  double x(double time) => isDone(time) ? _endPosition : super.x(time);
}


// SPRING IMPLEMENTATIONS
149

150 151
abstract class _SpringSolution {
  factory _SpringSolution(
152
    SpringDescription spring,
153
    double initialPosition,
154
    double initialVelocity,
155
  ) {
156
    final double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.stiffness;
157
    if (cmk == 0.0) {
158
      return _CriticalSolution(spring, initialPosition, initialVelocity);
159 160
    }
    if (cmk > 0.0) {
161
      return _OverdampedSolution(spring, initialPosition, initialVelocity);
162
    }
163
    return _UnderdampedSolution(spring, initialPosition, initialVelocity);
164 165
  }

166 167
  double x(double time);
  double dx(double time);
168 169 170 171 172
  SpringType get type;
}

class _CriticalSolution implements _SpringSolution {
  factory _CriticalSolution(
173
    SpringDescription spring,
174
    double distance,
175
    double velocity,
176
  ) {
177
    final double r = -spring.damping / (2.0 * spring.mass);
178
    final double c1 = distance;
179
    final double c2 = velocity - (r * distance);
180
    return _CriticalSolution.withArgs(r, c1, c2);
181 182 183
  }

  _CriticalSolution.withArgs(double r, double c1, double c2)
184 185 186
    : _r = r,
      _c1 = c1,
      _c2 = c2;
187

188 189
  final double _r, _c1, _c2;

190
  @override
191
  double x(double time) {
192
    return (_c1 + _c2 * time) * math.pow(math.e, _r * time);
193
  }
194

195
  @override
196
  double dx(double time) {
197
    final double power = math.pow(math.e, _r * time) as double;
198 199
    return _r * (_c1 + _c2 * time) * power + _c2 * power;
  }
200

201
  @override
202
  SpringType get type => SpringType.criticallyDamped;
203 204 205 206
}

class _OverdampedSolution implements _SpringSolution {
  factory _OverdampedSolution(
207
    SpringDescription spring,
208
    double distance,
209
    double velocity,
210
  ) {
211
    final double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.stiffness;
212 213
    final double r1 = (-spring.damping - math.sqrt(cmk)) / (2.0 * spring.mass);
    final double r2 = (-spring.damping + math.sqrt(cmk)) / (2.0 * spring.mass);
214 215
    final double c2 = (velocity - r1 * distance) / (r2 - r1);
    final double c1 = distance - c2;
216
    return _OverdampedSolution.withArgs(r1, r2, c1, c2);
217 218 219
  }

  _OverdampedSolution.withArgs(double r1, double r2, double c1, double c2)
220 221 222 223
    : _r1 = r1,
      _r2 = r2,
      _c1 = c1,
      _c2 = c2;
224

225
  final double _r1, _r2, _c1, _c2;
226

227
  @override
228
  double x(double time) {
229 230
    return _c1 * math.pow(math.e, _r1 * time) +
           _c2 * math.pow(math.e, _r2 * time);
231 232
  }

233
  @override
234
  double dx(double time) {
235 236
    return _c1 * _r1 * math.pow(math.e, _r1 * time) +
           _c2 * _r2 * math.pow(math.e, _r2 * time);
237
  }
238

239
  @override
240
  SpringType get type => SpringType.overDamped;
241 242 243 244
}

class _UnderdampedSolution implements _SpringSolution {
  factory _UnderdampedSolution(
245
    SpringDescription spring,
246
    double distance,
247
    double velocity,
248
  ) {
249 250
    final double w = math.sqrt(4.0 * spring.mass * spring.stiffness - spring.damping * spring.damping) /
        (2.0 * spring.mass);
251
    final double r = -(spring.damping / 2.0 * spring.mass);
252 253
    final double c1 = distance;
    final double c2 = (velocity - r * distance) / w;
254
    return _UnderdampedSolution.withArgs(w, r, c1, c2);
255 256 257
  }

  _UnderdampedSolution.withArgs(double w, double r, double c1, double c2)
258 259 260 261
    : _w = w,
      _r = r,
      _c1 = c1,
      _c2 = c2;
262

263
  final double _w, _r, _c1, _c2;
264

265
  @override
266
  double x(double time) {
267
    return (math.pow(math.e, _r * time) as double) *
268 269
           (_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time));
  }
270

271
  @override
272
  double dx(double time) {
273
    final double power = math.pow(math.e, _r * time) as double;
274 275
    final double cosine = math.cos(_w * time);
    final double sine = math.sin(_w * time);
276
    return power * (_c2 * _w * cosine - _c1 * _w * sine) +
277
           _r * power * (_c2 *      sine   + _c1 *      cosine);
278
  }
279

280
  @override
281
  SpringType get type => SpringType.underDamped;
282
}