Exploring 2D differential growth with JavaScript

Jason Webb
12 min readJan 29, 2019

In the natural world there is a seemingly endless array of shapes and forms produced by a myriad of mechanical and chemical processes acting upon materials in complex ways, often through what turn out to be simple rules.

One such example is that of differential growth, a process that produces winding, undulating, buckling forms that remind one of meandering rivers, rippled surface textures of fruits and seeds, foliose lichen, and the space-filling behaviors of worms, snakes, intestines, and perhaps even the brain!

Euphrates with Lake Qadisiyah, Iraq by NASA (left); Foliose lichen on a rock in Shawnee National Forrest, southern Illinois, USA by Daniel Schwen (right)
Image from page 51 of “Nursing in abdominal surgery and diseases of women” (1893) via Internet Archive Book Images (left); Outer surface of the human brain by Sanger Brown M.D.

Though we may not be able to know for certain the exact mechanisms of these real-world processes, we can digitally simulate their behaviors through careful examination of their effects and speculating on the fundamental rules at play.

In this article we’ll take a look at the process from both a technical and aesthetic perspective through an iterative series of visual experiments I built using JavaScript. Think of it as part lesson and part project documentation!

You can play with all of the experiments shown in this article here:

Understanding the algorithm

The differential growth process can be understood through a series of rules that are repeatedly applied to points in space (called nodes) connected into chains by lines (edges) to form paths.

In each iteration of the simulation we run the following rules on each node:

  1. Each node wants to be close to it’s connected neighbor nodes and will experience a mutual attraction force to them.
  2. Each node wants to maintain a minimum distance from all nearby nodes, connected or not, and will experience a mutual repulsion force from them.
  3. Each node wants to rest exactly halfway between it’s connected neighbor nodes on as straight of a line as possible and will experience an alignment force towards the midpoint.

Adaptive subdivision

As paths grow it will be natural for some nodes to drift further and further apart creating long, straight edges that don’t really reflect what happens in the real world. In nature these spaces are generally filled in with new material through processes like cell division, surface tension, and pressure. We can approximate this behavior by injecting new nodes in these areas, essentially recursively splitting up (subdividing) edges that become too long.

Growth

Finally we must consider the growth aspect of differential growth, without which a system using the rules above would quickly reach a stable (and boring!) equilibrium state. Just as new cells are born as an organism consumes nutrients over time, new nodes must be injected into the system at a rate faster than the countervailing forces from the rules above can compensate for them in order to produce an overall net increase in volume. The exact criteria that is used to inject these new nodes can vary to produce varying growth patterns.

Anders Hoff (inconvergent) has a great article on this topic that includes a number of illustrative animations demonstrating these rules in action:

Setting up the environment

My implementation uses p5.js for it’s canvas-based drawing functions along with ES6-flavored JavaScript transpiled to ES5 for current-gen browsers using Babel and Browserify through a Gulp-based build system.

As I built out of my sketches I ended up using the following helpful packages:

  • rbush — for a fast R-tree spatial index implementation to keep track of the positions of every node each frame.
  • rbush-knn — for k-nearest neighbors searching of the R-tree index, allowing us to find nearby nodes within a certain radius.
  • point-in-polygon — for constraining nodes inside of bounding paths. As soon as a node drifts outside of a defined polygonal boundary it is frozen in place.
  • svg-pathdata — for extracting coordinates out of SVG <path> elements. Used to import SVG files and produce Path objects.
  • svg-points — for generating the d attribute of SVG <path> elements in order to export properly-formatted SVG files from Paths.
  • file-saver — for initiating a download prompt when exporting SVG files

All of the code is available over on Github here:

Implementation

Using our understanding of the algorithm from earlier we can see that there are only a couple of fundamental modules that we need to build. Each of these modules is contained in the core folder:

  • Node — a single point in space whose only job is to move from it’s current position to a destination position that is provided externally.
  • Path — manages a collection of Nodes connected in a particular order. Responsible for applying the behavioral rules defined earlier to each of it’s nodes.
  • Since edges are really just lines drawn between connected Nodes, no special module is really needed for them. Instead, edges can be rendered as the Path is drawn.

I knew I wanted to create multiple independent sketches to explore specific concepts, so it seemed to me that it would be helpful to have one more layer of abstraction to keep the code modular and DRY. To do this I took a page from the Box2d library and created a World module that all sketches could make use of:

  • World — manages a collection of Paths and maintains a spatial index of every Node of every Path it is aware of. Also manages the state of various rendering effects and whether or not the entire simulation is paused.

You will also find a couple other classes that I created to make certain features cleaner and easier to work with:

  • Bounds — a polygon that can be used to contain the growth of one or more Paths. As soon as a node is about to leave the interior of this polygon it is “frozen”.
  • SVGLoader — a utility for extracting points from an embedded SVG document and generating appropriately-structured Paths that can be added to a World.

Finally there is a very important module (Defaults) that contains default values and descriptions of every parameter that is available for tweaking. Any of these values can be overridden at the sketch level through a local Settings module in that sketch’s folder, which gets passed in to the World and merged with the Defaults object.

Complete documentation (generated with JSDoc) of each of these modules/classes and their various functions is available here.

Keyboard commands

