blob: 6a6cc4cad46f7f8c744afe9cd8ac06757e5ba1a8 [file] [log] [blame]
// Copyright 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 'dart:math' as math;
const double kGravity = -0.980; // m^s-2
abstract class System {
void update(double deltaT);
}
class Particle extends System {
final double mass;
double velocity;
double position;
Particle({this.mass: 1.0, this.velocity: 0.0, this.position: 0.0});
void applyImpulse(double impulse) {
velocity += impulse / mass;
}
void update(double deltaT) {
position += velocity * deltaT;
}
void setVelocityFromEnergy({double energy, double direction}) {
assert(direction == -1.0 || direction == 1.0);
assert(energy >= 0.0);
velocity = math.sqrt(2.0 * energy / mass) * direction;
}
}
abstract class Box {
void confine(Particle p);
}
class ClosedBox extends Box {
final double min; // m
final double max; // m
ClosedBox({this.min, this.max}) {
assert(min == null || max == null || min <= max);
}
void confine(Particle p) {
if (min != null) {
p.position = math.max(min, p.position);
if (p.position == min)
p.velocity = math.max(0.0, p.velocity);
}
if (max != null) {
p.position = math.min(max, p.position);
if (p.position == max)
p.velocity = math.min(0.0, p.velocity);
}
}
}
class GeofenceBox extends Box {
final double min; // m
final double max; // m
final Function onEscape;
GeofenceBox({this.min, this.max, this.onEscape}) {
assert(min == null || max == null || min <= max);
assert(onEscape != null);
}
void confine(Particle p) {
if (((min != null) && (p.position < min)) ||
((max != null) && (p.position > max)))
onEscape();
}
}
class ParticleInBox extends System {
final Particle particle;
final Box box;
ParticleInBox({this.particle, this.box}) {
box.confine(particle);
}
void update(double deltaT) {
particle.update(deltaT);
box.confine(particle);
}
}
class ParticleInBoxWithFriction extends ParticleInBox {
final double friction; // unitless
final double _sign;
final Function onStop;
ParticleInBoxWithFriction({Particle particle, Box box, this.friction, this.onStop})
: super(particle: particle, box: box),
_sign = particle.velocity.sign;
void update(double deltaT) {
double force = -_sign * friction * particle.mass * -kGravity;
particle.applyImpulse(force * deltaT);
if (particle.velocity.sign != _sign) {
particle.velocity = 0.0;
}
super.update(deltaT);
if ((particle.velocity == 0.0) && (onStop != null))
onStop();
}
}
class Spring {
final double k;
double displacement;
Spring(this.k, {this.displacement: 0.0});
double get force => -k * displacement;
}
class ParticleAndSpringInBox extends System {
final Particle particle;
final Spring spring;
final Box box;
ParticleAndSpringInBox({this.particle, this.spring, this.box}) {
_applyInvariants();
}
void update(double deltaT) {
particle.applyImpulse(spring.force * deltaT);
particle.update(deltaT);
_applyInvariants();
}
void _applyInvariants() {
box.confine(particle);
spring.displacement = particle.position;
}
}
class ParticleClimbingRamp extends System {
// This is technically the same as ParticleInBoxWithFriction. The
// difference is in how the system is set up. Here, we configure the
// system so as to stop by a certain distance after having been
// given an initial impulse from rest, whereas
// ParticleInBoxWithFriction is set up to stop with a consistent
// decelerating force assuming an initial velocity. The angle theta
// (0 < theta < π/2) is used to configure how much energy the
// particle is to start with; lower angles result in a gentler kick
// while higher angles result in a faster conclusion.
final Particle particle;
final Box box;
final double theta;
final double _sinTheta;
ParticleClimbingRamp({
this.particle,
this.box,
double theta, // in radians
double targetPosition}) : this.theta = theta, this._sinTheta = math.sin(theta) {
assert(theta > 0.0);
assert(theta < math.PI / 2.0);
double deltaPosition = targetPosition - particle.position;
double tanTheta = math.tan(theta);
// We need to give the particle exactly as much (kinetic) energy
// as it needs to get to the top of the slope and stop with
// energy=0. This is exactly the same amount of energy as the
// potential energy at the top of the slope, which is g*h*m.
// If the slope's horizontal component is delta P long, then
// the height is delta P times tan theta.
particle.setVelocityFromEnergy(
energy: (kGravity * (deltaPosition * tanTheta) * particle.mass).abs(),
direction: deltaPosition > 0.0 ? 1.0 : -1.0
);
box.confine(particle);
}
void update(double deltaT) {
particle.update(deltaT);
// Note that we apply the impulse from gravity after updating the particle's
// position so that we overestimate the distance traveled by the particle.
// That ensures that we actually hit the edge of the box and don't wind up
// reversing course.
particle.applyImpulse(particle.mass * kGravity * _sinTheta * deltaT);
box.confine(particle);
}
}
class Multisystem extends System {
final Particle particle;
System _currentSystem;
Multisystem({ this.particle, System system }) {
assert(system != null);
_currentSystem = system;
}
void update(double deltaT) {
_currentSystem.update(deltaT);
}
void transitionToSystem(System system) {
assert(system != null);
_currentSystem = system;
}
}