___         ___         ___         ___                   ___         ___         ___         ___         ___         ___         ___         ___                   ___         ___         ___
     /\__\       /\  \       /\  \       /\  \        ___      /\  \       /\  \       /\__\       /\  \       /\__\       /\  \       /\  \       /\  \        ___      /\  \       /\  \       /\__\
    /:/  /      /::\  \     /::\  \     /::\  \      /\  \    /::\  \     /::\  \     /::|  |     /::\  \     /:/  /      /::\  \     /::\  \     /::\  \      /\  \    /::\  \     /::\  \     /::|  |
   /:/__/      /:/\:\  \   /:/\:\  \   /:/\:\  \     \:\  \  /:/\ \  \   /:/\:\  \   /:|:|  |    /::::\  \   /:/__/      /:/\:\  \   /:/\:\  \   /:/\:\  \     \:\  \  /:/\ \  \   /:/\:\  \   /:|:|  |
  /::\  \ ___ /::\~\:\  \ /::\~\:\  \ /::\~\:\  \    /::\__\_\:\~\ \  \ /:/  \:\  \ /:/|:|  |__ /::::::\  \ /::\  \ ___ /::\~\:\  \ /::\~\:\  \ /::\~\:\  \    /::\__\_\:\~\ \  \ /:/  \:\  \ /:/|:|  |__
 /:/\:\  /\__/:/\:\ \:\__/:/\:\ \:\__/:/\:\ \:\__\__/:/\/__/\ \:\ \ \__/:/__/ \:\__/:/ |:| /\__/:::HH:::\__/:/\:\  /\__/:/\:\ \:\__/:/\:\ \:\__/:/\:\ \:\__\__/:/\/__/\ \:\ \ \__/:/__/ \:\__/:/ |:| /\__\
 \/__\:\/:/  \/__\:\/:/  \/_|::\/:/  \/_|::\/:/  /\/:/  /  \:\ \:\ \/__\:\  \ /:/  \/__|:|/:/  \::1991::/  \/__\:\/:/  \/__\:\/:/  \/_|::\/:/  \/_|::\/:/  /\/:/  /  \:\ \:\ \/__\:\  \ /:/  \/__|:|/:/  /
      \::/  /     \::/  /   |:|::/  /   |:|::/  /\::/__/    \:\ \:\__\  \:\  /:/  /    |:/:/  / \::::::/  /     \::/  /     \::/  /   |:|::/  /   |:|::/  /\::/__/    \:\ \:\__\  \:\  /:/  /    |:/:/  /
      /:/  /      /:/  /    |:|\/__/    |:|\/__/  \:\__\     \:\/:/  /   \:\/:/  /     |::/  /   \::::/  /      /:/  /      /:/  /    |:|\/__/    |:|\/__/  \:\__\     \:\/:/  /   \:\/:/  /     |::/  /
     /:/  /      /:/  /     |:|  |      |:|  |     \/__/      \::/  /     \::/  /      /:/  /     \::/  /      /:/  /      /:/  /     |:|  |      |:|  |     \/__/      \::/  /     \::/  /      /:/  /
     \/__/       \/__/       \|__|       \|__|                 \/__/       \/__/       \/__/       \/__/       \/__/       \/__/       \|__|       \|__|                 \/__/       \/__/       \/__/
Rendering spirals and radial patterns with particles in WebGL
A WebGL experiment to render and animate spirals and radial patterns with particles, involving some trigonometry and graphing equations.
Written May 22, 2018 · Updated March 17, 2024

Trigonometry recap

Back to the trigonometry days of high school, the sine function is the foundation of plotting from polar coordinates to the Cartesian coordinates, which we're going to be doing a little bit of here.

It's simply the ratios of the sides of a right-angled triangle. The sine function being the ratio of the opposite side to the hypotenuse, and the cosine function is the ratio of the adjacent side to the hypotenuse (adjacent to the 90° angle).

As an engineer, these are functions that accept a numerical value in radians and return a value between -1 and 1. The difference between the two functions is that the sine function starts at 0 and the cosine function starts at 1.