To make it easier to play around with the system without having to change code and recompile, I made the following keyboard shortcuts available in all of the experiments:

  • Space = pause or unpause the simulation
  • r = reset simulation with same parameters
  • s = download an SVG of current geometry
  • 1-9 = access different variations (if available)
  • t = toggle trace effect
  • n = toggle visibility of nodes
  • i = toggle inverting of colors
  • d = toggle "debug mode"
  • f = toggle shape fills
  • h = toggle drawing of periodic path history
  • b = toggle visibility of path bounds (if available)

Experiment 01 — a single line

To start things off I wanted to explore the simplest possible conditions for the differential growth process to start with — an open Path consisting of two Nodes connected by a single edge.

Initially the path expands horizontally as new nodes are injected through the growth and adaptive subdivision rules. Eventually the various forces become asymmetrical and begin to force some nodes out of alignment. As soon as even the slightest vertical wiggle occurs in one region it immediately becomes exaggerated and induces cascading and compounding asymmetry throughout the entire Path, producing the result seen above.

Play with this experiment in the browser!

For a bit of fun, I also added in the ability to invert the colors at any time using the i key — use it in any sketch!

Experiment 02 —a simple shape

The next logical step was explore the simplest possible closed Path — a triangle. After all, a triangle is just a path with three Nodes instead of two, with an additional edge connecting the last Node to the first!

Seeing this experiment run for the first time was magical! There is something about seeing simple closed shape morph into something so organic and complex that really sells the illusion of a life-like, natural process, as though it were a cell under a microscope.

Play with this experiment in the browser!

In this experiment I also built in a couple other shapes that can be accessed using the 1-3 number keys. I also thought it’d be nice to allow the shapes to be rendered with or without a solid fill — use the f key toggle that in any sketch!

Experiment 03 — multiple shapes

With single closed shapes working well on their own, what happens when we add multiple shapes to the simulation? Well, at first, not what I was expecting! Up until this point I had been using the Path module to keep track of all of it’s Nodes and make sure they avoid each other using an internal R-tree spatial index and a nearest neighbor search. But when there are multiple Paths on the canvas there was nothing preventing them from overlapping because they all had totally separate spatial indices that were not taking into the account the Nodes of other Paths.

To get multiple Paths to interact with each other correctly there needed to be a shared spatial index that keeps track of all the Nodes from every Path. This turned out to be pretty easy — just elevate the R-tree index to the World module and pass a reference to it into each Path so they can perform the same lookups and movement behavior rules.

Play with this experiment in the browser!

Experiment 04 — using SVG shapes

Having multiple shapes growing and interacting with each other is great, but one can only come up with so many interesting starting shapes programmatically. At a certain point it makes sense to enable any shape provided by the user to be used.

Luckily the perfect file format for this already exists — SVG! SVG files are vector graphic files that define all of it’s shapes, lines, colors, and effects using an XML-like syntax that is pretty easy for us to parse and extract data out of using JavaScript.

In these experiments I embed an SVG file into the HTML of the sketch’s page with an <object> tag, then use the svg-pathdata package to essentially convert all of the <path> elements it contains into Path objects that get added to the World. Check out the SVGLoader module to see how I did that.

If you’re running this locally and want to try using your own SVG files with this experiment, you should make sure that your file uses absolute coordinates and only straight lines. Some more tips can be found in the README for this sketch.

Play with this experiment in the browser!

Experiment 07 — bounds

Lastly I wanted to explore what would happen when the growth process is confined within shapes, so I set up a special Bounds module that can be referenced by Paths.

Play with this experiment in the browser!

My implementation is pretty crude — whenever a Node leaves the confines of the relevant Bounds object, it gets “frozen” in place and is no longer allowed to move. This works well enough to create the basic effect I was looking for, but it would be better if the Nodes were free to continue moving within the Bounds, even when the shape of the Bounds changes or moves. If you can think of a way to do this, I’d love to see a pull request on Github!

Line studies

With all of the core features built out, I set about building a series of experiments to explore what happens when differential growth is applied to various arrangements of lines. The results were really satisfying!

Shape studies

Finally I ran another series of experiments exploring what happens when differential growth is applied to various arrangements of shapes:

Playground

Last but not least I wanted to see if I could create a way for people other than developers to play around with this process through some sort of user interface. What I came up with was a simple drawing application that lets you draw whatever you want using lines, shapes, and even imported geometry, then initiate the differential growth process using a giant “play” button. You can even adjust all of the parameters of the growth algorithm using a handy (admittedly buggy) control panel!

Play with this experiment here!

I posted a short video of this app on Twitter and received a much stronger response than I was expecting! For me this was some great insight into what really “clicks” with people, and how I can package my work to reach larger audiences. I will definitely be creating more web apps like this in the future!

Applications

From the beginning my intent was to keep the output of these experiments fairly schematic, focusing more on producing colorless vector line drawings than purely on-screen visual effects. This is because my personal interest in these sorts of processes is in how they can leveraged in various digital fabrication workflows to produce interesting objects in the real world, thereby coming full circle with their natural counterparts.

Early on in this series I implemented an export feature that can be triggered with the s key at any time to produce SVG files containing all of the geometry on the screen. These files can be edited and post-processed using standard vector art editors like Inkscape and Illustrator, and even imported into standard CAD software like Fusion 360, Solidworks, and OpenSCAD.

There is quite a lot left to explore in this area, and I’d love to see what you come up with if you make use of this code in a project!

Resources

If you’d like to learn more about the differential growth process and it’s implementation, here is a list of relevant articles and code projects that I found helpful in my own research:

--

--

Jason Webb

Creative technologist exploring digital morphogenesis through code, simulation, and digital fabrication.