Commit d5becff7 authored by Adam Barth's avatar Adam Barth

Merge branch 'newton'

parents c7dfbc04 e5a96aca
# Newton
[![Build Status](https://travis-ci.org/flutter/newton.svg?branch=master)](https://travis-ci.org/flutter/newton)
[![Coverage Status](https://coveralls.io/repos/domokit/newton/badge.svg?branch=master)](https://coveralls.io/r/domokit/newton?branch=master)
Simple Physics Simulations for Dart. Springs, friction, gravity, etc.
To run the tests:
pub get
dart test/newton_test.dart
// 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.
library newton;
import 'dart:math' as math;
part 'src/simulation.dart';
part 'src/simulation_group.dart';
part 'src/tolerance.dart';
part 'src/utils.dart';
part 'src/friction_simulation.dart';
part 'src/gravity_simulation.dart';
part 'src/scroll_simulation.dart';
part 'src/spring_simulation.dart';
part 'src/spring_solution.dart';
// 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.
part of newton;
class FrictionSimulation extends Simulation {
final double _drag;
final double _dragLog;
final double _x;
final double _v;
FrictionSimulation(double drag, double position, double velocity)
: _drag = drag,
_dragLog = math.log(drag),
_x = position,
_v = velocity;
// 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));
}
// A friction simulation that starts and ends at the specified positions
// and velocities.
factory FrictionSimulation.through(double startPosition, double endPosition, double startVelocity, double endVelocity) {
return new FrictionSimulation(
_dragFor(startPosition, endPosition, startVelocity, endVelocity),
startPosition,
startVelocity)
.. tolerance = new Tolerance(velocity: endVelocity.abs());
}
double x(double time) => _x + _v * math.pow(_drag, time) / _dragLog - _v / _dragLog;
double dx(double time) => _v * math.pow(_drag, time);
@override
bool isDone(double time) => dx(time).abs() < this.tolerance.velocity;
}
class BoundedFrictionSimulation extends FrictionSimulation {
BoundedFrictionSimulation(
double drag,
double position,
double velocity,
double this._minX,
double this._maxX) : super(drag, position, velocity);
final double _minX;
final double _maxX;
double x(double time) {
return super.x(time).clamp(_minX, _maxX);
}
bool isDone(double time) {
return super.isDone(time) ||
(x(time) - _minX).abs() < tolerance.distance ||
(x(time) - _maxX).abs() < tolerance.distance;
}
}
// 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.
part of newton;
class GravitySimulation extends Simulation {
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;
double x(double time) => _x + _v * time + 0.5 * _a * time * time;
double dx(double time) => _v + time * _a;
@override
bool isDone(double time) => x(time).abs() >= _end;
}
// 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.
part of newton;
/// 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.
class ScrollSimulation extends SimulationGroup {
ScrollSimulation(
double position,
double velocity,
this._leadingExtent,
this._trailingExtent,
this._spring,
this._drag) {
_chooseSimulation(position, velocity, 0.0);
}
final double _leadingExtent;
final double _trailingExtent;
final SpringDescription _spring;
final double _drag;
bool _isSpringing = false;
Simulation _currentSimulation;
double _offset = 0.0;
@override
bool step(double time) => _chooseSimulation(
_currentSimulation.x(time - _offset),
_currentSimulation.dx(time - _offset), time);
@override
Simulation get currentSimulation => _currentSimulation;
@override
double get currentIntervalOffset => _offset;
bool _chooseSimulation(double position, double velocity, double intervalOffset) {
if (_spring == null && (position > _trailingExtent || position < _leadingExtent))
return false;
/// This simulation can only step forward.
if (!_isSpringing) {
if (position > _trailingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _trailingExtent, velocity);
return true;
} else if (position < _leadingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity);
return true;
}
}
if (_currentSimulation == null) {
_currentSimulation = new FrictionSimulation(_drag, position, velocity);
return true;
}
return false;
}
}
// 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.
part of newton;
abstract class Simulatable {
/// The current position of the object in the simulation
double x(double time);
/// The current velocity of the object in the simulation
double dx(double time);
}
/// 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.
abstract class Simulation implements Simulatable {
Tolerance tolerance = toleranceDefault;
/// Returns if the simulation is done at a given time
bool isDone(double time);
}
// 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.
part of newton;
/// 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 `Simulatable` methods.
abstract class SimulationGroup extends Simulation {
/// The currently active simulation
Simulation get currentSimulation;
/// The time offset applied to the currently active simulation;
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.
bool step(double time);
double x(double time) {
_stepIfNecessary(time);
return currentSimulation.x(time - currentIntervalOffset);
}
double dx(double time) {
_stepIfNecessary(time);
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;
}
}
}
// 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.
part of newton;
class SpringDescription {
/// The mass of the spring (m)
final double mass;
/// The spring constant (k)
final double springConstant;
/// The damping coefficient.
/// Note: Not to be confused with the damping ratio. Use the separate
/// constructor provided for this purpose
final double damping;
SpringDescription(
{double this.mass, double this.springConstant, double 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);
}
enum SpringType { unknown, criticallyDamped, underDamped, overDamped, }
/// Creates a spring simulation. Depending on the spring description, a
/// critically, under or overdamped spring will be created.
class SpringSimulation extends Simulation {
final double _endPosition;
final _SpringSolution _solution;
/// A spring description with the provided spring description, start distance,
/// end distance and velocity.
SpringSimulation(
SpringDescription desc, double start, double end, double velocity)
: this._endPosition = end,
_solution = new _SpringSolution(desc, start - end, velocity);
SpringType get type => _solution.type;
double x(double time) => _endPosition + _solution.x(time);
double dx(double time) => _solution.dx(time);
@override
bool isDone(double time) =>
_nearEqual(x(time), _endPosition, this.tolerance.distance) &&
_nearZero(dx(time), this.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);
bool _isDone(double position, double velocity) {
return _nearEqual(position, _endPosition, tolerance.distance) && _nearZero(velocity, tolerance.velocity);
}
@override
double x(double time) {
double xAtTime = super.x(time);
return _isDone(xAtTime, dx(time)) ? _endPosition : xAtTime;
}
@override
bool isDone(double time) => _isDone(x(time), dx(time));
}
// 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.
part of newton;
abstract class _SpringSolution implements Simulatable {
factory _SpringSolution(
SpringDescription desc, double initialPosition, double initialVelocity) {
double cmk =
desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
if (cmk == 0.0) {
return new _CriticalSolution(desc, initialPosition, initialVelocity);
} else if (cmk > 0.0) {
return new _OverdampedSolution(desc, initialPosition, initialVelocity);
} else {
return new _UnderdampedSolution(desc, initialPosition, initialVelocity);
}
return null;
}
SpringType get type;
}
class _CriticalSolution implements _SpringSolution {
final double _r, _c1, _c2;
factory _CriticalSolution(
SpringDescription desc, double distance, double velocity) {
final double r = -desc.damping / (2.0 * desc.mass);
final double c1 = distance;
final double c2 = velocity / (r * distance);
return new _CriticalSolution.withArgs(r, c1, c2);
}
SpringType get type => SpringType.criticallyDamped;
_CriticalSolution.withArgs(double r, double c1, double c2)
: _r = r,
_c1 = c1,
_c2 = c2;
double x(double time) => (_c1 + _c2 * time) * math.pow(math.E, _r * time);
double dx(double time) {
final double power = math.pow(math.E, _r * time);
return _r * (_c1 + _c2 * time) * power + _c2 * power;
}
}
class _OverdampedSolution implements _SpringSolution {
final double _r1, _r2, _c1, _c2;
factory _OverdampedSolution(
SpringDescription desc, double distance, double velocity) {
final double cmk =
desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
final double r1 = (-desc.damping - math.sqrt(cmk)) / (2.0 * desc.mass);
final double r2 = (-desc.damping + math.sqrt(cmk)) / (2.0 * desc.mass);
final double c2 = (velocity - r1 * distance) / (r2 - r1);
final double c1 = distance - c2;
return new _OverdampedSolution.withArgs(r1, r2, c1, c2);
}
_OverdampedSolution.withArgs(double r1, double r2, double c1, double c2)
: _r1 = r1,
_r2 = r2,
_c1 = c1,
_c2 = c2;
SpringType get type => SpringType.overDamped;
double x(double time) =>
(_c1 * math.pow(math.E, _r1 * time) + _c2 * math.pow(math.E, _r2 * time));
double dx(double time) => (_c1 * _r1 * math.pow(math.E, _r1 * time) +
_c2 * _r2 * math.pow(math.E, _r2 * time));
}
class _UnderdampedSolution implements _SpringSolution {
final double _w, _r, _c1, _c2;
factory _UnderdampedSolution(
SpringDescription desc, double distance, double velocity) {
final double w = math.sqrt(4.0 * desc.mass * desc.springConstant -
desc.damping * desc.damping) /
(2.0 * desc.mass);
final double r = -(desc.damping / 2.0 * desc.mass);
final double c1 = distance;
final double c2 = (velocity - r * distance) / w;
return new _UnderdampedSolution.withArgs(w, r, c1, c2);
}
_UnderdampedSolution.withArgs(double w, double r, double c1, double c2)
: _w = w,
_r = r,
_c1 = c1,
_c2 = c2;
SpringType get type => SpringType.underDamped;
double x(double time) => math.pow(math.E, _r * time) *
(_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time));
double dx(double time) {
final double power = math.pow(math.E, _r * time);
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);
}
}
// 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.
part of newton;
class Tolerance {
final double distance;
final double time;
final double velocity;
const Tolerance({this.distance: epsilonDefault, this.time: epsilonDefault,
this.velocity: epsilonDefault});
}
const double epsilonDefault = 1e-3;
const Tolerance toleranceDefault = const Tolerance();
// 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.
part of newton;
bool _nearEqual(double a, double b, double epsilon) =>
(a > (b - epsilon)) && (a < (b + epsilon));
bool _nearZero(double a, double epsilon) => _nearEqual(a, 0.0, epsilon);
// 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.
library simple_physics.test;
import 'package:test/test.dart';
import 'package:newton/newton.dart';
void main() {
test('test_friction', () {
var friction = new FrictionSimulation(0.3, 100.0, 400.0);
friction.tolerance = const Tolerance(velocity: 1.0);
expect(friction.isDone(0.0), false);
expect(friction.x(0.0), 100);
expect(friction.dx(0.0), 400.0);
expect(friction.x(1.0) > 330 && friction.x(1.0) < 335, true);
expect(friction.dx(1.0), 120.0);
expect(friction.dx(2.0), 36.0);
expect(friction.dx(3.0), 10.8);
expect(friction.dx(4.0) < 3.5, true);
expect(friction.isDone(5.0), true);
expect(friction.x(5.0) > 431 && friction.x(5.0) < 432, true);
});
test('test_friction_through', () {
// Use a normal FrictionSimulation to generate start and end
// velocity and positions with drag = 0.025.
var startPosition = 10.0;
var startVelocity = 600.0;
var f = new FrictionSimulation(0.025, startPosition, startVelocity);
var endPosition = f.x(1.0);
var endVelocity = f.dx(1.0);
expect(endPosition, greaterThan(startPosition));
expect(endVelocity, lessThan(startVelocity));
// Verify that that the "through" FrictionSimulation ends up at
// endPosition and endVelocity; implies that it computed the right
// value for _drag.
var friction = new FrictionSimulation.through(
startPosition, endPosition, startVelocity, endVelocity);
expect(friction.isDone(0.0), false);
expect(friction.x(0.0), 10.0);
expect(friction.dx(0.0), 600.0);
double epsilon = 1e-4;
expect(friction.isDone(1.0 + epsilon), true);
expect(friction.x(1.0), closeTo(endPosition, epsilon));
expect(friction.dx(1.0), closeTo(endVelocity, epsilon));
// Same scenario as above except that the velocities are
// are negative.
startPosition = 1000.0;
startVelocity = -500.0;
f = new FrictionSimulation(0.025, 1000.0, -500.0);
endPosition = f.x(1.0);
endVelocity = f.dx(1.0);
expect(endPosition, lessThan(startPosition));
expect(endVelocity, greaterThan(startVelocity));
friction = new FrictionSimulation.through(
startPosition, endPosition, startVelocity, endVelocity);
expect(friction.isDone(1.0 + epsilon), true);
expect(friction.x(1.0), closeTo(endPosition, epsilon));
expect(friction.dx(1.0), closeTo(endVelocity, epsilon));
});
test('test_gravity', () {
var gravity = new GravitySimulation(200.0, 100.0, 600.0, 0.0);
expect(gravity.isDone(0.0), false);
expect(gravity.x(0.0), 100.0);
expect(gravity.dx(0.0), 0.0);
// Starts at 100
expect(gravity.x(0.25), 106.25);
expect(gravity.x(0.50), 125);
expect(gravity.x(0.75), 156.25);
expect(gravity.x(1.00), 200);
expect(gravity.x(1.25), 256.25);
expect(gravity.x(1.50), 325);
expect(gravity.x(1.75), 406.25);
// Starts at 0.0
expect(gravity.dx(0.25), 50.0);
expect(gravity.dx(0.50), 100);
expect(gravity.dx(0.75), 150.00);
expect(gravity.dx(1.00), 200.0);
expect(gravity.dx(1.25), 250.0);
expect(gravity.dx(1.50), 300);
expect(gravity.dx(1.75), 350);
expect(gravity.isDone(2.5), true);
expect(gravity.x(2.5), 725);
expect(gravity.dx(2.5), 500.0);
});
test('spring_types', () {
var crit = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0), 0.0, 300.0, 0.0);
expect(crit.type, SpringType.criticallyDamped);
crit = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0, ratio: 1.0), 0.0, 300.0, 0.0);
expect(crit.type, SpringType.criticallyDamped);
var under = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0, ratio: 0.75), 0.0, 300.0, 0.0);
expect(under.type, SpringType.underDamped);
var over = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0, ratio: 1.25), 0.0, 300.0, 0.0);
expect(over.type, SpringType.overDamped);
// Just so we don't forget how to create a desc without the ratio.
var other = new SpringSimulation(
new SpringDescription(mass: 1.0, springConstant: 100.0, damping: 20.0),
0.0, 20.0, 20.0);
expect(other.type, SpringType.criticallyDamped);
});
test('crit_spring', () {
var crit = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0, ratio: 1.0), 0.0, 500.0, 0.0);
crit.tolerance = const Tolerance(distance: 0.01, velocity: 0.01);
expect(crit.type, SpringType.criticallyDamped);
expect(crit.isDone(0.0), false);
expect(crit.x(0.0), 0.0);
expect(crit.dx(0.0), 5000.0);
expect(crit.x(0.25).floor(), 458.0);
expect(crit.x(0.50).floor(), 496.0);
expect(crit.x(0.75).floor(), 499.0);
expect(crit.dx(0.25).floor(), 410);
expect(crit.dx(0.50).floor(), 33);
expect(crit.dx(0.75).floor(), 2);
expect(crit.isDone(1.50), true);
expect(crit.x(1.5) > 499.0 && crit.x(1.5) < 501.0, true);
expect(crit.dx(1.5) < 0.1, true /* basically within tolerance */);
});
test('overdamped_spring', () {
var over = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0, ratio: 1.25), 0.0, 500.0, 0.0);
over.tolerance = const Tolerance(distance: 0.01, velocity: 0.01);
expect(over.type, SpringType.overDamped);
expect(over.isDone(0.0), false);
expect(over.x(0.0), 0.0);
expect(over.x(0.5).floor(), 445.0);
expect(over.x(1.0).floor(), 495.0);
expect(over.x(1.5).floor(), 499.0);
expect(over.dx(0.5).floor(), 273.0);
expect(over.dx(1.0).floor(), 22.0);
expect(over.dx(1.5).floor(), 1.0);
expect(over.isDone(3.0), true);
});
test('underdamped_spring', () {
var under = new SpringSimulation(new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 100.0, ratio: 0.25), 0.0, 300.0, 0.0);
expect(under.type, SpringType.underDamped);
expect(under.isDone(0.0), false);
// Overshot with negative velocity
expect(under.x(1.0).floor(), 325);
expect(under.dx(1.0).floor(), -65);
expect(under.dx(6.0).floor(), 0.0);
expect(under.x(6.0).floor(), 299);
expect(under.isDone(6.0), true);
});
test('test_kinetic_scroll', () {
var spring = new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 50.0, ratio: 0.5);
var scroll = new ScrollSimulation(100.0, 800.0, 0.0, 300.0, spring, 0.3);
scroll.tolerance = const Tolerance(velocity: 0.5, distance: 0.1);
expect(scroll.isDone(0.0), false);
expect(scroll.isDone(0.5), false); // switch from friction to spring
expect(scroll.isDone(3.5), true);
var scroll2 = new ScrollSimulation(100.0, -800.0, 0.0, 300.0, spring, 0.3);
scroll2.tolerance = const Tolerance(velocity: 0.5, distance: 0.1);
expect(scroll2.isDone(0.0), false);
expect(scroll2.isDone(0.5), false); // switch from friction to spring
expect(scroll2.isDone(3.5), true);
});
test('scroll_with_inf_edge_ends', () {
var spring = new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 50.0, ratio: 0.5);
var scroll =
new ScrollSimulation(100.0, 400.0, 0.0, double.INFINITY, spring, 0.3);
scroll.tolerance = const Tolerance(velocity: 1.0);
expect(scroll.isDone(0.0), false);
expect(scroll.x(0.0), 100);
expect(scroll.dx(0.0), 400.0);
expect(scroll.x(1.0) > 330 && scroll.x(1.0) < 335, true);
expect(scroll.dx(1.0), 120.0);
expect(scroll.dx(2.0), 36.0);
expect(scroll.dx(3.0), 10.8);
expect(scroll.dx(4.0) < 3.5, true);
expect(scroll.isDone(5.0), true);
expect(scroll.x(5.0) > 431 && scroll.x(5.0) < 432, true);
// We should never switch
expect(scroll.currentIntervalOffset, 0.0);
});
test('over/under scroll spring', () {
var spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
var scroll = new ScrollSimulation(500.0, -7500.0, 0.0, 1000.0, spring, 0.025);
scroll.tolerance = new Tolerance(velocity: 45.0, distance: 1.5);
expect(scroll.isDone(0.0), false);
expect(scroll.x(0.0), closeTo(500.0, .0001));
expect(scroll.dx(0.0), closeTo(-7500.0, .0001));
expect(scroll.isDone(0.025), false);
expect(scroll.x(0.025), closeTo(320.0, 1.0));
expect(scroll.dx(0.25), closeTo(-2982, 1.0));
expect(scroll.isDone(2.0), true);
expect(scroll.x(2.0), 0.0);
expect(scroll.dx(2.0), closeTo(0.0, 45.0));
});
}
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