When dealing with angles in school I only ever remember using degrees, and never heard of the word "theta" or "radians", but in the world of programming and mathematics these are what we're going to want to know and use.

It's pretty simple, radians are the unit of PI (π), π radians is equivalent to 180° (degrees), so 2π radians is equivalent to 360°. Theta is just the named variable for a value that represents an angle.

Fig. - Math.cos and Math.sin
Math.cos(theta) // X coordinate Math.sin(theta) // Y coordinate // Example Math.cos(0) // 1 Math.sin(0) // 0 Math.cos(Math.PI) // -1 Math.sin(Math.PI) // 0

There's 2 ways I find useful to visualise cosine and sine. The first is to see their wave form overlaid on each other, where the X axis is the increasing theta value (radians) and the Y axis is the the corresponding Math.sin(x) or Math.cos(x) value

Fig. - Sine and Cosine
  • sin θ: 0.00
  • cos θ: 1.00
  • θ: 0.00

The second is to see them together as a point on a circle, where theta is the angle around the circle and the value of Math.sin(x) or Math.cos(x) is the X or Y coordinate of the point on the circle. Note if the radius of the circle is not 1, then we just multiply the results of the sine and cosine functions by the radius.

Fig. - Sine and Cosine
0
  • sin θ: 0.00
  • cos θ: +1.00
  • θ: 0.00

The Archimedes Spiral

We now know how to get an x and y coordinate for a circle with a radius between -1 and 1. Let's create an Archimedes spiral (to begin with). This is a spiral that increases linearly with the angle.

Fig. - Shows code for the equation r = x and t = x * 0.01.
const r = x; const t = x * 0.01; // This will be the same for all code snippets going // forward, so it will be omitted going forward. const x = r * Math.cos(t); const y = r * Math.sin(t);

Well that was easy, we have an Archimedes spiral. However, we have 5000 particles here so lets space these out a bit more. Let's try multiplying the index by 2 so it skips 1 points along the spiral before placing the next point.

Fig. - Shows code for the equation r = x and t = x * 0.01 * 2.
const r = x; const t = x * 0.01 * 2;

That's kind of worked on the outside but the center of the spiral is still far too packed. It doesn't look like we're getting equal spacing between the particles so there seems to be a scaling problem here. Notice how by increasing the rate of the theta value, we've also gained more arms to the spiral.

Fig. - Shows chart for the equation r = x and t = x * 0.01.
  • r = x
  • t = x * 0.01

!!! Fig 404 !!! shows our linear equation y = x. Where the x axis is the index of the point and the y axis is the corresponding radius/theta values. What it shows is that while the index increases, the radius/theta increases at the same rate, and this is exactly what we don't want.

Fig. - Shows chart for the equation r = x ** 2 and t = x * 0.01.
  • r = x ** 2
  • t = x * 0.01

Using the many mathematical functions available to us, we can create equations that cause values to scale at different rates, and use those to inform our rate of change for both our variables. For example, using y = for the radius (!!! Fig 404 !!!), starts off slow and gradually increases over time. This is called a logarithmic spiral.

This isn't what we need though, in fact it's the complete opposite!

Fig. - Shows chart for the equation r = x >> 4 and t = Math.floor(x / 16).
  • r = x >> 4
  • t = Math.floor(x / 16)

Graphing equations can even be useful to help understand the bitwise operators. Like shifting a number to the right by 4 bits is the same as dividing it by 16 and flooring the result, which creates a step scale (!!! Fig 404 !!!). This creates some more definitive points that a whole bunch of our coloured particles can be grouped around.

Fig. - Shows chart for the equation r = x * (x & 4) and t = r * 0.01.
  • r = x * (x & 4)
  • t = r * 0.01

Using the & operator to get the remainder of a division by 4, which combined with multiplying by the index creates an oscillation between 0 and linear growth (!!! Fig 404 !!!), and causes our points on the spiral to form groups of 4. However, we're not fixing the issue of the center of the spiral being too dense. We're after the rate of change to be faster at the beginning and then slow down as the index increases.

