Orthogonal Logic: Animating a Non-Traditional Node Garden
Reinventing the Node Garden
This Processing-based animation explores a variation of the classic "Node Garden" algorithm. By replacing proximity-based connections with a stricter set of geometric rules, the piece creates a structured, evolving lattice of lines and shapes.
This piece demonstrates a smooth morphing transition between different node garden configurations.
The Classic Approach: Distance-Based Connections
In a standard node garden, lines are drawn between nodes based purely on their distance. While effective, the result often looks chaotic and tangled.
if (dist(xa, ya, xb, yb) < 100) {
line(xa, ya, xb, yb);
}
It's a solid starting point, but I wanted something cleaner—something with more structural intent.
Introducing Constraints: Grid-Based Nodes
The first step was to constrain the nodes to a grid. This immediately brings a sense of order to the composition.
pvs.push(createVector(
floor(random(1.0, 10.0)) / 10,
floor(random(1.0, 10.0)) / 10
));
The real shift happens here: instead of connecting nodes by distance, I only draw lines between those that align horizontally or vertically (along the X or Y axis).
if (xa == xb || ya == yb) {
line(xa, ya, xb, yb);
}
Since working with floats can be tricky, I used a small epsilon (0.01) to check for axis alignment.
if (abs(xF - xT) < 0.01 || abs(yF - yT) < 0.01) {
The Final Result: Dynamic Evolution
This simple constraint yields fascinatingly diverse patterns. To add depth, I mapped the size of each node to its connection count, or "degree" in graph theory terms.
Now, let's bring the system to life through motion. I implemented a morphing function to transition between different states.
// morphing
float xF = _to.get(f).x * _rate + _from.get(f).x * (1.0 - _rate);
float yF = _to.get(f).y * _rate + _from.get(f).y * (1.0 - _rate);
The result is a fluid, rhythmic transformation—the true joy of creative coding.
Implementation in Processing
The following script generates a series of high-resolution frames directly to disk. It is designed for offline rendering and does not display an image window during execution. These frames are intended to be compiled into a final animation.
This code is provided under the GPL license. Feel free to experiment with it—I would be honored to see any works inspired by this approach.
/**
* Johnny on the Monorail.
* It's an animation of a strange kind of Node Garden.
*
* @author @deconbatch
* @version 0.1
* Processing 3.5.3
* 2020.08.25
*/
void setup() {
size(720, 720);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
rectMode(CENTER);
smooth();
noLoop();
}
void draw() {
int nodeCnt = 30;
int frmMax = 24 * 12; // 24fps x 12sec
int frmMorph = 24 * 4; // morphing duration frames
ArrayList<PVector> nodesFrom = new ArrayList<PVector>();
ArrayList<PVector> nodesTo = setNodes(nodeCnt);
for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
float easeRatio = InFourthPow(map(frmCnt % frmMorph, 0, frmMorph - 1, 0.0, 1.0));
// copy to -> from
if (frmCnt % frmMorph == 0) {
nodesFrom = new ArrayList<PVector>(nodesTo);
nodesTo = setNodes(nodeCnt);
}
blendMode(BLEND);
background(0.0, 0.0, 90.0, 100.0);
fill(0.0, 0.0, 90.0, 100.0);
stroke(0.0, 0.0, 10.0, 100.0);
strokeWeight(2.0);
construct(nodesFrom, nodesTo, easeRatio);
blendMode(BLEND);
casing();
saveFrame("frames/" + String.format("%04d", frmCnt) + ".png");
}
exit();
}
/**
* setNodes : set nodes to draw.
* @param _cnt any : node count.
* @return : nodes in ArrayList<PVector>.
*/
private ArrayList<PVector> setNodes(int _cnt) {
ArrayList<PVector> n = new ArrayList<PVector>();
for (int i = 0; i < _cnt; i++) {
n.add(new PVector(
floor(random(2.0, 9.0)) / 10.0,
floor(random(2.0, 9.0)) / 10.0
));
}
return n;
}
/**
* construct : calculate morphing location and draw.
* @param _from : nodes of start location of morphing.
* @param _to : nodes of end location of morphing.
* @param _rate 0.0 - 1.0 : start(0.0) - end(1.0)
*/
private void construct(ArrayList<PVector> _from, ArrayList<PVector> _to, float _rate) {
for (int f = 0; f < _from.size() - 1; f++) {
int cons = 0;
float xF = _to.get(f).x * _rate + _from.get(f).x * (1.0 - _rate);
float yF = _to.get(f).y * _rate + _from.get(f).y * (1.0 - _rate);
for (int t = f + 1; t < _to.size(); t++) {
float xT = _to.get(t).x * _rate + _from.get(t).x * (1.0 - _rate);
float yT = _to.get(t).y * _rate + _from.get(t).y * (1.0 - _rate);
if (abs(xF - xT) < 0.01 || abs(yF - yT) < 0.01) {
line(xF * width, yF * height, xT * width, yT * height);
cons++;
}
}
if (cons % 2 == 0) {
rect(xF * width, yF * height, cons * 5.0, cons * 5.0, 4);
} else {
ellipse(xF * width, yF * height, cons * 5.0, cons * 5.0);
}
}
}
/**
* InFourthPow : easing function.
* @param _t 0.0 - 1.0 : linear value.
* @return 0.0 - 1.0 : eased value.
*/
private float InFourthPow(float _t) {
return 1.0 - pow(1.0 - _t, 4);
}
/**
* casing : draw fancy casing
*/
public void casing() {
fill(0.0, 0.0, 0.0, 0.0);
strokeWeight(30.0);
stroke(0.0, 0.0, 0.0, 100.0);
rect(width * 0.5, height * 0.5, width, height);
strokeWeight(28.0);
stroke(0.0, 0.0, 100.0, 100.0);
rect(width * 0.5, height * 0.5, width, height);
}
/*
Copyright (C) 2020- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
Gallery: Further Examples
Here are a few more outputs generated with this specific algorithm.











