Introduction
For this article, I decided to play around with a new type of layout manager for Windows applications. I thought that it would be cool if I could layout the application's controls not using docking, snapping, or absolute positions, but instead hang my controls from elastic "rubber bands". This might not be the best layout manager for most applications, but it would definitely be really cool-looking for some UIs.
I decided to keep my Physics implementation rather simple, I would use Springs, Gravity, and Drag, as movement constraints, but not collisions.
The example application that comes with the download reads the RSS feed for the featured Windows Vista Sidebar Gadgets and lets you "pick" them from a "clothes line" as a way of visually downloading the gadgets. I think it looks and feels really cool.
Note: This is article 1 of 2; this article is a full implementation but lacks support for WPF. In part 2 of 2, I'll show you how to port this implementation to WPF.
Disclaimer: I'm Swedish, English is not my native language, so if I'm using the term bounciness instead of coefficient of restitution or something like that in this article, bear with me :)
Using the code
Because this type of an implementation moves controls around quite a bit, it is important that the computer running the applications using it are able to update the Forms fast. Therefore, it is recommended to use Windows Vista as it is really good at updating UI controls.
I'm using LINQ to select particles and springs, so .NET Framework 3.0 or later is required.
Physics
I'm going to run through the basics of my simple Physics implementation. It is pretty straightforward, but I guess parts of it can be quite confusing for people with little or no knowledge of vector math or Newtonian Physics simulations using Euler integration. My aim has been to abstract away the internals of the Physics implementation so that everyone can build UIs using this layout manager regardless of prior Physics knowledge (although it will require you to understand how rubber bands and gravity work :)).
Particle
The most basic entity in my Physics implementation is the Particle
class. It represents a point-like entity in 2D space that can have a mass. Other properties of the Particle
are position, velocity, and force. Position and velocity are, of course, the particle's current position and velocity; force is used during integration (I'll explain this in more detail in the chapter ParticleSystem), and is the sum of all the forces acting on the particle. Another property is a Control
member; if this is set to something other than null
, this means that whenever the Particle
's position changes, it also changes the position of the Control
. The actual Particle
is never rendered to screen, unlike Spring
s which are rendered (if enabled).
The reason that the Control
is an optional property (it can be null
) is that although you can't actually see Particle
s, they're still used to build up the spring systems as middle links or anchor points.
The mass of a Particle
determines how it is affected by forces; the heavier it is, the harder it is to move, and if the mass is set to Single.PositiveInfinity
, the Particle
s are immovable (at least from the ParticleSystem
's point of view, it can still be moved by a user dragging its Control
).
The Particle
also holds an internal temporary state in a ParticleState
member; this is used by the ParticleSystem
during calculations to store the derivatives.
The Particle
class is implemented like this:
namespace Bornander.UI.Physics
{
public delegate void ParticleMovedEventHandler(Particle particle);
public class Particle
{
public event ParticleMovedEventHandler Move;
public Particle(float mass, Vector position)
{
this.Mass = mass;
this.Position = position;
this.Velocity = Vector.Empty;
this.Force = Vector.Empty;
}
private void FireMoveEvent()
{
if (Move != null)
Move(this);
}
public void AddForce(Vector newForce)
{
Force += newForce;
}
public void ResetForce()
{
Force = Vector.Empty;
}
public void SnapControl()
{
if (Control != null)
{
Control.Location = new Point(
(int)Position.X - Control.Width / 2,
(int)Position.Y - Control.Height / 2);
}
}
public void MovePosition(Vector delta)
{
if (!Single.IsInfinity(Mass))
Position += delta;
}
public void SetPosition(Vector position)
{
this.Position = position;
FireMoveEvent();
}
public float Mass
{
get; set;
}
public Vector Position
{
get; set;
}
public Vector Velocity
{
get; set;
}
public Vector Force
{
get; set;
}
public ParticleState State
{
get; set;
}
public Control Control
{
get; set;
}
}
}
Spring
A Particle
not attached to anything would just fall off the screen because of gravity, and that's where the Spring
s come in. The Spring
class is a representation of a spring that is connected to two Particle
s. The Spring
has three properties (apart from those associated to Particle
s); rest length, spring constant, and damping constant.
The rest length is the length that the Spring
wants to have, meaning that if no other forces are acting upon the Spring
's two Particle
s, the spring would move the Particle
s until they're a distance of Spring.RestLength
apart.
The spring constant is a value that describes the stiffness of the Spring
; the higher this value is, the stiffer the Spring
is.
The damping constant is a value that describes how much internal damping the Spring
should apply; the higher the value, the more damping. This is used to prevent erratic movement.
The Spring
itself knows how to calculate which forces to add to its Particle
s. The ParticleSystem
calls Spring.Apply
during integration to have the Spring
apply its forces. The implementation of the Spring.Apply
method might look a bit scary for people with no prior knowledge of vector math. It basically implements the function described here, but in two dimensions instead of one.
Example:
The red particle hangs in a (purple) spring from an immovable orange particle. The black arrow represents the current velocity of the red particle. The blue arrow is the sum of the forces acting on the particle spring force plus gravity. The spring is, in this case, stronger than gravity, and pulls the red particle towards the orange one. The green arrow represents the red particle's velocity after integration.
The Spring
class is implemented like this:
namespace Bornander.UI.Physics
{
public class Spring
{
public Spring(Particle from, Particle to, float restLength,
float springConstant, float dampingConstant)
{
this.From = from;
this.To = to;
this.RestLength = restLength;
this.SpringConstant = springConstant;
this.DampingConstant = dampingConstant;
this.SpringPen = new Pen(Brushes.DarkBlue, 2.0f);
}
public void Apply()
{
Vector deltaX = From.Position - To.Position;
Vector deltaV = From.Velocity - To.Velocity;
float term1 = SpringConstant * (deltaX.Length - RestLength);
float term2 = DampingConstant * (Vector.Dot(deltaV, deltaX) /
deltaX.Length);
float leftMultiplicant = -(term1 + term2);
Vector force = new Vector(deltaX.X, deltaX.Y);
force *= 1.0f / deltaX.Length;
force *= leftMultiplicant;
From.Force += force;
To.Force -= force;
}
public void Render(Graphics graphics)
{
graphics.DrawLine(
SpringPen,
(int)From.Position.X,
(int)From.Position.Y,
(int)To.Position.X,
(int)To.Position.Y);
}
public Particle From
{
get; set;
}
public Particle To
{
get; set;
}
public float RestLength
{
get; set;
}
public float SpringConstant
{
get; set;
}
public float DampingConstant
{
get; set;
}
public Pen SpringPen
{
get; set;
}
}
}
ParticleSystem
The ParticleSystem
is what keeps track of all the Particle
s and Spring
s and is responsible for doing the integration. The integration is when the next step in the simulation is calculated, and it performed in a series of steps:
- Set all forces acting upon all
Particle
s to zero.
- Add a gravitational force to each
Particle
.
- Add a drag force to each
Particle
.
- For each
Spring
, calculate and add forces to its associated Particle
.
- Store an updated
ParticleState
containing the derivate just calculated for each Particle
.
- Multiply the
ParticleState
with the elapsed time for each Particle
.
- Update the current position and velocity with the derivates for each
Particle
.
This is calculated once per simulation frame. This whole procedure might sound complicated, but what happens is basically this: figure out what forces are acting upon a Particle
, calculate the difference in position and velocity that those forces mean, and then add those differences to the Particle
's current position and velocity. In order to estimate the next step, the implementation uses Euler integration, which is not very accurate at the time steps used, but good enough for this particular implementation.
Since things like gravity and drag are global to the simulation, these are owned by the ParticleSystem
, and are exposed as properties:
namespace Bornander.UI.Physics
{
public class ParticleSystem
{
#region Private members
private List<Particle> particles = new List<Particle>();
private List<Spring> springs = new List<Spring>();
#endregion
public ParticleSystem()
{
this.DragFactor = 0.75f;
this.Gravity = new Vector(0.0f, 20.0f);
}
public void CalculateDerivative()
{
foreach (Particle particle in particles)
{
particle.ResetForce();
particle.AddForce(Gravity);
Vector drag = particle.Velocity * -DragFactor;
particle.AddForce(drag);
}
foreach (Spring spring in springs)
{
spring.Apply();
}
foreach (Particle particle in particles)
{
particle.State = new ParticleState(particle.Velocity,
particle.Force * (1.0f / particle.Mass));
}
}
public void DoEulerStep(float deltaTime)
{
CalculateDerivative();
foreach (Particle particle in particles)
{
particle.State.Position *= deltaTime;
particle.State.Velocity *= deltaTime;
particle.Position = particle.Position + particle.State.Position;
particle.Velocity = particle.Velocity + particle.State.Velocity;
}
}
public void Render(Graphics graphics)
{
foreach (Spring spring in springs)
{
spring.Render(graphics);
}
}
public float DragFactor
{
get; set;
}
public Vector Gravity
{
get; set;
}
public List<Particle> Particles
{
get { return particles; }
}
public List<Spring> Springs
{
get { return springs; }
}
}
}
SimulationPanel
To make it easy to use this Physics based layout manager, all of the above are handled by a SimulationPanel
that inherits from System.Windows.Forms.Panel
. This means that adding a panel using this type of layout is as easy as dragging and dropping a component using the Visual Designer. A timer for updating the simulation is built in to the SimulationPanel
, so that also is taken care of for you. Unfortunately, there's currently no way to visually create the system of Spring
s and Particle
s, but this is the scope for part 2 of this article.
The SimulationPanel
is the main contact point for modifying the entities in the simulation. To add three Particle
s (two immovable ones and a moving one in the middle connected to the immovable ones with springs), you simply do:
...
Particle leftAnchor = new Particle(Single.PositiveInfinity, 0.0f, 100.0f));
Particle rightAnchor = new Particle(Single.PositiveInfinity, 200.0f, 100.0f));
Particle center = new Particle(5.0f, 100.0f, 50.0f);
Spring leftSpring = new Spring(leftAnchor, center, 25.0f, 3.0f, 2.0f);
Spring rightSpring = new Spring(rightAnchor, center, 25.0f, 3.0f, 2.0f);
ParticleSystem particleSystem = simulationPanel.ParticleSystem;
particleSystem.Particles.Add(leftAnchor);
particleSystem.Particles.Add(rightAnchor);
particleSystem.Particles.Add(center);
particleSystem.Springs.Add(leftSpring);
particleSystem.Springs.Add(rightSpring);
simulationPanel.RenderParticleSystem = true;
...
After this, you need to initialize the simulation and start it:
...
simulationPanel.OwnerForm = this;
simulationPanel.Initialize();
simulationPanel.StartSimulation();
...
By setting the SimulationPanel.OwnerForm
property, the simulation panel can listen to the Form.Move
event. It does this so that when you move the window, the particles move correspondingly. This means that you can swing your controls by rocking the window.
The SimulationPanel.Initialize
method iterates over all the child controls, and adds mouse listeners to them so that they can be moved using the mouse. This step is not required if you do not want to be able to move the controls yourself.
The last step starts the simulation by starting an internal Timer
.
If you want Control
s associated with the center particle, for example, you would have to explicitly set this using the Particle.Control
property:
Particle center = new Particle(5.0f, 100.0f, 50.0f);
center.Control = new System.Windows.Forms.Button();
Vector
I decided to implement my own 2D vector to remove any dependencies to DirectX or XNA. I am not going to go into details about this class, because there are tons of sources on the Internet that describes 2D vector math better than I ever could.
Example Application
To test out this layout manager, I decided to create an application where I let users download stuff from the Internet by grabbing and pulling the items free from an elastic "clothes line". The example application is an application that lets the user download items from the featured contents of Windows Vista Sidebar Gadgets. It does this by reading the RSS stream, getting the preview picture into a control that is associated with a Particle
that is connected to a series of Spring
s at the top of the screen. Then, by dragging the preview pictures downwards on the screen, the application cuts the Spring
s from the top line, and when the control is dropped, the bottom line (which represents some form of "shopping basket") creates Spring
s that attaches to the Particle
there by pulling it down to the bottom line.
Then, users can just click Download and the .gadget files are downloaded to a location of their choice. I admit this might not be the most useful application in the world, but it certainly looks cool, and I think it is a neat way of downloading stuff.
Making Springs Snap
There's unfortunately no magic behind the snapping "clothes line" in the example application; the Spring
does not have a built in durability that causes it to snap if stretched too much. I'm sorry to say it all has to be done "by hand" by checking the coordinates of the Particle
as it is being dragged by the mouse. The Particle
exposes an event called Particle.Move
that is fired whenever its position is changed by the method Particle.SetPostion
. This method is called by the SimulationPanel
when a Particle
is dragged. By listening to this event, the example application can check if a particle has been dragged to the lower half of the panel, and in that case, remove the Spring
s holding it in place:
class GadgetDownloadForm
{
...
private void HandleParticleMoveByDrag(Particle particle)
{
if (sourceItemPickSystem.Particles.Contains(particle))
{
if (particle.Position.Y > (simulationPanel.Height / 2) + particle.Control.Height)
{
sourceItemPickSystem.RemoveParticle(particle);
}
}
}
}
The variable sourceItemPickSystem
in the snippet above is a helper class that represents a "clothes line" on which you can hang and remove Particle
s. It handles recalculation of the Spring
's rest length when based on the width of the SimulationPanel
and the number of Particle
s currently hanging from the line.
Reading RSS Feeds
The code used for reading RSS feeds is basically a slightly modified and cleaned-up version of the code that is generated when you create a Visual Studio project of type Screen Saver Starter Kit.
To download the actual .gadget file, the page which the Link
element of the RSS item refers to is loaded and parsed (in a rather ugly but functional way), then a WebClient
is used to download the file.
Future Improvements
The things I plan to add for second part of this two part series are:
- WPF port so that it can be used with WPF instead of WinForms.
- A WYSIWIG Visual Designer style application that allows you to create the spring-particle systems.
Points of Interest
If you haven't tried any Physics coding before, I believe this application is a really simple point to start at.
The way the SimulationPanel
listens to global, cross component mouse events is similar to what I did in this article.
On the subject of LINQ and the SQL style syntax additions to C#: I was not entirely convinced that the addition of the LINQ keywords to the C# language is a great idea as I felt it's bloating the language. I didn't mind the addition of LINQ, that's all good, but I was skeptical about adding support for the use of a set of classes at language level. After using it a bit though, I must say that it is extremely easy to use and very intuitive. I'm still not convinced that it is not bloating the language, but I do not care any more. Me likes it.
All comments are most welcome.
History
- 2007-12-03 - First version.