Drawing Circles with Lines: An Exploration of Structured Node Gardens
Node Garden is a fundamental yet versatile technique in creative coding, where lines are dynamically drawn between nodes based on their proximity. With relatively simple logic, it can generate incredibly intricate and beautiful patterns.
In this article, I'll showcase a few explorations using the Node Garden technique, accompanied by source code. Feel free to use these examples as a starting point for your own creative projects.
I've used p5.js to explain the core logic, and you can find the complete source code for both p5.js and Processing at the end of this post.
Concept and Inspiration
The central theme of this exploration is "drawing lines between nodes arranged along a circumference."
This idea was inspired by a YouTube live session: "Daily Coding Live Sessions #15 [ARTSCLOUD]" by the talented creative coder @takawo.
I'm a frequent viewer of @takawo’s sessions. It’s fascinating to observe his creative process and the evolving thoughts behind his code.
What would happen if @takawo's random nodes were rearranged into a more disciplined structure? I was fascinated by the possibility of finding beauty in the delicate balance between rigid order and organic disorder. This curiosity became the driving force behind this project.
The Basic Code to Create the Node Garden
If you want to know what's the Node Garden and how to create it, you can read my articles that explain it simply.
The basic code to create the Node Garden is here.
/*
* The basic code to create the 'Node Garden'
* p5.js
* @author @deconbatch
* @version 0.1
* created 2022.05.04
* license CC0 https://creativecommons.org/publicdomain/zero/1.0/
*/
function setup() {
createCanvas(640, 640);
const baseDist = min(width, height);
const nodes = getNodes();
drawNodeGarden(nodes, baseDist * 0.1, baseDist * 0.2);
for (let n of nodes) {
circle(n.x, n.y, 10);
}
}
/*
* getNodes : returns the nodes array
*/
function getNodes() {
const nodes = new Array();
for (let i = 0; i < 50; i++) {
nodes.push(
createVector(
random(width),
random(height)
)
);
}
return nodes;
}
/*
* drawNodeGarden : draw the lines between nodes
*/
function drawNodeGarden(_nodes, _minDist, _maxDist) {
for (let n of _nodes) {
for (let m of _nodes) {
let d = dist(n.x, n.y, m.x, m.y);
if (d > _minDist && d < _maxDist) {
line(n.x, n.y, m.x, m.y);
}
}
}
}
I wrote functions for placing nodes (getNodes) and drawing (drawNodeGarden). So you can modify this code easily.
The getNodes() function places the nodes randomly at this time. You can make your original getNodes() function that places the nodes as you like.
The drawNodeGarden() gets three parameters as below and draws line between nodes when the distance is within the minimum and the maximum value.
- nodes
- minimum distance
- maximum distance
Note: The current implementation uses a nested loop that draws each connection twice. While computational complexity and redundant drawing would require optimization in a large-scale system, I've prioritized code readability and simplicity here, as the performance impact is negligible for a few thousand nodes.
The code below shows the place of nodes. You can verify if the node placement is correct. Of course, you can delete it if you don't need to check.
for (let n of nodes) {
circle(n.x, n.y, 10);
}
Create a Piece of Work by Changing the getNodes() Function
Let's place the 36 nodes on the circumference.
function getNodes() {
const nodeNum = 36; // node number on the circumference
const ringR = min(width, height) * 0.4; // radius
const nodes = new Array();
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t);
let y = ringR * sin(t);
nodes.push(createVector(x, y));
}
return nodes;
}
The placing of nodes looks good. Then make three different radius rings and place concentric.
The node number increases toward the outside.
function getNodes() {
const ringNum = 3; // ring number
const nodeNumMin = 18; // node number on most inner ring
const ringMaxR = min(width, height) * 0.4;
const nodes = new Array();
for (let ring = 0; ring < ringNum; ring++) {
let ringR = ringMaxR * (ring + 1) / ringNum;
let nodeNum = (ring + 1) * nodeNumMin;
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t);
let y = ringR * sin(t);
nodes.push(createVector(x, y));
}
}
return nodes;
}
It looks like a wheel with too many spokes. The looks change when you change the number of nodes and the number of rings.
const ringNum = 3;
const nodeNumMin = 6;
const ringNum = 6;
const nodeNumMin = 6;
Refining the Visual Aesthetics
Let's tweak the parameters to give the visuals more character.
First, I introduced a slight offset to the center of each ring. By shifting the origin point, we can transform a rigid geometric shape into something more organic and swirly.
phaseT means the direction of moving. phaseD means the distance of moving.
let phaseT = random(TWO_PI);
let phaseD = ringMaxR * 0.25 / ringNum;
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t) + cos(phaseT) * phaseD;
let y = ringR * sin(t) + sin(phaseT) * phaseD;
nodes.push(createVector(x, y));
}
And next, you can change the look by changing the condition of the drawing line.
drawNodeGarden(nodes, baseDist * 0.51, baseDist * 0.52);
drawNodeGarden(nodes, baseDist * 0.14, baseDist * 0.15);
Next, let's add some color. By switching to HSB mode, we can dynamically map the stroke hue to the angle of each line.
function setup() {
createCanvas(640, 640);
colorMode(HSB, 360, 100, 100, 100); // make the color mode 'HSB'
// change the hue value with the angle
let lHue = map(atan2(m.y - n.y, m.x - n.x), -PI, PI, 0, 360);
stroke(lHue, 60, 80, 100);
line(n.x, n.y, m.x, m.y);
Pro-tip: If the result feels a bit too flashy, try limiting the hue range (for example, to a 120-degree span) to create a more harmonious color palette.
// ex. 220(cyan) to 340(violet)
let lHue = (220 + map(atan2(m.y - n.y, m.x - n.x), -PI, PI, 0, 120)) % 360;
The p5.js/Processing Example Codes
I wrote an application code in p5.js/Processing. The license is CC0. You can use these freely.
The Example Code of p5.js
/*
* An example creative coding work of drawing lines between nodes that are placed on the circumference.
*
* p5.js
* @author @deconbatch
* @version 0.1
* created 2022.05.04
* license CC0 https://creativecommons.org/publicdomain/zero/1.0/
*/
function setup() {
createCanvas(980, 980);
colorMode(HSB, 360, 100, 100, 100);
smooth();
noLoop();
const ringNum = 26; // ring number
const nodeMin = 4; // node number on most inner ring
const minDist = 1.2; // min distance to draw nodes
const maxDist = 1.8; // max distance to draw nodes
const baseSiz = min(width, height) * 0.45;
const baseHue = random(360);
background((baseHue + 240) % 360, 90, 30, 100);
noFill();
translate(width * 0.5, height * 0.5);
rotate(random(PI));
drawNodeGarden(
getNodes(baseSiz, ringNum, nodeMin),
baseSiz * minDist / ringNum,
baseSiz * maxDist / ringNum,
baseHue
);
}
/*
* getNodes : place the nodes and return nodes array
*
* _ringMaxR : max radius of the ring
* _ringNum : ring number
* _nodeNumMin : node number on most inner ring
*/
function getNodes(_ringMaxR, _ringNum, _nodeNumMin) {
const nodes = new Array();
for (let ring = 0; ring < _ringNum; ring++) {
let ringR = _ringMaxR * (ring + 1) / _ringNum;
let nodeNum = (ring + 1) * _nodeNumMin;
let phaseT = random(TWO_PI);
let phaseD = _ringMaxR * 0.5 / _ringNum;
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t) + cos(phaseT) * phaseD;
let y = ringR * sin(t) + sin(phaseT) * phaseD;
nodes.push(createVector(x, y));
}
}
return nodes;
}
/*
* drawNodeGarden : draw between nodes by the distance condition
*
* _nodes : nodes array
* _min, _max : draw line when _min < distance between nodes < _max
* _hue : base hue value
*/
function drawNodeGarden(_nodes, _min, _max, _hue) {
strokeWeight(3);
for (let i = 0; i < _nodes.length - 1; i++) {
let n = _nodes[i];
for (let j = i + 1; j < _nodes.length; j++) {
let m = _nodes[j];
let d = dist(n.x, n.y, m.x, m.y);
if (d > _min && d < _max) {
let lHue = _hue + map(d, _min, _max, 0, 120);
stroke(lHue % 360, 60, 80, 100);
line(n.x, n.y, m.x, m.y);
}
}
}
}
The Example Code of Processing
/*
* An example creative coding work of drawing lines between nodes that are placed on the circumference.
*
* Processing 3.5.3
* @author @deconbatch
* @version 0.1
* created 2022.05.04
* license CC0 https://creativecommons.org/publicdomain/zero/1.0/
*/
void setup() {
size(980, 980);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
smooth();
noLoop();
int ringNum = 26; // ring number
int nodeMin = 4; // node number on most inner ring
float minDist = 1.2; // min distance to draw nodes
float maxDist = 1.8; // max distance to draw nodes
float baseSiz = min(width, height) * 0.45;
float baseHue = random(360);
background((baseHue + 240.0) % 360.0, 90.0, 30.0, 100.0);
noFill();
translate(width * 0.5, height * 0.5);
rotate(random(PI));
drawNodeGarden(
getNodes(baseSiz, ringNum, nodeMin),
baseSiz * minDist / ringNum,
baseSiz * maxDist / ringNum,
baseHue
);
}
/*
* getNodes : place the nodes and return nodes array
*
* _ringMaxR : max radius of the ring
* _ringNum : ring number
* _nodeNumMin : node number on most inner ring
*/
ArrayList<PVector> getNodes(float _ringMaxR, int _ringNum, int _nodeNumMin) {
ArrayList<PVector> nodes = new ArrayList();
for (int ring = 0; ring < _ringNum; ring++) {
float ringR = _ringMaxR * (ring + 1) / _ringNum;
int nodeNum = (ring + 1) * _nodeNumMin;
float phaseT = random(TWO_PI);
float phaseD = _ringMaxR * 0.5 / _ringNum;
for (int nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
float t = TWO_PI * nodeCnt / nodeNum;
float x = ringR * cos(t) + cos(phaseT) * phaseD;
float y = ringR * sin(t) + sin(phaseT) * phaseD;
nodes.add(new PVector(x, y));
}
}
return nodes;
}
/*
* drawNodeGarden : draw between nodes by the distance condition
*
* _nodes : nodes array
* _min, _max : draw line when _min < distance between nodes < _max
* _hue : base hue value
*/
void drawNodeGarden(ArrayList<PVector> _nodes, float _min, float _max, float _hue) {
strokeWeight(3.0);
for (int i = 0; i < _nodes.size() - 1; i++) {
PVector n = _nodes.get(i);
for (int j = i + 1; j < _nodes.size(); j++) {
PVector m = _nodes.get(j);
float d = dist(n.x, n.y, m.x, m.y);
if (d > _min && d < _max) {
float lHue = _hue + map(d, _min, _max, 0.0, 120.0);
stroke(lHue % 360.0, 60.0, 80.0, 100.0);
line(n.x, n.y, m.x, m.y);
}
}
}
}
The source code is released under the CC0 license, so feel free to experiment, modify, and build upon it for your own projects.