Fig. - Shows chart for the equation r = Math.sqrt(x) and t = r.
  • r = Math.sqrt(x)
  • t = r

Using the square root of the index (!!! Fig 404 !!!) might not look any different at first, but at the center of spiral the spacing between the particles is less dense, but the rest are still too close together.

Fig. - Shows code for the equation r = Math.sqrt(x) and t = r * Math.PI.
const r = Math.sqrt(x); const t = r * Math.PI;

Remember earlier we scaled our value by multiplying it by 2 and the effect it had on the spacing of particles? We can do the same again and scale up our theta value. We'll also scale it up by π so that the dots all align (!!! Fig 404 !!!).

Fig. - Shows code for the equation r = Math.sqrt(x * (x & 4)) and t = r.
const r = Math.sqrt(x * (x & 4)); const t = r;

We can also apply this same square root technique to the bitwise operator equation (!!! Fig 404 !!!) to get the same oscillation effect, but with the added benefit of the center of the spiral being less dense (!!! Fig 404 !!!).

Fig. - Shows code for the equation r = x and t = x * Math.PI.
const r = x; const t = x * Math.PI;

Finally for this section, just one more pattern I came across which I found interesting is to increase the radius linearly and the theta value by π (!!! Fig 404 !!!).

Vogel spiral

Another spiral that gets a lot of attention is one that described more recently in 1979 by a mathematician called Helmut Vogel, and thus named the Vogel Spiral.

He explains that this particular spiral is the maths that drives the structure of sunflower seeds, and the number of spirals in each direction are always consecutive Fibonacci numbers.

Fig. - Shows chart and code for the equation r = Math.sqrt(x) and t = x * 2.39998131.
  • r = Math.sqrt(x)
  • t = x * 2.39998131
const r = Math.sqrt(x); const t = x * 2.39998131;

Notice we're still using the square root of the index for the radius (!!! Fig 404 !!!), but we're multiplying the theta value by a number that appears to be a random number, but this is actually produces something called the Golden Angle , which is linked to the ratio of consecutive Fibonacci numbers.

Fig. - Golden angle calculation
Math.PI * (3 - Math.sqrt(5));

We can play around with this multiplier to get slightly different results (!!! Fig 404 !!!).

Fig. - Shows code for the equation r = Math.sqrt(x) and t = x * 1.5.
const r = Math.sqrt(x); const t = x * 1.5;

Ulam spiral

This one is a little different, and is also known as a Prime spiral. Instead of using polar coordinates, we're going to use a grid and plot points on it. Starting at the center, we're going to move right, then up, then left, then down, and repeat. If the number we're on is a prime number, we're going to plot a point at that position.

Fig. - Ulam spiral implementation
let n = 100; // Number of points let direction = 0; let index = 0; let shiftCount = 1; let shiftTotal = 1; let x = 0; let y = 0; const points: Point[] = []; while (n > 0) { if (isPrimeNumber(index)) { n--; points.push([x, y]); } // shiftCount is used to determine how many steps // to take in a direction, once it reaches 0, we change // direction. if (shiftCount === 0) { direction = (direction + 1) % 4; shiftCount = direction == 0 || direction == 2 ? shiftTotal++ : shiftTotal; } shiftCount--; index++; if (direction === 0) x++; // Right if (direction === 1) y--; // Up if (direction === 2) x--; // Left if (direction === 3) y++; // Down } return points;

What's interesting about this spiral is that there are clear patterns that emerge, where the prime numbers are more likely to be found on diagonals, columns and rows and some that have exactly none. This has a perfectly rational explanation due to these lines landing on multiples. 3Blue1Brown has a great video on this.

Rendering spirals and radial patterns with particles in WebGL
A WebGL experiment to render and animate spirals and radial patterns with particles, involving some trigonometry and graphing equations.
Written May 22, 2018 · Updated March 17, 2024

Trigonometry recap

Back to the trigonometry days of high school, the sine function is the foundation of plotting from polar coordinates to the Cartesian coordinates, which we're going to be doing a little bit of here.

