A Perlin-like flow with canvas, shaders and three.js (Part 1)
While searching for a suitable example for a post at Re-Versing, I remembered one implementation in CodePen that I really liked. It is called “Perlin Flow Field” by Darryl Huffman.
The pen looks like this:
See the Pen Perlin Flow Field by Darryl Huffman (@darrylhuffman) on CodePen.
Darryl utilized the 2D graphics canvas API, Three.js (the 3D graphics library), and GLSL shaders, to create this pen.
The mix of all those graphic features and tools, as well as the use of the noise function to create an flow effect, piqued my curiosity. I made some reverse engineering to find out how Darryl managed to get that effect.
The Code
The code by Garryl Kuffman can be divided into three sections:
- The “context” canvas and the Hair class
- The noise function, the shader, and the Three.js plane geometry
- The relationship between the texture (the “perlinCanvas”) and the “context” canvas
Let’s follow Garryl’s code for each of those sections in that order.
THE “context” CANVAS AND THE Hair CLASS
Garryl made use of two canvas components. They were closely connected; we will explain that better later on.
the line strokes that would be animated were on the “context” canvas. Let’s examine its implementation.
Garryl instantiated the two canvas's width and height based on the container's offset (in his case, the body element). The context canvas was made transparent. Here I show a canvas in grey. In addition, he declared an empty "hairs" array as well as the parameters of an object called "circle" with values corresponding to the context canvas. The context canvas was appended to the container.
const canvas = document.createElement('canvas'),
context = canvas.getContext('2d'),
perlinCanvas = document.createElement('canvas'),
perlinContext = perlinCanvas.getContext('2d'),
width = canvas.width = container.offsetWidth,
height = canvas.height = container.offsetHeight,
circle = {
x: width / 2,
y: height / 2,
r: width * .2
},
hairs = []
document.body.appendChild(canvas)
The "perlin" canvas was eventually instantiated with the same dimensions as the context canvas, but it was not appended to any HTML element.
let perlinImgData = undefined
perlinCanvas.width = width
perlinCanvas.height = height
It is on top of the context canvas that Darryl would randomly draw the strokes.
All "hair"'s were instances of the class Hair. The class constructor took several inputs. The global circle object was one of them. For each instance of Hair, the class constructor generated random values for "r" and "d" of a unit circle. These values where then used to calculate random positional (x,y) values enclosed within the global circle object by applying the parametric equation of the circle. Addtionally, the class assigned a random length to each stroke.
class Hair {
constructor(){
let r = 2 * Math.PI * Math.random(),
d = Math.sqrt(Math.random())
this.position = {
x: Math.floor(circle.x + Math.cos(r) * d * circle.r),
y: Math.floor(circle.y + Math.sin(r) * d * circle.r)
}
this.length = Math.floor(Math.random() * 10) + 10
hairs.push(this)
}
...
In Hair class, Darryl included a method to draw each of the strokes. Notice that there are two elements that will come as very important later: the perlinImgData and the canvas API moveTo and lineTo methods.
...
draw(){
let { position, length } = this,
{ x, y } = position,
i = (y * width + x) * 4,
d = perlinImgData.data,
noise = d[i],
angle = (noise / 255) * Math.PI
context.moveTo(x, y)
context.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length)
}
}
With a for-loop, Darryl instanted 6000 "hairs". The destination of each new instance is not clear by just looking at the loop. By exploring the code of the class Hair you will find that Darryl made it to use the now global hairs list to register each new instance at the time of the instance construction.
for(var i = 0; i < 6000; i++){
new Hair()
}
Tada!
The last bit of code I want to show you for now is the render of the canvas elements:
|
|
One nice element of the functionality was the way Darryl drew the strokes (hairs.map(hair => hair.draw())
). Something I also want to bring attention to is what happens with the perlinContext as well as the value assignment to the global perlinImgData in the Hair’s draw method. For the sake of this example we left the values of the perlinImgData variables at zero. However, close examination of Darryl’s code makes clear that the perlinImgData is feeding data to the draw function and therefore affecting the position of the hairs in the circle.
So… What did we learn from this code?
So far, one of the things that for me was very interesting from Darryl Huffman’s example was the simplicity of ideas. I won’t say the code is very simple, but some of the design concepts of the exercise, like randomly distributing hairs in a circle, were very nice. The use of the class to even append the instances in a global list were kind of smart.
Apart of that, there is still much to reveal about this code. What about the noise function? And what is the role of the “perlinContext” canvas? You might ask. Before we move to the next part, I can say that using two canvas elements is a common trick - using one canvas to extract data from an application (eg. a video) and to feed that data into another canvas to affect a visualization.
For now, happy coding!