Flow Field

Particles flow through a field which influences their velocity.

  • Grid Size controls how far a particle needs to travel before being influenced by a new flow vector.
  • Noise Factor controls how smooth the changes in flow are. A higher value means smoother transitions.
  • Particle Speed controls how quickly particles move by influencing the overall strength of the field.
  • Redistribute Particles randomizes the position of each particle.
  • Randomize Flow Field re-seeds the noise function which controls flow vectors.

Source Code for this Sketch

let field
let gridsize = 20 // grid size
let drawVectors = true
let flowSmoothness = 10
let particles = []
let particleCount = 300
let particle_size = gridsize/2
let speed = 1
let flowSeed = 0

class Particle {
    constructor(c) {
        // initialize in a random position on screen
        this.v = createVector(random(0, width), random(0, height));
        if (c) {
            this.color = c
        } else {
            colorMode(HSL)
            this.color = color(random(0, 360), 100, 50)
        }
    }

    get x() {
        return this.v.x
    }

    get y() {
        return this.v.y
    }

    reset() {
        this.v.x = random(0, width)
        // reset to the top.
        // -particle_size/2 reduces the "popping" effect during reset
        this.v.y = -particle_size/2
    }

    random() {
        this.v.x = random(0, width)
        this.v.y = random(0, height)
    }

    flow(vec) {
        // If the vector is a zero value, re-initialize the particle
        if (vec.x === 0 && vec.y === 0 || vec.x > width || vec.y > height) {
            this.reset()
            return
        }

        this.v.x += vec.x*speed
        this.v.y += vec.y*speed
    }

    draw() {
        stroke(this.color)
        point(this.v.x, this.v.y)
    }
}

function initializeParticles() {
    // Particle initialization
    for (let i = 0; i < particleCount; i++) {
        if (particles[i]) {
            continue
        }
        // Bias towards cooler colors
        let hue = (360+randomGaussian(160, 85))%360
        let sat = randomGaussian(80, 10)
        let lit = randomGaussian(52, 5)

        let c = color(hue, sat, lit)
        particles[i] = new Particle(c)
    }
    particles = particles.slice(0, particleCount)
}

function randomFlowSeed() {
    flowSeed = random(0, Number.MAX_SAFE_INTEGER)
    noiseSeed(flowSeed)
    let flowSeedValue = document.getElementById("flowSeedValue")
    flowSeedValue.innerText = flowSeed.toFixed(0)
}

function initializeFlowField(x, y) {
    return createVector(noise(x / flowSmoothness, y/flowSmoothness)*2-1,
        noise(x / flowSmoothness, y / flowSmoothness))
}

function setup() {
    createCanvas(800, 600, P2D, canvas)
    chrisDefaults()
    randomFlowSeed()

    field = new FlowField(20, 20, gridsize, initializeFlowField)
    field.resizeToCanvas()
    initializeParticles()
}

function draw() {
    background(100)

    // Draw flow field
    if (drawVectors) {
        strokeWeight(.5)
        stroke(0)
        noFill()

        field.draw()
    }

    strokeWeight(particle_size)
    for (let i = 0; i < particles.length; i++) {
        particles[i].flow(field.at(particles[i].x, particles[i].y))
        particles[i].draw()
    }
}

document.addEventListener("DOMContentLoaded", (e) => {
    let p5ui = document.getElementById("p5ui")
    p5ui.innerHTML = `
	<div>
		<label for="gridSizeInput">Grid Size</label>
		<input id="gridSizeInput" type="range"
		    min="10" max="40" step="5" value="20">
		<span id="gridSizeValue" class="mono">20</span>
		<br>
		
		<label for="flowSmoothnessInput">Flow Smoothness</label>
		<input id="flowSmoothnessInput" type="range"
		    min="3" max="30" step=".1" value="10">
		<span id="flowSmoothnessValue" class="mono">10.0</span>
		<br>
		
		<label for="speedInput">Particle Speed</label>
		<input id="speedInput" type="range"
		    min=".5" max="5" step=".1" value="1.0">
		<span id="speedValue" class="mono">1.0</span>
		<br>
		
		<label for="particleCountInput">Particle Count</label>
		<input id="particleCountInput" type="range"
		    min="30" max="3000" step="10" value="300">
		<span id="particleCountValue" class="mono">300</span>
		<br>
		
		<button id="redistribute">Redistribute Particles</button>
		<button id="flowSeedInput">Randomize Flow Field</button>
		<span id="flowSeedValue" class="mono"></span>
	</div>
	`

    let gridSizeInput = document.getElementById("gridSizeInput")
    let gridSizeValue = document.getElementById("gridSizeValue")

    let flowSmoothnessInput = document.getElementById("flowSmoothnessInput")
    let flowSmoothnessValue = document.getElementById("flowSmoothnessValue")

    let speedInput = document.getElementById("speedInput")
    let speedValue = document.getElementById("speedValue")

    let particleCountInput = document.getElementById("particleCountInput")
    let particleCountValue = document.getElementById("particleCountValue")

    let redistribute = document.getElementById("redistribute")
    let flowSeedInput = document.getElementById("flowSeedInput")

    gridSizeInput.addEventListener("input", (e) => {
        field.scale = Math.round(Number(e.target.value))
        field.resizeToCanvas()
        gridSizeValue.innerText = String(field.scale)
    })

    flowSmoothnessInput.addEventListener("input", (e) => {
        // TODO get numeric type with 1 decimal precision (10.0) then cast to
        //  string. Using a string for the smoothness can result in weird behavior
        flowSmoothnessValue.innerText = Number(e.target.value).toFixed(1)
        flowSmoothness = e.target.value
        field.visit(initializeFlowField)
    })

    speedInput.addEventListener("input", (e) => {
        // TODO get numeric type with 1 decimal precision (10.0) then cast to
        //  string. Using a string for the speed can result in weird behavior
        speedValue.innerText = Number(e.target.value).toFixed(1)
        speed = e.target.value
    })

    particleCountInput.addEventListener("input", (e) => {
        // TODO get numeric type with 1 decimal precision (10.0) then cast to
        //  string. Using a string for the particle count can result in weird behavior
        particleCountValue.innerText = Number(e.target.value).toFixed(1)
        particleCount = e.target.value
        initializeParticles()
    })

    redistribute.addEventListener("click", (e) => {
        for(let i = particles.length - 1; i >= 0; i--) {
            particles[i].random()
        }
    })

    flowSeedInput.addEventListener("click", (e) => {
        randomFlowSeed()
        field.visit(initializeFlowField)
    })
})

You may use all or part of the p5 code seen on this page for your own creations, without restriction. Attribution is appreciated but not required.

This sketch also includes the following sources: p5common.js . Sources are available under their respective licenses.

See all p5.js sketches