Building Generative Art Step by Step — A Node Garden Example

Published: (January 17, 2026 at 06:14 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

What We’re Building

When creating generative art you usually start with an image in your head: “I want it to move like this.”
When you try to implement it, it often looks complex and you don’t know where to start.

A useful approach is “break it down into elements and build up from the bottom.”
In this article I’ll demonstrate that approach using a node garden – a classic generative‑art sketch – as an example.

1️⃣ Turn the Vague Idea into Words

For this node garden I imagined:

IdeaDescription
Points floatingPoints drift around the canvas.
Lines connect when they get closeWhen two points are within a certain distance a line appears between them.
They influence each other and change movementNodes exert forces (repulsion/attraction) on one another.
Something organic feelingEach node has a personality; lines stretch in an animated way.

Even vague concepts like “something organic” should be written down for now.

2️⃣ Break Each Description into Concrete Processes

Visual ElementProcess
Points floatingDraw points; give them velocity.
Connect when closeCompute distance, check a threshold, draw a line.
Influence each otherApply forces (e.g., repulsion).
Organic feelingAssign a random “charge” per node; animate line growth.

The exact breakdown will vary from person to person. I usually separate visible elements from movement rules and then organise them into a layered dependency structure.

3️⃣ Layer Structure (Dependencies)

Layer 4: Charge + Stretch Animation

Layer 3: Line Connection & Opacity

Layer 2: Point Movement & Screen Wrap

Layer 1: Point Drawing

Upper layers cannot exist without the lower ones.
For example, to implement “force from charge” you first need “distance calculation,” and to calculate distance you need the nodes’ positions, etc.

🛠️ Let’s Write the Code – Verify at Each Layer

Below you’ll find the incremental code snippets. Each block is a self‑contained step that you can test before moving on.

Step 1 – Draw Points

// CONFIG ---------------------------------------------------------
const CONFIG = {
  width: 340,
  height: 340,
  numNodes: 60,
  nodeRadius: 3,
};

// NODE CLASS ----------------------------------------------------
class Node {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = CONFIG.nodeRadius;
  }

  draw(ctx) {
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
  }
}

// INITIALISE --------------------------------------------------
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const nodes = Array.from({ length: CONFIG.numNodes }, () =>
  new Node(
    Math.random() * CONFIG.width,
    Math.random() * CONFIG.height
  )
);

// DRAW ---------------------------------------------------------
ctx.clearRect(0, 0, CONFIG.width, CONFIG.height);
nodes.forEach(node => node.draw(ctx));

Step 2 – Give Points Velocity and Make Them Move

class Node {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = Math.random() * 3 - 1.5; // –1.5 … 1.5
    this.vy = Math.random() * 3 - 1.5;
    this.radius = CONFIG.nodeRadius;
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;

    // Wrap around the edges
    if (this.x > CONFIG.width) this.x = 0;
    else if (this.x  CONFIG.height) this.y = 0;
    else if (this.y  {
    node.update();
    node.draw(ctx);
  });
  requestAnimationFrame(loop);
}
loop();

Step 3 – Calculate Distance & Draw Lines When Within a Threshold

// EXTEND CONFIG -------------------------------------------------
const CONFIG = {
  // …previous values
  minDist: 100,
  lineWidth: 1.5,
};

class Node {
  // …previous methods
  distanceTo(other) {
    const dx = this.x - other.x;
    const dy = this.y - other.y;
    return { dx, dy, dist: Math.sqrt(dx * dx + dy * dy) };
  }
}

// UPDATED LOOP -------------------------------------------------
function loop() {
  ctx.clearRect(0, 0, CONFIG.width, CONFIG.height);
  ctx.lineWidth = CONFIG.lineWidth;

  for (let i = 0; i  CONFIG.maxSpeed) {
      this.vx = (this.vx / speed) * CONFIG.maxSpeed;
      this.vy = (this.vy / speed) * CONFIG.maxSpeed;
    }

    this.x += this.vx;
    this.y += this.vy;
    // …wrap logic (same as before)
  }

  applyForce(ax, ay) {
    this.vx += ax;
    this.vy += ay;
  }

  distanceTo(other) {
    const dx = this.x - other.x;
    const dy = this.y - other.y;
    return { dx, dy, dist: Math.hypot(dx, dy) };
  }
}

// CONNECTION STATE ---------------------------------------------
const connections = new Map(); // key: "i-j", value: { progress: 0…1 }

function getConnectionKey(i, j) {
  return i  {
    node.update();
    node.draw(ctx);
  });

  // Compute forces & connections
  for (let i = 0; i  **Note:** The code above is intentionally concise for readability. In a production project you might want to extract utilities, add UI controls, or optimise the double‑loop.

🎉 What You Have Now

  1. Points that float and wrap around the canvas.
  2. Lines that appear when points get close, with opacity based on distance.
  3. Charges that make nodes attract or repel each other.
  4. Animated line growth that gives the whole system an organic, “living” feel.

Feel free to experiment: change the number of nodes, tweak the forces, or add colour gradients. The layered approach makes it easy to extend the garden without breaking existing functionality. Happy coding!

Code Snippet

// Draw a connection between two nodes with a given opacity and progress
function drawConnection(node1, node2, opacity, progress) {
  const x1 = node1.x + (node2.x - node1.x) * progress;
  const y1 = node1.y + (node2.y - node1.y) * progress;
  const x2 = node2.x - (node2.x - node1.x) * progress;
  const y2 = node2.y - (node2.y - node1.y) * progress;

  ctx.beginPath();
  ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
  ctx.moveTo(node1.x, node1.y);
  ctx.lineTo(x1, y1);
  ctx.moveTo(node2.x, node2.y);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}

// Inside loop
if (dist < CONFIG.minDist) {
  const key = getConnectionKey(i, j);
  const currentProgress = connections.get(key) || 0;
  const newProgress = Math.min(1, currentProgress + CONFIG.lineGrowSpeed);
  connections.set(key, newProgress);

  const opacity = 1 - dist / CONFIG.minDist;
  drawConnection(node1, node2, opacity, newProgress);

  // Multiply charges (same sign → repel, opposite sign → attract)
  const interaction = node1.charge * node2.charge;
  const ax = dx * CONFIG.springAmount * interaction;
  const ay = dy * CONFIG.springAmount * interaction;
  node1.applyForce(ax, ay);
  node2.applyForce(-ax, -ay);
}

Artistic Process Overview

What’s important in implementing art expressions is this process of taking the image in your head and:

  1. Verbalizing it – put the idea into words.
  2. Breaking it into elements – identify the individual components that will make up the final piece.
  3. Organizing as layers – decide the order in which elements will be added, from background to foreground.
  4. Building from the bottom – start with the base layer and work upward.

At each layer you can verify “it’s working, it’s correct,” making it easy to spot problems. You can also decide mid‑way “I don’t need this element” and remove it.

Back to Blog

Related posts

Read more »

ASCII Clouds

Article URL: https://caidan.dev/portfolio/ascii_clouds/ Comments URL: https://news.ycombinator.com/item?id=46611507 Points: 7 Comments: 2...

Websockets with Socket.IO

This post contains a flashing gif. HTTP requests have taken me pretty far, but I’m starting to run into their limits. How do I tell a client that the server upd...