Drawing Circles with Lines: An Exploration of Structured Node Gardens

Title image for the

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.

[Japanese version / 日本語版はこちら]

 

Concept and Inspiration

The central theme of this exploration is "drawing lines between nodes arranged along a circumference."

Creative coding example: connecting nodes arranged on a circle

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.

  1. nodes
  2. minimum distance
  3. 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;
}

Nodes evenly distributed on a circle

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;
}

Node garden example with nodes on a circle

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;

Reduced node count example


  const ringNum = 6;
  const nodeNumMin = 6;

Multi-circle variation

 

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));
  }

Example using circles with shifted centers

And next, you can change the look by changing the condition of the drawing line.


  drawNodeGarden(nodes, baseDist * 0.51, baseDist * 0.52);

Lines drawn only between distant node pairs


  drawNodeGarden(nodes, baseDist * 0.14, baseDist * 0.15);

Lines drawn only between adjacent node pairs

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);

Colorized node garden example

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;

Example with limited hue range






 

The p5.js/Processing Example Codes

I wrote an application code in p5.js/Processing. The license is CC0. You can use these freely.

Final version: connecting nodes on a circle

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.

 

Next Post Previous Post
No Comment
Add Comment
comment url