Generative Watercolor in Processing
All Processing code for this article, along with images and animated GIFs, can be found on Github
I recently came across Tyler Hobbs’ fantastic post on simulating watercolor paints and decided to implement it in Processing. This article explains in detail how you can implement Tyler’s technique from scratch, and here is the kind of effect you’ll be able to create by the end.
Generative Watercolor: An Overview
The steps required to generate a single brush in the watercolor style shown above are: (i) start with a regular n-sided polygon, (ii) deform it to form a base polygon, (iii) create variations of the base polygon by deforming it further, (iv) stack all the variations using a low opacity, and finally (v) interleave polygon layers from different brushes so that overlapping areas blend correctly. As a bonus, I’ve also added a section on how to add some canvas- or paper-like texture to the final result.
Step 1: Creating a Regular Polygon
We begin by creating a regular polygon with a specified number of sides. This
can be done quite simply with the function below, which generates an nsides
-sided
regular polygon with radius r
with the center at (x, y)
.
ArrayList<PVector> rpoly(float x, float y, float r, int nsides) {
ArrayList<PVector> points = new ArrayList<PVector>();
float sx, sy;
float angle = TWO_PI / nsides;
/* Iterate over edges in a pairwise fashion. */
for (float a = 0; a < TWO_PI; a += angle) {
sx = x + cos(a) * r;
sy = y + sin(a) * r;
points.add(new PVector(sx, sy));
}
return points;
}
Note the use of an ArrayList<PVector>
to hold our list of points. A PVector
is an easy way to represent a pair of X/Y coordinates in Processing, and since
we want a polygon with multiple points, we use an array of such coordinates (via the
ArrayList
class). This is a handy way to represent polygons in Processing that
are comprised of many points, and we can draw these points easily with the
following code.
void setup() {
ArrayList<PVector> poly;
size(500, 500);
background(255);
noStroke();
fill(255, 0, 0);
poly = rpoly(width/2, height/2, width/3, 10);
beginShape();
for (int i = 0; i < poly.size(); i++)
vertex(poly.get(i).x, poly.get(i).y);
endShape(CLOSE);
}
In the above, we generate a 10-sided regular polygon centered at (width/2, height/2)
with a radius of width/3
. The loop iterates over the points sequentially, adding
them as vertices of a shape, and we finally close the shape with a call to
endShape()
. What we end up with is the following.
It should be noted that there’s no inherent reason to begin with a regular polygon for our watercolor brushes other than convenience and simplicity. You can specify any set of points making up any arbitrary shape (e.g., for implementing different brush shapes).
Step 2: Deforming a Polygon
Now that we have a regular polygon, the next step is to deform it. The function below shows my version of this.
ArrayList<PVector> deform(ArrayList<PVector> points, int depth,
float variance, float vdiv) {
float sx1, sy1, sx2 = 0, sy2 = 0;
ArrayList<PVector> new_points = new ArrayList<PVector>();
if (points.size() == 0)
return new_points;
/* Iterate over existing edges in a pairwise fashion. */
for (int i = 0; i < points.size(); i++) {
sx1 = points.get(i).x;
sy1 = points.get(i).y;
sx2 = points.get((i + 1) % points.size()).x;
sy2 = points.get((i + 1) % points.size()).y;
new_points.add(new PVector(sx1, sy1));
subdivide(new_points, sx1, sy1, sx2, sy2,
depth, variance, vdiv);
}
return new_points;
}
In the above code, the first thing to note is that the function takes as input,
a set of polygon vertices (via the points
parameter) and returns a new set of
deformed vertices (via new_points
).
In the main loop, we iterate over the set of points in a pairwise fashion to
get the X/Y coordinates for both points of individual edges. These get stored
in (sx1, sy1)
and (sx2, sy2)
. You’ll note the use of the indices i
and
i + 1
to get the ith and (i+1)th vertex. Since for the
last point i + 1
would exceed the length of the array, we wrap back to the
0th index using the module operator (%
).
Finally, we add the first point to new_points
, followed by a call to a
subdivide()
function that adds the randomly-shifted midpoint of the two ends
of the edge to new_points
. We’ll look into this function in more detail in a
moment, but it receives a depth
parameter that specifies the recursion depth
to which we want to subdivide the edge, a variance
that is used to specify
the magnitude of the shift, and a vdiv
parameter that is used to reduce the
variance with each successive recursive call. We’ll look at this in more detail
in a bit.
You’ll note in our loop that we don’t add the second point to new_points
.
This is simply because, when iterating pairwise, the second point will be added
when it becomes the first point of the next iteration. This just leaves one
corner case of the last edge, which has no next iteration. However, here again,
since we start with a closed polygon, the second point of the last edge is the
same as the first point of the first edge, which we already added to new_points
.
Subdividing an Edge
/*
* Recursively subdivide a line from (x1, y1) to (x2, y2) to a
* given depth using a specified variance.
*/
void subdivide(ArrayList<PVector> new_points,
float x1, float y1, float x2, float y2,
int depth, float variance, float vdiv) {
float midx, midy;
float nx, ny;
if (depth >= 0) {
/* Find the midpoint of the two points comprising the edge */
midx = (x1 + x2) / 2;
midy = (y1 + y2) / 2;
/* Move the midpoint by a Gaussian variance */
nx = midx + randomGaussian() * variance;
ny = midy + randomGaussian() * variance;
/* Add two new edges which are recursively subdivided */
subdivide(new_points, x1, y1, nx, ny,
depth - 1, variance/vdiv, vdiv);
new_points.add(new PVector(nx, ny));
subdivide(new_points, nx, ny, x2, y2,
depth - 1, variance/vdiv, vdiv);
}
}
Creating a Base Polygon
We can now generate a base polygon to work with.
ArrayList<PVector> create_base_poly(float x, float y,
float r, int nsides) {
ArrayList<PVector> bp;
bp = rpoly(x, y, r, nsides);
bp = deform(bp, 5, r/10, 2);
return bp;
}
We create a regular polygon, deform it, and return it. I’ve found that having
five layers with a variance of a 10th of the radius, with a halving
of the variance between recursive calls (i.e., vdiv
is 2) works nicely.
Here’s the result.
Generating Variations of the Base Polygon
Generating variations is fairly easy since our deform function is agnostic to the shape being passed. Instead of a regular polygon, we just give it our base polygon from the previous step.
void setup() {
ArrayList<PVector> base, variation;
base = create_base_poly(width/2, height/2, width/3, 10);
variation = deform(base_poly, 5, random(r/10, r/4), 4);
}
Step 4: Stacking Multiple Polygons
So far we’ve been generating individual polygons. It’s now time to stack them up together to form our watercolor brush.
Above, we represent a single vertex using a PVector
, and a single polygon using
ArrayList<PVector>
, that is a list of PVector
’s. Well, we can make use of
the same built-in types to create a stack of polygons: we represent it as a
list of polygons, which is an ArrayList<ArrayList<PVector>>
!
Here’s a simple function for creating a stack of polygons centered at (x, y)
and radius r
starting with a base polygon with nsides
sides.
ArrayList<ArrayList<PVector>>
polystack(float x, float y, float r, int nsides) {
ArrayList<ArrayList<PVector>> stack;
ArrayList<PVector> base_poly, poly;
stack = new ArrayList<ArrayList<PVector>>();
/* Generate a base polygon with depth 5 and variance 15 */
base_poly = rpoly(x, y, r, nsides);
base_poly = deform(base_poly, 5, r/10, 2);
/* Generate a variation of the base polygon with a random variance */
for (int k = 0; k < 100; k++) {
poly = deform(base_poly, 5, random(r/15, r/5), 4);
stack.add(poly);
}
return stack;
}
We first create a base polygon followed by one hundred variations. Each time,
we add the variation to the stack before finally returning it at the end.
There’s a lot of room for exploration in the variance
and vdiv
parameters
passed to the deform()
functions, but for convenience, I’ve picked and
hard-coded ones that look visually appealing to me.
We can render this stack of polygons on screen using the following function.
void draw_stack(ArrayList<ArrayList<PVector>> stack) {
for (int i = 0; i < stack.size(); i++) {
ArrayList<PVector> poly = stack.get(i);
draw_poly(poly);
}
}
Here is the result!
In Tyler’s original article, he also has a clever approach for creating areas of high and low variation, but I haven’t found an elegant way to do this. I’ll update this post at some point once I find one.
Step 5: Interleaving Brush Layers
To interleave layers, we take many stacked polygons and draw them a few layers at a time. Note that this means it’s not easy to draw watercolor brushes incrementally. That is, you must know beforehand, all the brushstrokes that are going to go on your canvas (or sub-region of the canvas) and draw them at once.
And I know what you’re thinking.
Yes.
That’s right.
We’re using an ArrayList<ArrayList<Arraylist<PVector>>>
.
I suppose you could use classes at this point to ease the notation a bit, but I like the simplicity of sticking with functions that use built-in types. It’s also very easy to remember and, if you forget, to easily recreate in your head (vertices → polygon → stack of polygons → list of stack of polygons). If you prefer classes, it might be a useful learning exercise to try and abstract this logic into classes.
ArrayList<ArrayList<ArrayList<PVector>>> stacklist;
int[] colors;
int color_index = 0;
void setup() {
...
stacklist = new ArrayList<ArrayList<ArrayList<PVector>>>();
colors = new int[1000]; /* Up to 1,000 polygon stacks */
...
stack = polystack(width/2, height/2, width/3, 10);
stacklist.add(stack);
colors[color_index++] = color(255, 0, 0, 4);
...
stacklist_draw(stacklist, colors, 5);
}
We first create a stacklist
variable and initialize it. Then we can create
one or more polygon stacks using the polystack()
function we wrote earlier,
and add them to our stack list. Finally we draw them using draw_stacklist()
which takes a parameter of how many layers comprise each interleaving. So a value
of 5 means that the first 5 layers will be drawn for all stacks, followed by layers
5 – 10 and so on till no more layers remain in any polygon stack. The code for
this is shown below.
Note that we now need to provide some additional color information for each stack
of polygons so that the drawing function can set the fill color correctly. We
do this via a separate colors
array. Everytime we add a stack to the stack list,
we also add a color associated with it to the colors array, which is passed
along with the stack list to the drawing function (see below). (Note that color
in Processing is simply an alias for int
, and so it works if we just use an
integer array. It’s also why it’s difficult to use an ArrayList
with colors as
they are not objects.)
void stacklist_draw(
ArrayList<ArrayList<ArrayList<PVector>>> stacklist,
int[] colors,
int interleave) {
int layer = 0;
boolean all_empty;
while (true) {
all_empty = true;
println("drawing layers " + layer + "--" + (layer+interleave));
for (int i = 0; i < stacklist.size(); i++) {
fill(colors[i]);
ArrayList<ArrayList<PVector>> stack = stacklist.get(i);
for (int j = layer; j < layer + interleave; j++) {
if (j < stack.size()) {
all_empty = false;
draw_poly(stack.get(j));
}
}
}
layer += interleave;
if (all_empty)
break;
}
}
Step 6: Simulating a Canvas Texture
Yes yes, I know. Watercolors are not painted on canvas. However, I think the effect it produces is very cool regardless! The basic idea is to finish drawing all our watercolor brush strokes and, at the very end, overlaying a fine canvas-like texture over the whole image.
Here’s the texture I came up with. (It’s been drawn on a black background for visibility as the lines are colored with a low-opacity white.)
The way this was done was to draw two sets of lines at 45° and 135° degrees. With a low spacing between the lines, this gives a nice crosshatch.
Here’s the code for generating this.
void grid() {
float spacing = 5;
for (int i = -width; i < height + width; i+=spacing)
line(i, 0, i + height, height);
for (int i = height + width; i >= -width; i-=spacing)
line(i, 0, i - height, height);
}
To make things a bit more interesting, I wrote a custom line-drawing function that draws the exact same lines, but in short segments, while varying the opacity, thickness, and orientation of each one (the last one only to a small extent). Here’s the code for that.
void gridline(float x1, float y1, float x2, float y2) {
float tmp;
/* Swap coordinates if needed so that x1 <= x2 */
if (x1 > x2) { tmp = x1; x1 = x2; x2 = tmp; tmp = y1; y1 = y2; y2 = tmp; }
float dx = x2 - x1;
float dy = y2 - y1;
float step = 1;
if (x2 < x1)
step = -step;
float sx = x1;
float sy = y1;
for (float x = x1+step; x <= x2; x+=step) {
float y = y1 + step * dy * (x - x1) / dx;
strokeWeight(1 + map(noise(sx, sy), 0, 1, -0.5, 0.5));
line(sx, sy, x + map(noise(x, y), 0, 1, -1, 1), y + map(noise(x, y), 0, 1, -1, 1));
sx = x;
sy = y;
}
}
The above is a simple line algorithm that calculates the change in X and Y
directions for a given step
and draws a smaller line with slight variations
in the position and stroke thickness. We simply replace the calls to line()
in our grid()
function to gridline()
calls and vary the opacity randomly
like shown below.
void grid() {
float spacing = 5;
for (int i = -width; i < height + width; i+=spacing) {
stroke(255, random(20, 50));
gridline(i, 0, i + height, height);
}
for (int i = height + width; i >= -width; i-=spacing) {
stroke(255, random(20, 50));
gridline(i, 0, i - height, height);
}
}
And that’s it!