# Perlin Noise - Flow Field

Category: projects
Published: 2020-12-08
Tags: p5.js, perlin noise, particles

I've always had this particular exercise in the back of my mind ever since I saw, well, the thumbnail to the [Coding Train](https://www.youtube.com/watch?v=BjoM9oKOAKY) video, but there was no point in doing it until I had some idea of what Perlin Noise was and get a bit more comfortable with both the setup on this site and P5.js, but having done that, I jumped at this and it's been most rewarding.

While this exercise needs a bit more tweaking, I want to take the time to walk through what's happening while it's fresh in my memory. As an overview, what you're seeing being generated bellow is the combination of two things, force and moving particles. We're building a grid of force vectors, spread out across the canvas into which we drop individual particles at random locations which, as literarily as leaves in the wind, move around the canvas leaving colored trails behind. Let's look a little bit closer to each part of this puzzle.

# Force vectors

So following the plan layed out above, the first piece of this puzzle is the vectors. The idea here is to cover our nice little canvas with a series of vectors all pointing to slightly different directions and to do that we'll use Perlin Noise, as we did in the previous examples, but instead of placing pixels with a given color, we'll orient vectors to a given angle based on the numbers that we pull from the Perlin Noise data.

<div class="text-center">
  <video autoplay=true loop=true muted=true>
      <source src="/content/creative-code-lab/projects/perlin-noise-flow-field/images/vectors.mp4" type="video/mp4">
      <source src="/content/creative-code-lab/projects/perlin-noise-flow-field/images/vectors.webm" type="video/webm">
  </video>
  <br>
</div>

As you can see, the little lines are spread out evenly on the canvas and their orientation changes ever so slightly as time passes. We do this by navigating all three dimensions of the Perlin Noise space, mapping the first two to the location on the screen and iterating the third dimension forward on each animation frame. Given the Perlin Noise characteristics the results are random but are also all related between neighboring locations.

# Particles

With the field in place, we can add the final piece to our puzzle, the particles. Particles are the only visible parts in this exercise, they move around influenced by the vectors we created above and they leave a trail as they move, that's what'll ultimately generate the images we see.


<div class="text-center">
  <video autoplay=true loop=true muted=true>
      <source src="/content/creative-code-lab/projects/perlin-noise-flow-field/images/particles.mp4" type="video/mp4">
      <source src="/content/creative-code-lab/projects/perlin-noise-flow-field/images/particles.webm" type="video/webm">
  </video>
  <br>
</div>

As you see the behavior is to follow the pathways created by the vectors, particles are randomly placed but once the "currents" take hold they start to follow common paths - where the combined forces push them into - but the field itself changes over time so particle groups are broken apart and new paths emerge, which is why we see this organic like behavior begin to emerge.

You can play around with a few settings bellow, namely:

* **Color Scheme**: I've added a few different color schemes for background and lines, happy to throw a few more so hit me with suggestions
* **Number of particles**: The more particles you throw at the system the more "paint" we get on, try to change this dynamically , start low end high :)
* **Angle Multiplier**: This influences the angles the vectors might take on. The default value is one, which means the angles vary between 0 and 360° but as the videos mention and someone brought up to me, Perlin Noise as a typical bell curve distribution, which means some angles will be statistically rare and this variable influences that by increasing the range, i.e. say the magnitude is 2, the new range is angles between 0 and 720° . The more you push this the more you'll see particles going in circles as the field is changing much faster beneath them (my metaphors need work, I know)
* **Magnitude Multiplier**: Same as above but applied to the magnitude of the vector. The higher this number the "stronger" the vectors will influence the particles
* **Debug mode**: While this will decrease the frame-rate quite a bit will let you see exactly what's going on, you can see individual particules flowing through the system and you'll see all the vectors that influence them. Good to see how the other controls affect the system


Finally there are some basic sketch controls you can use:

* **Play/Pause** will ... well, start - stop the system
* **Reset** Restarts the system
* **Download** Downloads the image you see on the canvas, the filename contains all the settings and it's time stamped so you can download as much as you want without bothering to rename the files
* **Fullscreen** If you're feeling adventurous with the framerate, go fullscreen and get as much resolution out of the sketch as possible (note: this really just sets the page to fullscreen, not the canvas - which would be an improvement I realize)

