Friday, February 19, 2021

Firemonkey Particles

We see images with connected points used a lot in diagrams, illustrations, advertising and just cool abstract art. They visually represent networks of related things and ideas or connectivity. The web, for example. It's effective and visually appealing and when the effect is animated, it's mesmerising. And popular. There are any number of animated backgrounds and JavaScript libraries to add moving particles to web pages, including the very appropriately named particles.js.

I'm going to make a Delphi version using FireMonkey.



Basically, we need a simple, two-dimensional particle system. An array of "particles", each of which has a randomly assigned position and velocity. For every frame of the animation, each particle's position is updated based on its velocity and particles that are close enough together are connected with lines, giving that signature constellation effect.

Source code for the example projects is available here and is compatible with the Delphi Community Edition.


Using TParticles

Include uParticles in your uses clause, declare a TParticles field and instantiate it in the form's constructor, passing in the control you want to draw on as a parameter. Any control with a TCanvas will work. So basically, all of them. I used a TPaintBox.

A TTimer repaints the TPaintBox every 16ms (around 60 frames per second) and calls FParticles.Update and FParticles.Paint in the TPaintBox's OnPaint event.

TParticles has a reference to the parent control, so it handles any mouse events and knows the control's size, even if it's resized. We don't have to pass it any additional information


    FParticles: TParticles;

  ...

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  FParticles := TParticles.Create(PaintBox1);
end;

  ...

procedure TfrmMain.PaintBox1Paint(Sender: TObject; Canvas: TCanvas);
begin
  FParticles.Update;
  FParticles.Paint;
end;


And since so little code is required in the form, I couldn't resist...




TParticles

All of the interesting stuff is in uParticles.pas. For flexibility, several of the properties are configurable instead of being hard coded.


TArray<TParticle>

TParticles maintains an array of TParticle.

As I wrote earlier, TParticle doesn't keep track of much. Just the current position and velocity.

Internally, a vector is stored as just two values, so you could get away with using a point (and lots of people do), but explicitly declaring Velocity as a TVector implies that it has magnitude (speed) and direction and makes the intent more obvious to anyone reading the code.

Annoyingly, the implicit conversion from TVector to TPointF and the explicit ToPointF function are deprecated and you need to explicitly cast the vector when adding to or multiplying by a TPointF type.


  TParticle = record
    Position: TPointF;
    Velocity: TVector;
    procedure Init(const AMaxSize: TSizeF);
  end;


ParticleCount

As the name suggests, this is the number of particles being drawn. If the count goes down, the array is truncated. If the count goes up, new particles are added and initialised with a random starting point and velocity.


LineDistance

A line is drawn between each particle and any other particles that are within this distance.


BackgroundColor and LineColor

These seem pretty self-explanatory.


MouseActivity

Particle libraries commonly have some kind of interactivity with the mouse. In our case, particles can ignore the mouse (default), be repelled by it or attracted to it.


MouseRadius

When particles interact with the mouse, it's within this radius of the mouse.


Update function

This updates the state of TParticles so the next frame of the animation can be painted.

First, each particle's position is updated by adding its velocity.

Next, if particles are reacting to the mouse and the mouse is hovering over the control, a mouse force is calculated for each particle and used to update its position again. This force is calculated based on the inverse square of the particle's distance from the mouse, which means it's weaker the further away it is. Like gravity.

Finally, if any particles have moved outside of the bounds of the parent control, they are wrapped around to the opposite side.


Paint function

This draws a circle for each particle on the parent control's canvas and draws a line between any particles that are within a certain distance of each other (LineDistance).

It's a little jarring when lines pop in and out of existence at full intensity. To avoid this, each line's opacity is adjusted based on the distance between particles so that longer lines are dimmer and shorter lines are brighter. Lines fade in and out, giving the animation a smoother appearance.


Configuring TParticles

It took some playing around to come up with reasonable and interesting combinations of settings, so I made a test harness to make this easier. It was also really useful for testing and tweaking the functionality.




Conclusion

If anyone adds some features, I'd love to see what they come up with. I have some suggestions:

  • Add particles on mouse click (or tap?).
  • Image or gradient background.
  • Multiple line colours based on some rule.
  • Dynamically calculate line distance based on the control size instead of using a fixed value.
  • Random particle sizes.
  • Have particles bounce off edges instead of wrapping around.
  • Configurable particle speed.


3 comments:

Magno Lima said...

Awesome work! I was thinking why Delphi couldn't have such with FMX since a while? Nice job and thank you for sharing!!

Unknown said...

Very nice. Can't wait to check it out. My thoughts/suggestion: add elevation/z-coordinate and line color based on that... Somewhere to a relief map. Dynamic landscapes? Nice job looks really great.

Unknown said...

Doh. "Similar to a relief map"