Creating a procedurally generated drawing of plants using Javascript and Paper.js
Paper.js
Paper Garden is a program written in about 300 lines of PaperScript using Paper.js. It generates stylized drawings of plants arranged in a wall garden. The plants are rendered as clumps of leaves, sometimes with hanging vines or long drooping flowers. All of this is drawn with rough little circles meant to simulate pen doodles from my sketchbook.
This example was inspired by the r/proceduralgeneration monthly challenge for March 2019, Procedural Garden.
The image above was drawn by the Paper Garden script. For this image, the script was configured to generate, compose, and draw 255 plants. The script allows you to configure the number and spacing of plants; the number, spacing, and sorting of leaves on each plant; and how rough, sloppy, and thick the strokes are. This allows the script to squeeze out a range of different looks.
You can configure, edit, and run the script in the editor below, or go on to read a breakdown of how it works.
Paper Garden is written in PaperScript, a Paper.js extension of Javascript that supports using mathematical operators on Paper.js objects. Other than that, PaperScript is basically the same as Javascript.
Paper Garden is written in a procedural style and comprises 18 functions. The diagram below organizes these functions into general responsibilities and maps out the key relationships between them.
The application doesn’t have much of an interface: you load it and it draws plants. makeScene()
is the main entry point for the application.
These functions build the structure of the drawing. createPlants()
decides where plants will go and what kind they should be. createPlant()
creates the leaves, flowers, and vines that make up an individual plant. createLeaf()
, createFlower()
, and createVine()
draw the plant parts.
These functions generate an array of nicely arranged points. clusterPoints()
creates some randomly placed points near 0,0
and then calls relaxPoints()
to space them out. relaxPoints()
repeatedly compares each pair of points and pushes them apart if they are too close.
Each of the sort*()
functions take an array of points and reorders them based on their positions. Shapes drawn early will be covered up by later shapes, so the order of the points in the array has a big impact on the look of the drawing.
For more on point placing see the chapter on strategies.
The create*()
functions all call drawCircle()
to create the individual circles in the drawing. The look of the circles created by drawCircle()
is configured through global parameters.
These functions are called from all over the program.
map()
map a value from one range to anotherrandomRange()
creates a random value in a given rangerandomPoint()
creates a random Point between -1,-1
and 1,1
pick()
chooses and returns a random item from an arrayshuffle()
randomly reorders the items in an arrayPaper Garden produces images with a somewhat hand-drawn look based on a doodle from my sketchbook, but new Path.Circle()
makes perfect circles. drawCiricle()
uses a few tricks and techniques to achieve a more hand-drawn appearance.
In my sketchbook doodle, I drew the circles front to back. When drawing with a pen, it is easy to not draw part of the circle and hard to erase marks. In Paper.js the opposite is true. New circles easily cover any marks already on the drawing, and drawing only part of the circle requires substantial extra work. Because of this, Paper Flowers draws from back to front.
This approach works fine for rasterized output on the screen but won’t work on vector outputs like pen plotters or laser engraving which will draw the entire path of each circle even if another circle covers it. See Plotting the Drawing
below.
The circles in my doodle are not very round. Paper Garden simulates this by displacing each control point of the Bezier curve in a random direction. The amount of displacement is controlled by the ROUGH
parameter.
The lines in the circles don’t always perfectly meet. To recreate this, drawCircle()
creates an unstroked white circle behind the stroked one. This circle is made larger to create gaps—GAPPY
—and randomly offset so the gaps are not even—SLOPPY
.
The strokes in the doodle have uneven widths and uneven color. In particular, the first little bit of each stroke is heavier than the rest. To simulate this, drawCircle()
duplicates the circle path and draws it again with a slightly thicker stroke. This thicker stroke is broken up using dashOffset
and dashArray
to create a randomized dash pattern.
The circles produced by drawCircle()
are rough, gappy, and uneven, but they are still much more “perfect” than the strokes on the doodle. This is largely an artistic judgment. I wanted to keep some of the richness and subtlety of the hand-drawn doodle but wanted the final image to look less sloppy and rushed.
Also, Paper Flower generates drawings that are much more complex than the doodle above with hundreds of plants and thousands of leaves. This added complexity already makes the overall drawing more rich and interesting. The stroke quality should support that interest without distracting from it.
Individual plants are generated by the createPlant()
function. This function places the leaves by following these steps:
-1,-1
and 1,1
The layout of the entire garden is generated by createPlants()
. This function follows the same steps used to generate the leaves with one addition: it removes some of the points after they are arranged.
-1,-1
and 1,1
Thats pretty much it: decide where plants should be, decide where the leaves should be, draw the leaves. To add a little more variation and interest, some of the leaves are swaped out for vines or flowers.
With different drawing parameters Paper Garden can generate a range of looks, but I like the slightly hand-drawn line-drawing style best, so I tried drawing it on a Silhouette Cameo pen plotter. The Paper Garden script can export vector files to draw with the plotter, but a little extra work needs to be done to get them to work correctly.
The version of the script above draws the circles with a white fill to cover up the lines from other circles. Since the pen plotter only draws lines, using fills to cover things up isn’t possible. I created a version of the script for plotting that procedurally removes the hidden lines.
This script shows how I removed the lines.
And here are some of the plots.