Thank you Tyler Hobbs for the inspiration

I recently found this piece of art (LINES 2A (2017)) created by Tyler Hobbs. This picture kinda looked very hand drawn, but it's completely generative. Something about this drawing and it's texture kind of resonated with me, so I wanted to try to study and replicate (or make something inspired by this work) using p5js.

I started out by plotting a bunch of random points within a circle like so.

w = 1000
function setup() {
  createCanvas(w, w);
  background('#F9F8F4');
}

function draw() {
  x = random(w)
  y = random(w)
  if (pow(w/2 - x, 2) + pow(w/2 - y, 2) < 7e4) {
    point(x,y)
  }
}

This is a painfully slow process to generate random points in a circle. I found a better way to do this later. What I wanted to do next was to generate flow fields, but restricted to the circular region.

It's super easy to generate flow field patterns using perlin noise.

  1. Choose a random point <x,y>
  2. Plot <x,y>
  3. Calculate n = noise(x,y)
  4. Do x+=cos(n * 2 * PI) and y+=sin(n * 2 * PI)
  5. Repeat 2.

We're going to plot flow fields inside the circle. Let's try this.

const is_in_circle = (x, y) => 
  (pow(w / 2 - x, 2) + pow(w / 2 - y, 2) < 7e4)

function draw() {
  if (is_in_circle(x = random(w), y = random(w)))
    while (is_in_circle(x, y)) {
      n = noise(x, y)
      x += sin(n * TAU)
      y += cos(n * TAU)
      point(x, y)
    }
}

OK, not very good. The noise at this level is pretty rough. we're going to zoom in to the noise function (by dividing the x,y inputs by some constant value) and probably use circle(x ,y ,0.3) to plot points instead if point function, because I feel it looks way smoother. Also, I'm adding a random() > 0.01 condition in the loop so that we also get short lines that are not trimmed away by the edge of the circle.

function draw() {
  if (is_in_circle(x = random(w), y = random(w)))
    while (is_in_circle(x, y) && random() > 0.01) {
      n = noise(x / 500, y / 500)
      x += sin(n * TAU)
      y += cos(n * TAU)
      circle(x, y, .3)
    }
}

Actually.. not bad. I think we manage almost replicate the original texture. The inverted version also looks pretty good.

I went ahead and made a つぶやきProcessing version of this.

Going Further: Animations

The code we wrote right now technically is animated. The animation however is not very smooth.

To make smooth animations, we need to generate new points in the circle, keep track of these points outside the draw() function. I found this neat technique, to find random points in a circle where a random radius r and angle theta are chosen and the x,y points are obtained as x = centerX + r * cos(theta) and y = centerY + r * sin(theta)

Let's try that first.

function random_point() {
  r = random(w / 4)
  t = random(TAU)
  return [
    w/2 + cos(t) * r, 
    w/2 + sin(t) * r
  ]
}

function setup() {
  createCanvas((w = 1e3), w);
  background(255)
  k = w / 2
  m = (Array(w).fill(0)).map(random_point)
}

function draw() {
  for (i = k; --i;) {
    [x, y] = m[i]
    circle(x, y, .3);
  }
}

and now we apply flow fields and try to move these points.

function random_point() {
  r = random(w / 4)
  t = random(TAU)
  return [
    w/2 + cos(t) * r, 
    w/2 + sin(t) * r
  ]
}

const w = 1000
function setup() {
  createCanvas(w, w);
  background('#F9F8F4')
  k = w / 2
  points = (Array(k).fill(0)).map(random_point)
}

function draw() {
  for (i = k; --i;) {
    [x, y] = m[i]
    x += sin(n = noise(x / 400, y / 400) * TAU) * h
    y += cos(n) * h
    stroke(i%255)
    circle(x, y,.3)
    if (pow(k - x, 2) + pow(k - y, 2) < 7e4)  // if point is in circle
      points[i] = [x, y, t]
    else points[i] = random_point() // replace with new point if not
  }
}

And a つぶやきProcessing version of course..

Adding Colors

There are many strategies to colorizing this sketch. One is by just giving each particle a random initial color.

However, I found that maintaining the initial x or y position in the particle array and using that to derive the hue information gives us some nice Jupiter/gaseous planet vibes.

The fringing at the sides can be avoided by moving 50% of the points in the reverse direction.

More color variations

And that's it. Hope this was educational!