I’ve recently encountered several demos on various parts of the web featuring a rather interesting particle effect. The effect is what appears to be a random population of particles with unique velocities. When a particle encounters another within some proximity, a connecting line is drawn between them, attaching the two particle centers. The overall effect appears similar to a dynamic display of constellations constantly re-arranging their patterning, or strands of a spider web connecting and pulling itself apart.
A family member had asked me to describe the technology being used in one such demo backdropping a website banner. His first guess was an animated GIF or PNG of some sort. Upon further inspection, my intuition proved correct. The banner’s effect leveraged an absolutely positioned HTML canvas. One that responds to window resizing to display responsively. Using such an effect as a backdrop to the banner-in-question elevated the content to appear quite tech-savvy, leaving quite a positive impression on me.
First thing to do is set up a canvas element in our HTML. We set a default width and height attribute, which we will change dynamically upon our script initialization. Setting the dimension attributes to zero ensures the canvas will not display if the script initialization fails.
We can wrap the canvas in a containing element which can be styled responsively. This will allow us to easily adjust the canvas dimensions to its parent element’s bounds during a window resize by calculating its wrapper’s bounds.
This will give our parent container elasticity to fill the entire container width while setting a fixed height. Note that you can just as easily allow internal content to determine the height of the parent element by setting the container height property to
We want to position our canvas absolutely and lock it to the top-left corner of its parent container. Any additional banner content should be added inside the container but after the canvas element.
Our canvas dimensions still need to be set after the page loads and bounding properties of our containing element has a chance to calculate. Adding some script to handle our initialization and window resize events to allow for responsive stretching is straight-forward enough:
pageInit() is fired when the page resources have loaded.
canvas is given a value during initialization, and
adjustCanvasBounds() is manually invoked to initially set the canvas bounds to match its parent’s. A window resize event will trigger the same function to update canvas size to ensure its ‘responsivity’.
We have a few other things about our canvas to consider. Whenever we create or resize a canvas we intend to draw on,
window.devicePixelRatio needs to be accounted for. This indicates a pixel ratio for certain displays (such as retina/quad-HD displays) which determines how many actual canvas pixels we have available to utilize. Without this adjustment, objects rendered on our canvas may appear blurry on devices with a pixel ratio > 1. We can accomplish this with an additional function call that happens any time we need to re-size the canvas.
Rather than exposing some of these variables and functions to the top-level scope, I prefer structuring a stage object which contains a reference to the canvas, a rendering context, and some methods that allow for common interaction with the canvas. An initial state might look something like the following:
Much of the code in the
Stage object is a re-hash of what we already wrote in its more functional form. A canvas reference is established from the constructors main parameter, and a canvas context is defined, exposing an avenue to the canvas’s rendering API.
_adjustCanvasFidelity() now live in the scope of the constructor and is exposed with a
resize() public method. The rest are getters that return private variable values in read-only form.
With particles ultimately moving around on-stage, the Stage object will need some way to manage animation. To achieve this, a loop must be created to tell the stage’s canvas to redraw each particle position at some given time coefficient or time increment. Ideally our rendering loop should support pausing & resuming. Our looping logic doesn’t have to explicitly define specific rendering instructions within the loop itself. Passing a reference to a function defining those steps is adequate enough.
Focusing mainly on the animation controller inside of
drawMethod() ultimately defines a series of specific rendering operations to perform during each available animation frame. We are using the
window.requestAnimationFrame object to space each loop sequence to delay additional frame rendering to a time when the browser is ready. This greatly reduces hiccups and inconsistencies in framerates, allowing the rendering logic and screen drawing to remain synchronous.
Before the loop begins, a starting timestamp is cached. Function
intervalMethod() is defined, which essentially calls a yet-to-be defined method
clear(), which is responsible for wiping the canvas’s contents each frame before a new frame is rendered. Without clearing the canvas, successive calls will paint ontop of the existing pixel data.
The loop function is immediately invoked, creating its own scope, where the most current timestamp is cached, and the ‘tick’ is calculated from the difference between the most recent timestamp and the second most recent timestamp. The
intervalMethod is invoked, ultimately triggering the argument-supplied
renderMethod(), providing both a context and a tick time as arguments. The handler method will need those references to properly calculate new positions for each particle, and a context to which they will be rendered. Notice
this keyword is bound to the function as other member methods are referenced within the function scope, ensuring that
this keyword remains bound to the same reference upon successive rendering loops.
paused flag is evaluated to determine if the loop should recurse or not. If so, the requestAnimationFrame alias
requestFrame() is invoked, with the loop function as an argument. When invoked,
requestFrame returns a reference ID. Storing this ID will allow us to later cancel the request if an interruption occurs before the next render cycle takes place.
Expanding on the methods of property
render are state-controlling functions for pausing and resumption of the rendering loop, and a convenient method to handle a complete pixel data wipe of the canvas.
pause() is invoked, the cached requestID reported from the alias
requestFrame() method is used as an argument to
cancelAnimationFrame(), preventing any pending requests from executing. Method
resume() clears the flag and invokes the loop method to begin again.
With these handler methods, we have a complete stage ready to handle rendering of our particle logic. So the next logical step is to define precisely what a particle is.
Like our Stage object, a particle would best be represented as an object with some inherent properties and methods. We use the
new keyword to create a new instance of the object along with some unique properties for that instance.
Lets define some basic information that a particle animating on our canvas might require:
- positional data ( [x,y] offsets )
- dimensions or radius
- a center point if different from positional data
- velocity ( both direction and speed )
- an influence threshold
With this list in mind, we can reserve some private variables within our object constructor’s scope as placeholders for these currently unknown values. We can assume these properties will be determined upon object instantiation, and passed as an argument as a dictionary or object literal format.
The Particle constructor props parameter can certainly be referenced via object notation to access its property values at any time. To cut down on any chance of that access to impact performance on a larger scale, I’ve chosen to cache those values in private variables within the constructor. This code also assumes all required properties are present in the parameter object.
The x and y position values are exposed as read-only properties, as well as several methods with different function.
influencedBy() compares the distance of some point at [a,b] to its own [x,y] position and returns a ratio of ‘closeness’, or strength of influence from that point. The closer the point in question, the higher the ratio value, ranging from a scale of 0-1. This ratio will allow ease of calculations for values of variance, such as connecting line-width and opacity.
render() contains a series of operations that are responsible for rendering the particle to an argument-supplied context. For more details on canvas rendering API, I recommend MDN’s thorough documentation on the subject.
setPosition() updates the particle instance’s position over a certain time delta provided as an argument. The formula uses both speed and theta properties to determine how far the particle has traveled after n milliseconds. Edge cases provide a canvas looping mechanism for the particle, so disappearing from one edge of the canvas will allow it to reappear on the opposing boundary. The method only updates the x and y property values, leaving rendering responsibilities out of scope of its purpose.
Now that we have both Stage and Particle components, some controlling logic is required to pull the pieces together. A list form of actions not-yet built include:
- particle generation with randomized properties
- populating and managing an array of particle instances
- a rendering routine for all particles as part of the Stage’s renderMethod
- drawing connective lines between two particles within each other’s influence
I intend to create a
ParticleGroup object that would manage these higher-level tasks in some well-defined structure, something resembling the following:
options object is being referenced, which is a dictionary containing certain property values collected in one convenient location. These values are being cached as private variables for quick reference inside the constructor’s scope.
The constructor has a private array
_collection to store the particle instances once method
populate() is invoked. Method
add() pushes either a supplied instance or a newly generated particle to the collection via private function
_generateNewParticle(). Public method
render() contains the logic to loop through instances within the collection to first position them after some time offset, render the instance onto the stage, and check for neighboring particles via private function
_checkForNeighboringParticles(). If one or more particles are identified,
_connectParticles() is responsible for rendering a connecting line between two points.
Taking a closer look at the rendering loop, I’ve written a solution that looks like this:
My First reaction when writing this was, “That’s potentially a lot of looping”. To ensure that all these loops are as efficient as possible, loop variables are aggressively cached.
The initial loop sets a flag property to false to indicate that it has not been thoroughly ‘checked’ for neighboring particle connections. This has to be reset during each loop. Additionally, each particle’s position is repositioned at time offset t. Finally the particle is then rendered on-stage.
The second loop takes place after positioning and rendering of each particle has taken place. Each particle is then checked to see what other particles fall within its sphere of influence via private function
_checkForNeighboringParticles(). A context is forwarded, provided as an argument from the
stage.render() method. the particle is then flagged as ‘checked’ so future cycles through the neighbor-checking loop can skip redundant comparisons with them.
Taking a closer look at the neighbor-checking function:
Again, loop variables are cached whenever possible, as well as p1’s
influenceBy() method. When looping through unknown number of iterations, property access and in particular prototype traversing can be costly.
The collection is iterated (again) and non-checked particles are compared against p1 to see if the hypoteneuse between their centers is less than p1’s sphere of influence. This ratio is reported back to variable
d, and if non-zero, private function
connectParticles() is invoked to render a line between the two particle’s center points on the provided context.
Our init function has to be slightly reconfigured to instantiate each of our objects. The
ParticleGroup instance should populate its collection of particle instances with the
populate() method. Afterwards, the
Stage instance render loop needs to start via
Stage.render.begin(). The reconfigured
init() function looks as follows:
The resulting demo creates quite a pleasant effect, with easily editable options which control properties of interest such as particle count, spheres of influence radius, velocity and size ranges, and color definitions.