p5.js Beginner Article 8

Randomness & Noise

random() vs Perlin noise — controlled chaos for organic-feeling generative visuals.

⏱ 18 min read random noise Perlin generative organic

random()

random() returns a pseudo-random floating-point number:

random()         // 0 to 1 (exclusive)
random(10)       // 0 to 10
random(5, 15)    // 5 to 15
random(-1, 1)    // -1 to 1

Pick a random element from an array:

let colours = ['#ff6432', '#3b82f6', '#22c55e', '#f59e0b', '#c084fc'];
let c = random(colours);
fill(c);

The problem with random()

Pure randomness is noisy — visually jarring. This grid of random sizes looks scattered:

function setup() {
  createCanvas(600, 400);
  noLoop();
}

function draw() {
  background(20);
  noStroke();
  fill(100, 200, 255, 150);

  for (let x = 20; x < width; x += 30) {
    for (let y = 20; y < height; y += 30) {
      circle(x, y, random(5, 25));  // chaotic jump between sizes
    }
  }
}

Each circle is independent of its neighbours. The result is visual static, not an organic field.

noise() — Perlin noise

noise(x) returns a smooth random value between 0 and 1. Adjacent inputs produce adjacent outputs — values flow smoothly rather than jumping wildly.

// Compare
random()        // 0.83
random()        // 0.12  ← jumps anywhere
random()        // 0.67

noise(0.0)      // 0.52
noise(0.01)     // 0.53  ← smooth transition
noise(0.02)     // 0.54

Noise-driven animation

let t = 0;

function draw() {
  background(20);
  noStroke();
  fill(100, 200, 255);

  let x = map(noise(t),        0, 1, 0, width);
  let y = map(noise(t + 100),  0, 1, 0, height);  // +100 to use a different region
  circle(x, y, 30);

  t += 0.01;  // move through noise space slowly
}

The + 100 offset is important: using the same t value for both x and y would make them mirror each other. Use different offsets for each independent dimension.

Noise field grid

function setup() {
  createCanvas(600, 400);
  noLoop();
}

function draw() {
  background(20);
  noStroke();

  for (let x = 20; x < width; x += 25) {
    for (let y = 20; y < height; y += 25) {
      let nx  = x / 200;          // scale controls how "zoomed in" the noise is
      let ny  = y / 200;
      let n   = noise(nx, ny);    // 2D noise
      let sz  = map(n, 0, 1, 4, 22);
      let hue = map(n, 0, 1, 180, 320);

      colorMode(HSB, 360, 100, 100);
      fill(hue, 70, 90, 80);
      circle(x, y, sz);
    }
  }
}

Change 200 to a smaller number (like 100) to zoom in — the shapes grow smoother and larger. A larger number (like 400) gives more variation.

3D noise for animation

Pass a third argument t to animate the noise field over time:

function draw() {
  background(15);
  noStroke();

  let t = frameCount * 0.003;

  for (let x = 15; x < width; x += 25) {
    for (let y = 15; y < height; y += 25) {
      let n  = noise(x / 200, y / 200, t);
      let sz = map(n, 0, 1, 2, 20);
      colorMode(HSB, 360, 100, 100);
      fill(map(n, 0, 1, 200, 300), 70, 90);
      circle(x, y, sz);
    }
  }
}

noiseSeed() and randomSeed()

Both noise() and random() are deterministic given a seed — the same seed always produces the same sequence:

function setup() {
  createCanvas(600, 400);
  noiseSeed(42);    // fix the noise pattern
  randomSeed(99);   // fix random() sequence
  noLoop();
}

This is essential for reproducible generative art — you can save an interesting seed number and regenerate the exact same image.

Flow fields

A flow field assigns a direction (angle) to every point in space using noise. Particles follow these directions:

let particles = [];
const NUM = 300;
const SCALE = 0.004;

function setup() {
  createCanvas(700, 500);
  background(15);
  for (let i = 0; i < NUM; i++) {
    particles.push({
      x: random(width),
      y: random(height),
      hue: random(180, 300)
    });
  }
}

function draw() {
  // Slow fade instead of clear — creates trails
  background(15, 15, 20, 8);

  colorMode(HSB, 360, 100, 100, 100);

  for (let p of particles) {
    let angle = noise(p.x * SCALE, p.y * SCALE) * TWO_PI * 2;

    p.x += cos(angle) * 1.5;
    p.y += sin(angle) * 1.5;

    // Wrap around edges
    if (p.x < 0) p.x = width;
    if (p.x > width) p.x = 0;
    if (p.y < 0) p.y = height;
    if (p.y > height) p.y = 0;

    stroke(p.hue, 60, 90, 40);
    strokeWeight(1);
    point(p.x, p.y);
  }
}

Gaussian (normal) distribution

randomGaussian() returns values clustered around 0, with a bell-curve distribution — things close to the centre are more likely:

// randomGaussian(mean, standardDeviation)
let x = randomGaussian(width / 2, 80);  // clustered around centre

Use it for effects where most particles should be near a focal point, with a few outliers.


Key takeaways

  • random() gives unpredictable jumps; noise() gives smooth, organic variation
  • noise(x, y, t) takes 1, 2, or 3 coordinates — use multiple offsets for independent dimensions
  • Noise scale controls zoom: small values = smooth/slow variation, large = fine/rapid
  • randomSeed() and noiseSeed() make sequences reproducible
  • Flow fields use noise to assign directions and steer particles organically
  • randomGaussian() creates normally-distributed values for more natural-looking scatter