It's simply the ratios of the sides of a right-angled triangle. The sine function being the ratio of the opposite side to the hypotenuse, and the cosine function is the ratio of the adjacent side to the hypotenuse (adjacent to the 90° angle).

As an engineer, these are functions that accept a numerical value in radians and return a value between -1 and 1. The difference between the two functions is that the sine function starts at 0 and the cosine function starts at 1.

When dealing with angles in school I only ever remember using degrees, and never heard of the word "theta" or "radians", but in the world of programming and mathematics these are what we're going to want to know and use.

It's pretty simple, radians are the unit of PI (π), π radians is equivalent to 180° (degrees), so 2π radians is equivalent to 360°. Theta is just the named variable for a value that represents an angle.

Fig. - Math.cos and Math.sin
Math.cos(theta) // X coordinate Math.sin(theta) // Y coordinate // Example Math.cos(0) // 1 Math.sin(0) // 0 Math.cos(Math.PI) // -1 Math.sin(Math.PI) // 0

There's 2 ways I find useful to visualise cosine and sine. The first is to see their wave form overlaid on each other, where the X axis is the increasing theta value (radians) and the Y axis is the the corresponding Math.sin(x) or Math.cos(x) value

Fig. - Sine and Cosine
  • sin θ: 0.00
  • cos θ: 1.00
  • θ: 0.00

The second is to see them together as a point on a circle, where theta is the angle around the circle and the value of Math.sin(x) or Math.cos(x) is the X or Y coordinate of the point on the circle. Note if the radius of the circle is not 1, then we just multiply the results of the sine and cosine functions by the radius.

Fig. - Sine and Cosine
0
  • sin θ: 0.00
  • cos θ: +1.00
  • θ: 0.00

The Archimedes Spiral

We now know how to get an x and y coordinate for a circle with a radius between -1 and 1. Let's create an Archimedes spiral (to begin with). This is a spiral that increases linearly with the angle.

Fig. - Shows code for the equation r = x and t = x * 0.01.
const r = x; const t = x * 0.01; // This will be the same for all code snippets going // forward, so it will be omitted going forward. const x = r * Math.cos(t); const y = r * Math.sin(t);

Well that was easy, we have an Archimedes spiral. However, we have 5000 particles here so lets space these out a bit more. Let's try multiplying the index by 2 so it skips 1 points along the spiral before placing the next point.

Fig. - Shows code for the equation r = x and t = x * 0.01 * 2.
const r = x; const t = x * 0.01 * 2;

That's kind of worked on the outside but the center of the spiral is still far too packed. It doesn't look like we're getting equal spacing between the particles so there seems to be a scaling problem here. Notice how by increasing the rate of the theta value, we've also gained more arms to the spiral.

Fig. - Shows chart for the equation r = x and t = x * 0.01.
  • r = x
  • t = x * 0.01

!!! Fig 404 !!! shows our linear equation y = x. Where the x axis is the index of the point and the y axis is the corresponding radius/theta values. What it shows is that while the index increases, the radius/theta increases at the same rate, and this is exactly what we don't want.

Fig. - Shows chart for the equation r = x ** 2 and t = x * 0.01.
  • r = x ** 2
  • t = x * 0.01

Using the many mathematical functions available to us, we can create equations that cause values to scale at different rates, and use those to inform our rate of change for both our variables. For example, using y = for the radius (!!! Fig 404 !!!), starts off slow and gradually increases over time. This is called a logarithmic spiral.

This isn't what we need though, in fact it's the complete opposite!

Fig. - Shows chart for the equation r = x >> 4 and t = Math.floor(x / 16).
  • r = x >> 4
  • t = Math.floor(x / 16)

Graphing equations can even be useful to help understand the bitwise operators. Like shifting a number to the right by 4 bits is the same as dividing it by 16 and flooring the result, which creates a step scale (!!! Fig 404 !!!). This creates some more definitive points that a whole bunch of our coloured particles can be grouped around.

