Note that you can click the Babel button on any of the demos below to display a tab with the source code. You can also click Edit on CodePen to open the demo in a new window and from there you can Fork the demo which means you create your own copy that you can change as you like and save for later. Please feel free to do some remixes!
First we need to define some concepts.
What is a vector?
You can think of it as an arrow. It has a certain length (magnitude) and a certain direction (angle). Example: velocity can be represented by a vector, the speed is the length of the vector and direction is the angle of the vector. Another way to think about it is a point (x, y) measured from origin. In the illustration below you can see both notations.
There's vector math. Yay! You can add vectors together, you can subtract vectors from each other, you can calculate the distance between two vectors, you can calculate the angles between two vectors. Lets say we have a physical object that can move, we can represent its position with a vector, its velocity with a vector and its acceleration with a vector. Given the objects acceleration we can update the objects velocity and position with the following vector math:
// Pseudo Code acceleration = currentAcceleration velocity = velocity + acceleration position = position + velocity // Using a vector math lib acceleration = currentAcceleration; velocity.add(acceleration); position.add(velocity);
So what does it mean and what happens if the acceleration is 0? That means that there is no change in speed, the object would move with constant speed.
Assume the object is moving forward, that means it has positive velocity. How would we make the object stop moving? We would need to get its velocity to become zero by setting the acceleration to something negative (deacceleration) for a while. When the velocity has become zero we need to stop the negative acceleration - otherwise the velocity would become negative and the object would start going backwards.
Read more about vectors on Wikipedia.
What is a flow field?
Sometimes called a vector field. A surface where each point has a vector assigned to it. Can be used to display wind direction and wind speed on a map.
Each symbol tells us the wind speed and direction at that particular location. The symbols are called wind barbs. The direction of the pole indicates the wind direction and the wind speed is indicated by how many flags there are on the pole.
Image from Meteoblue
Read more about vector fields on Wikipedia.
What is Perlin noise?
It is gradient noise. You can think of it as a smooth random function. When Ken Perlin was working with the special effects for the movie Tron in the 80's he was not satisfied with the looks of computer graphics so he developed this technique for producing gradient noise with natural appearance. It can be used to generate terrain with mountains and valleys, textures, smoke, clouds etc. He even won an Academy Award for the technique several years later.
In the Pens below you can see the difference between Perlin noise and random values.
First we have 1D Perlin noise, that means we have one parameter. Lets draw a line. We let x begin at zero, left border of the canvas, and increase to the right border of the canvas. We use x as parameter to the noise function and use the return value to decide y. Because the noise function always returns a value between 0 and 1 we multiply with the desired height. Now we can draw a line to the point (x, y). Then we increase x with one and calculate a new y and draw a line to the new point, and so on. We can clearly see that the noise curve is much smoother than the random curve. Already now we can see the natural appearance, don't you think it looks a bit like a mountain ridge? Click the RERUN button a few times.
let y = noise(x) * height; lineTo(x, y);
Then we have 2D Perlin noise which means that we have two parameters. We use the current point (x, y) as parameters to calculate the grayscale value we use to draw the point. This time we multiply with 255 because the grayscale value must be between 0 and 255.
let value = noise(x, y) * 255; color(value); point(x, y);
We can also use 2D noise to animate the 1D noise curve from the first example. Combining one geometric dimension with time gives us two dimensions.
let y = noise(x, time) * height; lineTo(x, y);
Click Run Pen.
3D Perlin noise with three parameters can be used to animate 2D noise. We use the current point (x, y) as the two first parameters. The third parameter is time.
let value = noise(x, y, time) * 255; color(value); point(x, y);
Click Run Pen.
We could go on and generate a static 3D noise image, it would be a 3D cloud. In the next step we would use 4D noise to animate the 3D noise cloud. (Three space dimensions and time is usually referred to as spacetime.) We could even continue further but then it becomes too abstract for my taste, but you get the idea. However, there is a problem with Perlin noise when moving up in dimensions: the complexity increases exponentially. Complexity for dimension n is O(2^n), BAD! 😰.
What is Simplex noise?
Simplex noise is an improvement of Perlin noise both in terms of computational speed and visual appearance also developed by Ken Perlin. The complexity for dimension n is O(n^2), not so bad 😐.
And that is why I opted for Simplex Noise in the following experiments. What happens if we control a flow field with Simplex noise? You're soon to find out!
My first step was to create and visualize the flow field.
How many vectors should be in the field?
On a typical computer screen there are millions of pixels, bet we don't need to have a vector for each pixel, we can cheat and create a grid with cells that are 20x20 pixels big. That way we save memory - how many vectors we need to keep track of and we save computations - how many vectors we need to update (and later on apply force to each particle).
How do we create gradient noise?
// Initialize noise.seed(Math.random()); ... // value is between 0 and 1 let value = noise.simplex3(x, y, time);
But the noise function returns a single value!
To be able to create each vector in the field we need two values: length and angle. In the animation we have three inputs to the noise function: x, y, and time. As you saw above the noise function only gives one output value. Therefor we need to call the noise function twice, one for angle and one for length. To get different patterns for angle compared to length we use different zoom factors (1/50 and 1/10) and a big offset (40000).
let angle = noise.simplex3(x/50, y/50, noiseZ) * Math.PI * 2; let length = noise.simplex3(x/10 + 40000, y/10 + 40000, noiseZ); field[x][y] = [angle, length];
As you can see we represent the flow field with a three dimensional array:
field[x][y][vector]. The angle of the vector at position (x, y) is
field[x][y] and the length is
If you look close on the animation below, you can see that the length of the vectors change in a different pattern compared to the angle thanks to the different zoom levels and x and y offset mentioned above.
My second step was to release particles into the flow field and let the field apply a force to each particle. Two new building blocks were needed: vector math and particles.
let v1 = new Vector(2, 3); let v2 = new Vector(4, 5); v1.addTo(v2); console.log(v1.x); // 6 console.log(v1.y); // 8
Getting the length (magnitude) of a vector:
let v1 = new Vector(3, 4); let result = v1.getLength(); console.log(result); // 5
I made a Particle class with three vector properties: position, velocity and acceleration. The position is the (x, y) coordinate of the particle. The velocity is how fast and in which direction the particle is moving. (As we saw earlier, a vector can be used to represent either a 2D point or an angle and length.) The acceleration is how fast and in which direction the velocity is changing. Another important property of the particle is that it wraps around the screen. If it moves out of sight to the right it re-appears from the left. You can see it in detail if you click the Babel tab in the embedded Pen below.
In the update loop for each particle we:
- draw it
- get the nearest vector from the flow field, which is achieved with a neat vector math trick: divide the position vector with the grid size, round the result and there we have the x and y index to be used to as index to our flow field array:
- apply the force (vector) to the acceleration of the particle
- move it
- wraps it's position around the screen if needed
Notice that the particles follows the direction of the flow field, and their different behavior when they are close to long (strong) vectors compared to weak (short).
One last detail: I use the following trick to get an equal density of particles regardless of the size of the canvas. The number of particles is set to the resolution divided by one thousand. I'm trying to always give the animation the same look and feel.
let numberOfParticles = canvas.width * canvas.height / 1000;
More info about vectors and physics:
Vectors - The Nature of Code by Daniel Shiffman (a Youtube playlist)
Physics by Keith Peters (the first four videos in the Youtube playlist)
My third step was to not display the flow field anymore and to let the particles leave trails behind them. Not displaying the field is easy, just remove the call to
drawFlowField()! Duh! Trails are accomplished by not clearing the canvas at all and by drawing the particles with a semi transparent color - an alpha value below 1. In this particular case we use 0.2. Click Rerun in the bottom right corner to re-seed the noise and start over again.
The animation looks better in Full Page mode. Refresh the Pen in your browser to re-seed the noise and start over again.
Because exploring is my favorite thing to do, I just had to make it tweakable. In the Quest for the Perfect Configuration I added two QuickSettings menus where many of the parameters can be played around with. Double click the title of the menu to hide it. Use the Pause/Resume button if you would like to save a still image. In Firefox and Chrome you can right click the canvas to store the current frame to a .png file.
Better in Full Page mode!
Update October 25th
I recently hit 500 followers on CodePen and decided to make a "500 Followers Flow Field". Drawing text on the canvas and then storing the pixel values for later use is a trick I've used numerous times before for text effects, because it's a good one. Here I combine it with the flow field.
- Use a blank canvas, it will be transparent black
- Draw text on the canvas
- Read the pixel values from the canvas into an array. Only pixels that forms the text will result in a value in the array, the untouched parts that are still transparent black will not result in any value in the array.
- Clear the canvas, we have what we need in our array.
- The rest of the Pen works as before but when calculating the flow field, for the pixel positions where there is text (indicated by our array) - instead of using a simplex noise value, pick a random length and a random angle.
Notice the explosion like effect when the particles hits the edge of a digit. This is caused by the random forces that we put there.
Watch in Full Page Mode.
Once again, I'm amazed by how relatively easy it is to build something that looks so complicated and cool (excluding vector and noise lib, the code for the last demo is under 200 lines). I hope you could follow along. Please write a comment if you have any feedback. Please post a link if you create a remix of one of my Pens or if this inspired you to create a Pen.
I keep all these Pens above and a few more in a collection: My Flow Fields
More Perlin noise can be seen in my collection Dripping.
More of my CodePen stuff in general on my CodePen profile.