An intro to Flow Fields in Scala
This article is an interpretation in Scala of the wonderful Flow Fields article by Tyler Hobbs. Getting started in generative art can be difficult and intimidating, and I'm extremely grateful for artists like Tyler that open up their process for others to either learn it for themselves or to just help admirers better understand the process. Lately I've been attempting to go from the "admirer" to the "learner", so I was drawn to this article since it not only gives a clear introduction to Flow Fields, but also uses pseudocode to illustrate the concepts. Since I primarily code in Scala, my goal with this post was to really understand the concepts by translating his pseudocode into Scala code utilizing the Java Processing library. I'll also add in some extra resources to help fill in some gaps if you're new to Flow Fields, like I am. When you see quotes, unless noted otherwise, they're quotes from his article.
If you're curious to try running some of this code, I have each stage capture in this repo with instructions on how to run them. If having a fresh palette to start from is more your thing, check out my Giter8 template here that can be used to quickly get you started with Scala and processing. Processing 4 isn't published to Maven Central, so this will basically do some necessary setup for you to use Processing via Scala.
A Grid of Angles
Flow fields are based around a grid. Roughly speaking, this grid covers the whole image/canvas. At each point in the grid, you store an angle. Your grid should be stored as a 2D array of floating point numbers. Each item in the array holds an angle and represents a point in the grid.
Just like the article, we'll take this step by step taking the pseudocode and translating it to Scala. While the pictures will for sure help illustrate the steps, if you're more of a visual person and would rather watch a video to fully introduce the concepts before we dive in, I can highly recommend this video by Chris Courses. It does a fantastic job of going through many of the concepts covered in this article.
Now, to start this out, we'll do just like the article says and we'll initialize
a grid. The grid will be 1000 x 1000, but in reality we'll make some extra
margin around the image so you don't see everything abruptly end at the edge of
our screen. The idea here is to just visualize the grid that we've created as
well as a default angle that we'll give to every section in the grid (Pi * 0.25
). We'll draw an arrow to visualize this angle.
import processing.core
import processing.core.*
import processing.core.PApplet
import scala.math.Pi
class One extends PApplet:
// The actual size of our canvas
val Width = 1000
val Height = 1000
// The actual size we'll work with for our flows with extra margin
val leftX = (Width * -0.5).toInt
val rightX = (Width * 1.5).toInt
val topY = (Height * -0.5).toInt
val bottomY = (Height * 1.5).toInt
// The resolution, which will impact how many cols and rows are on the canvas
val resolution = (Width * 0.01).toInt
val numCols = (rightX - leftX) / resolution
val numRows = (bottomY - topY) / resolution
val grid = Array.ofDim[Double](numCols, numRows)
override def settings(): Unit =
size(Width, Height)
override def draw(): Unit =
background(255)
// Drawing out all the arrows in the grid
for col <- 0 until numCols do
for row <- 0 until numRows do
val x = (col * resolution)
val y = (row * resolution)
val angle = Pi * 0.25
drawArrow(x, y, angle, resolution - 2)
/** Given x and y coordinates we "GOTO" that part of the grid and then draw an
* arrow showing the given angle.
*/
def drawArrow(x: Double, y: Double, angle: Double, len: Double): Unit =
pushMatrix()
translate(x.toFloat, y.toFloat)
rotate(angle.toFloat)
val arrowSize = 2
val lineLength = len.toFloat - arrowSize
line(0, 0, lineLength, 0)
triangle(
lineLength,
0,
lineLength - arrowSize,
(arrowSize / 2).toFloat,
lineLength - arrowSize,
(-arrowSize / 2).toFloat
)
popMatrix()
end One
Just to fill in a possible gap, a popular thing you'll see is that often when
drawing a shape on the grid you won't actually store every point at each
location on the grid. Instead, you'll store the points to draw the shape in a
single location on the grid, and then move the grid around continually drawing
the same shape. This is what you're seeing with the pushMatrix()
and
popMatrix()
in the drawArrow
method. You can read more about this here in
this processing tutorial about 2D
transformations.
When ran, the image produced will look like this:
Generated from
One.scala
Now, all these arrow are just pointing in the same direction, which isn't what
we want. So we'll introduce a slight turn as we iterate throw all the rows. The
for loop we have that calls drawArrow
can be changed slightly to do this:
for col <- 0 until numCols do
for row <- 0 until numRows do
val x = (col * resolution)
val y = (row * resolution)
- val angle = Pi * 0.25
+ val angle = ((row.toDouble / numRows.toDouble) * Pi)
drawArrow(x, y, angle, resolution - 2)
Ran with this change, you'll now see the arrows curving:
Generated from
Two.scala
Drawing Curves through the Field
The next step of this is to use this curve to guide us in drawing a line following the flow of the arrows. To make this a bit easier for the moment we'll simply plot a dot in red to easily show where the line would be drawn. In order to make this work we change the code around a bit to no longer just draw out the angle when we iterate over it, but instead we store the angle in the grid because we need to iterate over the grid again to draw the dots. The full code for this looks like this:
import processing.core
import processing.core.*
import processing.core.PApplet
import scala.math.Pi
import scala.math.cos
import scala.math.sin
class Three extends PApplet:
// The actual size of our canvas
val Width = 1000
val Height = 1000
// The actual size we'll work with for our flows with extra margin
val leftX = (Width * -0.5).toInt
val rightX = (Width * 1.5).toInt
val topY = (Height * -0.5).toInt
val bottomY = (Height * 1.5).toInt
// The resolution, which will impact how many cols and rows are on the canvas
val resolution = (Width * 0.01).toInt
val numCols = (rightX - leftX) / resolution
val numRows = (bottomY - topY) / resolution
val grid = Array.ofDim[Double](numCols, numRows)
override def settings(): Unit =
size(Width, Height)
override def draw(): Unit =
background(255)
for col <- 0 until numCols do
for row <- 0 until numRows do
grid(col)(row) = ((row.toDouble / numRows.toDouble) * Pi)
for col <- 0 until numCols do
for row <- 0 until numRows do
drawArrow(
col * resolution,
row * resolution,
grid(col)(row),
resolution - 2
)
var x: Double = 500
var y: Double = 0
val stepLength = 10
val steps = 300
for step <- 0 until steps do
drawPoint(x, y)
val xOffset: Double = x - leftX
val yOffset: Double = y - topY
val columnIndex: Int = (xOffset / resolution).toInt
val rowIndex: Int = (yOffset / resolution).toInt
if (columnIndex > 0 && columnIndex < grid.length)
&& (rowIndex > 0 && rowIndex < grid(columnIndex).length)
then
val gridAngle: Double = grid(columnIndex)(rowIndex)
val xStep: Double = stepLength * cos(gridAngle)
val yStep: Double = stepLength * sin(gridAngle)
x += xStep
y += yStep
end draw
def drawPoint(x: Double, y: Double): Unit =
val baseWeight = g.strokeWeight
val baseStroke = g.strokeColor
stroke(255, 0, 0)
strokeWeight(10)
point(x.toFloat, y.toFloat)
strokeWeight(baseWeight)
stroke(baseStroke)
/** Given x and y coordinates we "GOTO" that part of the grid and then draw an
* arrow showing the given angle.
*/
def drawArrow(x: Double, y: Double, angle: Double, len: Double): Unit =
pushMatrix()
translate(x.toFloat, y.toFloat)
rotate(angle.toFloat)
val arrowSize = 2
val lineLength = len.toFloat - arrowSize
line(0, 0, lineLength, 0)
triangle(
lineLength,
0,
lineLength - arrowSize,
(arrowSize / 2).toFloat,
lineLength - arrowSize,
(-arrowSize / 2).toFloat
)
popMatrix()
end Three
When the code for this is ran it will produce an image that looks like this:
Generated from
Three.scala
There are a few different important things that you can change here to make quite a bit of difference in how your line is drawn. From the article:
We need to pick values for a few key parameters of how we draw the curves: the step_length, num_steps, and the starting position (x, y). The simplest is the step_length. Typically, this should be small enough that you don't see any sharp points on the curve. For me, that's usually around 0.1% to 0.5% of the image width. I go larger for quicker render speeds, and smaller if there are tight turns that need to be clean.
So in our example step_length
(aka stepLength
) is set to 10 resulting in the
dots being pretty close together. If we were to change this to say, 30, you can
tell the difference right away.
Notice how much farther apart the dots are now, which will end up resulting in a choppier line when traced.
The num_steps
(steps
) controls the amount of steps you'll take. So for
example if you have a short step length and a short amount of steps it'll result
in a short choppy line like this:
Finally, the starting positions x
and y
also make a difference since...
that's where your line is starting from. In our example we're starting at 500
for the x axis and 0 for the y which makes the line start nice and center at the
top of the grid. However, we could shift this down to be 300 for the y axis and
you'll notice the original image then changes to this:
With these three values alone you start to see how you can control what each line looks like. Whether it's short, long, choppy, smooth, and where it's positioned can all make a pretty large difference in the end result. The original article gives some great illustrations of these differences at a larger scale.
Distorting the Vectors
One big design decision is how you want to distort the vectors in your field. The method you use for this will determine what shapes your curves take. It will determine whether there are loops, abrupt turns, and overlapping lines.
About 90% of the time, people use Perlin noise to initialize the vectors. This is handy and easy, because it gives you smooth, continuous values across the 2D plane.
Perlin noise is exactly what we're going to use here. It's so popular in fact
that processing has built-in support for it via noise
. The change we need to
do here to actually implement this is shockingly small.
for col <- 0 until numCols do
for row <- 0 until numRows do
+ val scaledX = col * 0.005
+ val scaledY = row * 0.005
+ val angle = noise(scaledX.toFloat, scaledY.toFloat) * Pi * 2
- grid(col)(row) = ((row.toDouble / numRows.toDouble) * Pi)
+ grid(col)(row) = angle
When running with this change, you'll generate an image similar to this:
Generated from
Four.scala
However, keep in mind now that even though your starting points may always be the same, the angles will not be due to the "controlled randomness" of the perlin noise.
Putting it all together
At this point we actually have most of what we need to really start generating
some things. Now that we understand the grid behind what we're doing the first
thing we can do is get rid of generating it. Just remember that it's always
there doing the same thing, but we don't need to generate out the arrows. We
can do this by removing the drawArrow
method and also the for loop that calls
it as we iterate through the columns and rows.
No more arrows!
Generated from
Five.scala
Also currently we have a hard coded start x
and y
point which we don't
really want. So instead what we can do is refactor this out to just define a
steps
, the stepLength
and also a new lineCount
for the amount of lines
we'd like to draw. Then we can randomly create the starting x
and y
for
every line. This sounds great, but it will introduce a new problem. Firstly, if
we run it, it will look like this:
Generated from
Six.scala
You'll notice that instead of just a single image, it will keep regenerating. We
first notice this now because we are introducing randomness into the generation
that changes on every "tick". We can prevent this by using noLoop
. Also, all
of the dots just look chaotic. It's hard to see any pattern here, so we'll
instead switch to actually drawing a line.
At this point our full code looks like this:
import processing.core
import processing.core.*
import processing.core.PApplet
import scala.math.Pi
import scala.math.cos
import scala.math.sin
import scala.util.Random
class Seven extends PApplet:
// The actual size of our canvas
val Width = 1000
val Height = 1000
// The actual size we'll work with for our flows with extra margin
val leftX = (Width * -0.5).toInt
val rightX = (Width * 1.5).toInt
val topY = (Height * -0.5).toInt
val bottomY = (Height * 1.5).toInt
// The resolution, which will impact how many cols and rows are on the canvas
val resolution = (Width * 0.01).toInt
val numCols = (rightX - leftX) / resolution
val numRows = (bottomY - topY) / resolution
val grid = Array.ofDim[Double](numCols, numRows)
override def settings(): Unit =
size(Width, Height)
override def draw(): Unit =
background(255)
for col <- 0 until numCols do
for row <- 0 until numRows do
val scaledX = col * 0.005
val scaledY = row * 0.005
// We switch this here to return the angle with perlin noise
val angle = noise(scaledX.toFloat, scaledY.toFloat) * Pi * 2
grid(col)(row) = angle
val stepLength = 100
val steps = 1000
val lineCount = 2000
(0 until lineCount).foreach: count =>
drawCurve(steps, count, stepLength)
noLoop()
end draw
def drawCurve(steps: Int, count: Int, stepLength: Int) =
var x: Double = Random().between(0, rightX)
var y: Double = Random().between(0, bottomY)
(0 until steps).foreach: _ =>
val xOffset: Double = x - leftX
val yOffset: Double = y - topY
val columnIndex: Int = (xOffset / resolution).toInt
val rowIndex: Int = (yOffset / resolution).toInt
if (columnIndex > 0 && columnIndex < grid.length)
&& (rowIndex > 0 && rowIndex < grid(columnIndex).length)
then
val gridAngle: Double = grid(columnIndex)(rowIndex)
val xStep: Double = stepLength * cos(gridAngle)
val yStep: Double = stepLength * sin(gridAngle)
val nextX = x + xStep
val nextY = y + yStep
line(x.toFloat, y.toFloat, nextX.toFloat, nextY.toFloat)
x = nextX
y = nextY
end drawCurve
end Seven
And when ran, you'll start to see what we're actually going for.
Generated from
Seven.scala
At this point you can really start playing around with the values of
stepLength
, steps
, and lineCount
, all which will pretty radically change
the way the generated image looks. It's also at this point that you can start
playing around with the way you generate your perlin noise. Chaning the * Pi * 2
will also start to radically change your image. Maybe you also want to start
changing the stroke width in your drawCurve
method, add some colors, etc.
Here's a couple examples of doing different things.
Conclusion
The possibilities with generative art are endless, and this is just a drop in the bucket of techniques and ideas. My hope is that as I continue to explore different facets of this field, you'll see them here. Again, I want to give more focus to the art and blog by Tyler Hobbs, which is truly inspirational. Hopefully these examples will give you what you need to dive into flow fields and Scala. Until next time.