Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Fun with Physics: Implementing a Physics based layout manager

0.00/5 (No votes)
8 Dec 2007 2  
An article on how the use of a simple Physics implementation can liven up a WinForms UI.

Screenshot - FunWithPhysics.jpg

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 Springs 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 Particles, 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 Particles 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 a Control is associated with this Particle then snap 


            // the Controls location so that it centers around the Particle


            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 Springs come in. The Spring class is a representation of a spring that is connected to two Particles. The Spring has three properties (apart from those associated to Particles); 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 Particles, the spring would move the Particles 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 Particles. 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:

Screenshot - Forces.jpg

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 Particles and Springs 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:

  1. Set all forces acting upon all Particles to zero.
  2. Add a gravitational force to each Particle.
  3. Add a drag force to each Particle.
  4. For each Spring, calculate and add forces to its associated Particle.
  5. Store an updated ParticleState containing the derivate just calculated for each Particle.
  6. Multiply the ParticleState with the elapsed time for each Particle.
  7. 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) 
            {
                // Clear all existing forces acting on the particle


                particle.ResetForce();

                // Add a gravity force


                particle.AddForce(Gravity);

                // Add world drag


                Vector drag = particle.Velocity * -DragFactor;
                particle.AddForce(drag);
            }
            
            foreach (Spring spring in springs) 
            {
                // Apply what ever forces this spring holds


                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 Springs and Particles, 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 Particles (two immovable ones and a moving one in the middle connected to the immovable ones with springs), you simply do:

    ...
    
    // Create to fixed particles acting as immovable anchors, notice the 
    // use of Single.PositiveInfinity to create a really heavy particle.


    Particle leftAnchor = new Particle(Single.PositiveInfinity, 0.0f, 100.0f));
    Particle rightAnchor = new Particle(Single.PositiveInfinity, 200.0f, 100.0f));
    
    // Create a particle in the middle of the the two anchors


    Particle center = new Particle(5.0f, 100.0f, 50.0f);
    
    // Create the two springs and attach them to the particles


    // The rest length is set to 25.0 so the spring will pull on the center particle


    Spring leftSpring = new Spring(leftAnchor, center, 25.0f, 3.0f, 2.0f); 
    Spring rightSpring = new Spring(rightAnchor, center, 25.0f, 3.0f, 2.0f); 

    // Grab a reference to the particle system and add the entities


    ParticleSystem particleSystem = simulationPanel.ParticleSystem;
    
    particleSystem.Particles.Add(leftAnchor);
    particleSystem.Particles.Add(rightAnchor);
    particleSystem.Particles.Add(center);
    
    particleSystem.Springs.Add(leftSpring);
    particleSystem.Springs.Add(rightSpring);
    
    // Enable rendering of the springs


    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 Controls 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 Springs at the top of the screen. Then, by dragging the preview pictures downwards on the screen, the application cuts the Springs from the top line, and when the control is dropped, the bottom line (which represents some form of "shopping basket") creates Springs that attaches to the Particle there by pulling it down to the bottom line.

Screenshot - FunWithPhysics2.jpg

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 Springs 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 Particles. It handles recalculation of the Spring's rest length when based on the width of the SimulationPanel and the number of Particles 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here