Finally, if you end up generating something cool be sure to share and let me know - say you can always [@ me on Twitter](https://twitter.com/dramalho), I would love to see what you made.

## Source (sketch.js)

```javascript
class Particle {
  constructor(sketch, grid_size_in_pixels, color_set) {
    this.sketch = sketch
    this.grid_size_in_pixels = grid_size_in_pixels

    this.max_speed = 5
    this.color_set = color_set
    this.color_increment_modifier = 0.001

    this.reset()
  }

  setColorSet(color_set) {
    this.color_set = color_set
    this.color_increment = 0
  }

  reset() {
    this.position = this.sketch.createVector(this.sketch.random(this.sketch.width),this.sketch.random(this.sketch.height))
    this.velocity = this.sketch.createVector(0,0)
    this.acceleration = this.sketch.createVector(0,0)
    this.previous_position = this.position.copy()
    this.color_increment = 0
  }

  update() {
    this.velocity.add(this.acceleration)
    this.velocity.limit(this.max_speed)

    this.position.add(this.velocity)
    this.acceleration.mult(0)

    this.edges()
  }

  applyForce(force) {
    this.acceleration.add(force)
  }

  show(draw_as_point = false) {
    this.sketch.push()

    if (draw_as_point) {
      this.sketch.stroke(this.color_set[0].toString('#rrggbb')) // Removes the alpha channel
      this.sketch.strokeWeight(3)
      this.sketch.point(this.position.x, this.position.y)
    } else {
      this.sketch.stroke(this.sketch.lerpColor(this.color_set[0], this.color_set[1], this.color_increment))
      this.sketch.strokeCap(this.sketch.PROJECT);
      this.sketch.strokeWeight(1)
      this.sketch.line(this.position.x, this.position.y, this.previous_position.x, this.previous_position.y)
    }

    this.sketch.pop()

    this.previous_position = this.position.copy()
    this.color_increment += this.color_increment_modifier
    if (this.color_increment > 1 || this.color_increment < 0) this.color_increment_modifier *= -1
  }

  edges() {
    if (this.position.x > this.sketch.width) {
      this.position.x = 0
      this.previous_position.x = 0
    }

    if (this.position.x < 0) {
      this.position.x = this.sketch.width
      this.previous_position.x = this.sketch.width
    }

    if (this.position.y > this.sketch.height) {
      this.position.y = 0
      this.previous_position.y = 0
    }

    if (this.position.y < 0) {
      this.position.y = this.sketch.height
      this.previous_position.y = this.sketch.height
    }
  }

  follow(vectors) {
    var x = this.sketch.floor(this.position.x / this.sketch.pixelDensity() / this.grid_size_in_pixels)
    var y = this.sketch.floor(this.position.y / this.sketch.pixelDensity() / this.grid_size_in_pixels)
    var cols = this.sketch.floor(this.sketch.width / this.grid_size_in_pixels)

    var index = x + y * cols

    this.applyForce(vectors[index])
  }
}


//= require creative-code-lab/particle.js

'use strict'

// Avoiding Global Mode: https://github.com/processing/p5.js/wiki/Global-and-instance-mode
const perlin_noise = ( sketch ) => {

  // This is the element where we'll create our P5 canvas
  let container = document.getElementById('sketch-holder')

  let gridSizeInPixels = 10
  let start = 0
  let increment = 0.01

  let angle_multiplier = 1
  let magnitude_multiplier = 1

  let canvas_el;
  let number_of_particles = 250
  let particles = [];
  let flow_field = [];
  let color_sets;
  let current_color_scheme = 'bw1'

  let debug_mode = false

  sketch.setup = () => {
    canvas_el = sketch.createCanvas(container.clientWidth, container.clientHeight)
    canvas_el.parent(container)

    color_sets = {
      bw1: {
        background: "#FFFFFF",
        lines: [
          sketch.color('rgba(0, 0, 0, 0.1)'),
          sketch.color('rgba(140, 140, 140, 0.1)')
        ],
        blend_mode: sketch.DARKEST
      },
      bw2: {
        background: "#000000",
        lines: [
          sketch.color('rgba(140, 140, 140, 0.1)'),
          sketch.color('rgba(255, 255, 255, 0.1)')
        ],
        blend_mode: sketch.LIGHTEST
      },
      color1: {
        background: "#000000",
        lines: [
          sketch.color('rgba(251, 82, 177, 0.1)'),
          sketch.color('rgba(250, 239, 80, 0.1)')
        ],
        blend_mode: sketch.LIGHTEST
      },
      color2: {
        background: "#000000",
        lines: [
          sketch.color('rgba(210, 131, 208, 0.1)'),
          sketch.color('rgba(89, 247, 137, 0.1)')
        ],
        blend_mode: sketch.LIGHTEST
      },
      color3: {
        background: "#FFFFFF",
        lines: [
          sketch.color('rgba(255, 0, 0, 0.1)'),
          sketch.color('rgba(0, 0, 255, 0.1)')
        ],
        blend_mode: sketch.DARKEST
      },

    }

    sketch.blendMode(color_sets[current_color_scheme].blend_mode)
    sketch.background(color_sets[current_color_scheme].background)
    sketch.noiseDetail(8, 0.6)

    updateParticleCount(number_of_particles)

    installControlHandlers()

    sketch.noLoop() // Don't auto-start experiment
  }

  function updateParticleCount(value) {
    number_of_particles = parseInt(value)

    if (number_of_particles < particles.length) {
      particles.splice(number_of_particles)
    } else {
      for(var i = particles.length; i < number_of_particles; i++) {
        particles[i] = new Particle(sketch, gridSizeInPixels, color_sets[current_color_scheme].lines)
      }
    }
  }

  function updateColorScheme(scheme) {
    if (!Object.keys(color_sets).includes(scheme)) return

    current_color_scheme = scheme

    sketch.clear()

    if (debug_mode) {
      sketch.blendMode(sketch.BLEND)
    } else {
      sketch.blendMode(color_sets[scheme].blend_mode)
    }

    sketch.background(color_sets[scheme].background)
    particles.forEach(particle => { particle.setColorSet(color_sets[scheme].lines) } )
  }

  function reset() {
    particles.forEach(particle => particle.reset())
    sketch.clear()
    updateColorScheme(current_color_scheme)
  }

  function installControlHandlers() {
    // Pause / Play
    document.getElementById("flow-field-control-pause-toggle").addEventListener("click", function() {
      this.querySelectorAll('svg').forEach( (el) => {
        if (!el.classList.contains('hidden')) { // Look for the active icon
          if (el.classList.contains('icon-pause')) {
            sketch.noLoop()
          } else if (el.classList.contains('icon-play')) {
            sketch.loop()
          }
        }

        el.classList.toggle('hidden')
      })
    })

    // Fullscreen
    document.getElementById("flow-field-control-fullscreen-toggle").addEventListener("click", function() {
      this.querySelectorAll('svg').forEach( (el) => {
        if (!el.classList.contains('hidden')) { // Look for the active icon
          if (el.classList.contains('icon-compress')) {
            sketch.fullscreen(false)
          } else {
            sketch.fullscreen(true)
          }
        }

        el.classList.toggle('hidden')
      })
    })

    document.getElementById("flow-field-control-redo").addEventListener("click", reset)
    document.getElementById("flow-field-control-color-scheme").addEventListener("change", function(event) {
      updateColorScheme(event.target.value)
    })


    document.getElementById("flow-field-control-particle-count").addEventListener("input", function(event) {
      document.getElementById("slider-particle-counter").innerHTML = event.target.value
    })
    document.getElementById("flow-field-control-particle-count").addEventListener("change", function(event) {
      updateParticleCount(event.target.value)
    })

    document.getElementById("flow-field-control-angle-multiplier").addEventListener("input", function(event) {
      document.getElementById("slider-angle-multiplier").innerHTML = event.target.value
    })
    document.getElementById("flow-field-control-angle-multiplier").addEventListener("change", function(event) {
      angle_multiplier = parseInt(event.target.value)
    })

    document.getElementById("flow-field-control-magnitude-multiplier").addEventListener("input", function(event) {
      document.getElementById("slider-magnitude-multiplier").innerHTML = event.target.value
    })
    document.getElementById("flow-field-control-magnitude-multiplier").addEventListener("change", function(event) {
      magnitude_multiplier = parseInt(event.target.value)
    })

    document.getElementById("flow-field-control-debug-mode").addEventListener("change", function(event) {
      debug_mode = event.target.checked

      if (debug_mode) {
        sketch.blendMode(sketch.BLEND)
      } else {
        updateColorScheme(current_color_scheme)
      }
    })

    document.getElementById("flow-field-control-save").addEventListener("click", function() {
      sketch.save(canvas_el, `perlin_noise_flow_field_${current_color_scheme}_${debug_mode}_${number_of_particles}_${angle_multiplier}_${magnitude_multiplier}_${Math.floor(Date.now() / 1000)}`, 'png')
    })

    // p5's fullscreen() always requests fullscreen on <html>, not on
    // #sketch-holder itself (see the :fullscreen CSS in application.css) —
    // and the native windowResized()/resize event doesn't reliably fire in
    // time with the CSS transition that actually grows #sketch-holder, so
    // the canvas was staying at its pre-fullscreen size. Explicitly
    // resizing on fullscreenchange (fired once the transition — in or out —
    // has actually settled) fixes both directions.
    document.addEventListener('fullscreenchange', () => sketch.windowResized())
  }

  sketch.windowResized = () => {
    sketch.resizeCanvas(container.clientWidth, container.clientHeight)
    // resizeCanvas() clears the canvas back to blank, so without this the
    // trails-so-far vanish and the canvas is left on whatever the cleared
    // default is (not the color scheme's own background) until the next
    // debug_mode-only repaint. Most noticeable via the fullscreen toggle,
    // which now actually exercises this path (see the fullscreenchange
    // listener above) — a plain browser-window resize hit the same gap.
    sketch.background(color_sets[current_color_scheme].background)
    sketch.draw()
  }

  sketch.draw = () => {
    if (debug_mode) sketch.background(color_sets[current_color_scheme].background)

    var z_offset = start
    var density = sketch.pixelDensity()

    var x = 0
    var y = 0
    var index = 0
    var color = 0
    var angle = 0

    // This maps noise values onto a 2D space
    for(x = 0; x < sketch.width * density; x += gridSizeInPixels * density) {
      for(y = 0;  y < sketch.height * density; y += gridSizeInPixels * density) {
        index = (x + y * sketch.width * density) * 4;
        noise_value = sketch.noise(
          x / density * increment,  // Use the X as index but transform to smaller steps in the noise space
          y / density * increment,  // Use the Y as index but transform to smaller steps in the noise space
          z_offset
        )

        angle = sketch.map(
          noise_value,
          0,                          // Noise lower bound value
          1,                          // Noise upper bound value
          0,
          sketch.TWO_PI * angle_multiplier
        )
        magnitude = sketch.map(
          noise_value,
          0,                          // Noise lower bound value
          1,                          // Noise upper bound value
          0.5,
          1 * magnitude_multiplier
        )

        vector_index = (x / density / gridSizeInPixels) + (y/density / gridSizeInPixels) * sketch.floor(sketch.width / gridSizeInPixels)
        flow_field[vector_index] = p5.Vector.fromAngle(angle)
        flow_field[vector_index].setMag(magnitude)

        if (debug_mode) {
          sketch.push()
          sketch.stroke(color_sets[current_color_scheme].lines[0])
          sketch.translate(x, y)
          sketch.rotate(flow_field[vector_index].heading())
          sketch.line(0,0, gridSizeInPixels * density, 0)
          // sketch.fill(color)
          // sketch.rect(x, y, gridSizeInPixels * density, gridSizeInPixels * density)
          sketch.pop()
        }

      }
    }

    start += increment

    for(var i = 0; i < number_of_particles; i++) {
      particles[i].follow(flow_field)
      particles[i].update()
      particles[i].show(debug_mode)
    }
  }
}

// Wait for everything to load
if (document.readyState === 'complete') {
  new p5(perlin_noise)
} else {
  window.onload = (event) => {
    new p5(perlin_noise)
  }
}

```