Fig. - Shows chart for the equation r = x * (x & 4) and t = r * 0.01.
  • r = x * (x & 4)
  • t = r * 0.01

Using the & operator to get the remainder of a division by 4, which combined with multiplying by the index creates an oscillation between 0 and linear growth (!!! Fig 404 !!!), and causes our points on the spiral to form groups of 4. However, we're not fixing the issue of the center of the spiral being too dense. We're after the rate of change to be faster at the beginning and then slow down as the index increases.

Fig. - Shows chart for the equation r = Math.sqrt(x) and t = r.
  • r = Math.sqrt(x)
  • t = r

Using the square root of the index (!!! Fig 404 !!!) might not look any different at first, but at the center of spiral the spacing between the particles is less dense, but the rest are still too close together.

Fig. - Shows code for the equation r = Math.sqrt(x) and t = r * Math.PI.
const r = Math.sqrt(x); const t = r * Math.PI;

Remember earlier we scaled our value by multiplying it by 2 and the effect it had on the spacing of particles? We can do the same again and scale up our theta value. We'll also scale it up by π so that the dots all align (!!! Fig 404 !!!).

Fig. - Shows code for the equation r = Math.sqrt(x * (x & 4)) and t = r.
const r = Math.sqrt(x * (x & 4)); const t = r;

We can also apply this same square root technique to the bitwise operator equation (!!! Fig 404 !!!) to get the same oscillation effect, but with the added benefit of the center of the spiral being less dense (!!! Fig 404 !!!).

Fig. - Shows code for the equation r = x and t = x * Math.PI.
const r = x; const t = x * Math.PI;

Finally for this section, just one more pattern I came across which I found interesting is to increase the radius linearly and the theta value by π (!!! Fig 404 !!!).

Vogel spiral

Another spiral that gets a lot of attention is one that described more recently in 1979 by a mathematician called Helmut Vogel, and thus named the Vogel Spiral.

He explains that this particular spiral is the maths that drives the structure of sunflower seeds, and the number of spirals in each direction are always consecutive Fibonacci numbers.

Fig. - Shows chart and code for the equation r = Math.sqrt(x) and t = x * 2.39998131.
  • r = Math.sqrt(x)
  • t = x * 2.39998131
const r = Math.sqrt(x); const t = x * 2.39998131;

Notice we're still using the square root of the index for the radius (!!! Fig 404 !!!), but we're multiplying the theta value by a number that appears to be a random number, but this is actually produces something called the Golden Angle , which is linked to the ratio of consecutive Fibonacci numbers.

Fig. - Golden angle calculation
Math.PI * (3 - Math.sqrt(5));

We can play around with this multiplier to get slightly different results (!!! Fig 404 !!!).

Fig. - Shows code for the equation r = Math.sqrt(x) and t = x * 1.5.
const r = Math.sqrt(x); const t = x * 1.5;

Ulam spiral

This one is a little different, and is also known as a Prime spiral. Instead of using polar coordinates, we're going to use a grid and plot points on it. Starting at the center, we're going to move right, then up, then left, then down, and repeat. If the number we're on is a prime number, we're going to plot a point at that position.

Fig. - Ulam spiral implementation
let n = 100; // Number of points let direction = 0; let index = 0; let shiftCount = 1; let shiftTotal = 1; let x = 0; let y = 0; const points: Point[] = []; while (n > 0) { if (isPrimeNumber(index)) { n--; points.push([x, y]); } // shiftCount is used to determine how many steps // to take in a direction, once it reaches 0, we change // direction. if (shiftCount === 0) { direction = (direction + 1) % 4; shiftCount = direction == 0 || direction == 2 ? shiftTotal++ : shiftTotal; } shiftCount--; index++; if (direction === 0) x++; // Right if (direction === 1) y--; // Up if (direction === 2) x--; // Left if (direction === 3) y++; // Down } return points;

What's interesting about this spiral is that there are clear patterns that emerge, where the prime numbers are more likely to be found on diagonals, columns and rows and some that have exactly none. This has a perfectly rational explanation due to these lines landing on multiples. 3Blue1Brown has a great video on this.