Create your own flow field

Create your own flow field by painting flow vectors. If you're not sure what a flow field is, take a look at an example

Source Code for this Sketch

let field
let particles = []
let particleCount = 300
let particleSize = 10
let speed = 1
let activeTool = null
let toolRadius = 50
let lastX = 0
let lastY = 0

let dragStarted = false
let dragFrames = 0

class ToolVector {
    static zero() {
        return new ToolVector(0, 0, 0, 0)
    }

    constructor(x, y, dx, dy) {
        this.x = x
        this.y = y
        this.dx = dx
        this.dy = dy
    }

    get position() {
        return createVector(this.x, this.y)
    }

    get delta() {
        return createVector(this.dx, this.dy)
    }

    isZero() {
        return this.dx === 0 && this.dy === 0
    }
}

class Particle {
    constructor(c) {
        // initialize in a random position on screen
        this.v = createVector(0, 0);
        this.random()
        this.color = c
    }

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

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

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

    flow(vec) {
        this.v.x += vec.x*speed
        this.v.y += vec.y*speed

        // wrap around, but random
        if(this.x > width) {
            this.v.x = 0
            this.v.y = random(0, height)
        }
        if(this.y > height) {
            this.v.y = 0
            this.v.x = random(0, width)
        }
        if(this.x < 0) {
            this.v.x = width
            this.v.y = random(0, height)
        }
        if(this.y < 0) {
            this.v.y = height
            this.v.x = random(0, width)
        }
    }

    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 initializeFlowField(x, y) {
    return createVector(0, .4)
}

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

    field = new FlowField(10, 10, 20, initializeFlowField, createVector(0, 0, 0))
    field.resizeToCanvas()
    initializeParticles()
}

function getMouseFlow() {
    // this is jumpy.
    // TODO We should probably do some kind of smoothing by sampling
    //  lastX and lastY as an array or rolling window and averaging them to
    //  reduce jumpiness. For example, continuously collect the last 5
    //  measurements and then average when returning
    const xyTreshold = 3
    const frameTreshold = 3

    if(!mouseIsPressed) {
        dragStarted = false
        dragFrames = 0
    }

    // At beginning of drag track the origin position of the mouse
    if(mouseIsPressed && !dragStarted) {
        dragStarted = true
        lastX = mouseX
        lastY = mouseY
    }

    // If the mouse button is held, increment drag frames
    if(mouseIsPressed) {
        dragFrames++
    }

    let vector = ToolVector.zero()

    if (dragFrames > 0 && (mouseX-lastX > xyTreshold || mouseY-lastY > xyTreshold || dragFrames > frameTreshold)) {
        vector = new ToolVector(lastX, lastY, mouseX-lastX, mouseY-lastY)
        lastX = mouseX
        lastY = mouseY
    }

    return vector
}

function visitGrid(x, y, radius, func) {
    radius /= field.scale
    x/=field.scale
    y/=field.scale

    let distance = Number.MAX_SAFE_INTEGER
    let xTest = 0
    let yTest = 0

    // Looking at the whole grid is expensive, so only look at the part
    // near where we clicked.
    let startI = max(floor(x-radius-1), 0)
    let startJ = max(floor(y-radius-1), 0)
    let endI = min(floor(x+radius+1), field.width)
    let endJ = min(floor(y+radius+1), field.height)

    for (let i = startI; i < endI; i++) {
        for (let j = startJ; j < endJ; j++) {
            // We still need to check whether we're in the radius of
            // the tool because the corners won't be, but whether we
            // include a block on the edge or not depends on the tool
            // radius.
            xTest = x/field.scale
            yTest = y/field.scale

            if (x < i) {
                xTest = i
            } else if (x > i) {
                xTest = i+1
            }

            if (y < j) {
                yTest = j
            } else if (y > j) {
                yTest = j+1
            }

            distance = sqrt(pow(x - xTest, 2) + pow(y - yTest, 2))
            if(distance <= radius) {
                // debug view to see brush influence
                // rect(i*field.scale, j*field.scale, field.scale, field.scale)
                field.field[i][j] = func(field.field[i][j], distance)
            }
        }
    }
}

function weightedVector(x, y, dx, dy, distance) {
    // Tune the exponent to increase the "strength" of the brush
    let factor = 1 - pow(distance/(toolRadius/field.scale), 4)
    let dfactor = 1-factor
    return createVector(x*factor+dx*dfactor, y*factor+dy*dfactor)
}

function applyFlow(vector) {
    if(vector.isZero()) {
        return
    }

    visitGrid(vector.x, vector.y, toolRadius, (gridVector, distance) => {
        // factor determines the amount of speed we add to the vector based
        // on the delta (in pixels). Speed should always be 0-1, and we'll take
        // a delta of 10 pixels to be the highest allowed speed, but we'll use
        // 10 as a baseline to allow smaller increments.
        let factor = 10

        vector.delta.x
        vector.delta.y
        let m = max(abs(vector.delta.x), abs(vector.delta.y))
        if(m > factor) {
            factor = m
        }

        return weightedVector(gridVector.x, gridVector.y, vector.delta.x/factor, vector.delta.y/factor, distance)
    })
}

function showParticleTool(x, y, radius) {
    const offset = radius*.7+20

    noStroke()
    fill(0, 100, 50)
    circle(x+offset-5, y+offset-15, 10)
    fill(100, 100, 50)
    circle(x+offset+1, y+offset-5, 10)
    fill(200, 100, 50)
    circle(x+offset-11, y+offset-5, 10)
}

function showVertexTool(x, y, radius) {
    const offset = radius*.7+20
    noFill()
    strokeWeight(1)
    stroke(0)
    bezier(x+offset-30, y+offset,
        x+offset+30, y+offset-30,
        x+offset-10, y+offset+30,
        x+offset, y+offset-30)
    triangle(x+offset, y+offset-30,
        x+offset+3, y+offset-30+6,
        x+offset-5, y+offset-30+5,)
}

function showToolRadius(x, y, radius, hue, func) {
    strokeWeight(1)
    stroke(hue, 100, 50, .5)
    fill(hue, 100, 50, .2)
    circle(x, y, radius*2)
    if (func) {
        func()
    }
}

function inRect(x, y, w, h) {
    return (mouseX >= x && mouseX < x+w && mouseY >= y && mouseY < y+h)
}

function draw() {
    background(100)

    push()

    // Draw flow field
    strokeWeight(.5)
    stroke(0)
    noFill()
    field.draw()

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

    pop()

    showToolRadius(mouseX, mouseY, toolRadius, 220)
    // showParticleTool(mouseX, mouseY, toolRadius)
    showVertexTool(mouseX, mouseY, toolRadius)

    let flow = getMouseFlow()
    if (!flow.isZero() && inRect(0, 0, width, height)) {
        applyFlow(flow)
    }
}

canvas.addEventListener("wheel", (e) => {
    if(e.wheelDelta < 0) {
        toolRadius -= 10
    } else {
        toolRadius += 10
    }
    if(toolRadius < 10) {
        toolRadius = 10
    }
    if (toolRadius > 100) {
        toolRadius = 100
    }
})

document.addEventListener("DOMContentLoaded", () => {
    let p5ui = document.getElementById("p5ui")
    p5ui.innerHTML = `
<table>
    <tr><td colspan="2">Use the mouse wheel to increase or decrease the size of the brush.<br>
    Click and drag to paint a new direction.</td></tr>
    <tr>
        <td>
            <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>
        </td>
        <td>Smaller grid sizes change velocity more frequently</td>
    </tr>
    <tr>
        <td>
            <label for="particlesInput">Particles</label>
            <input id="particlesInput" type="range"
                   min="10" max="3000" step="10" value="300">
            <span id="particlesValue" class="mono">300</span>
        </td>
        <td>Change the number of particles</td>
    </tr>
    <tr>
        <td>
            <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>
        </td>
        <td>Speed up or slow down particle movement</td>
    </tr>
    <tr>
        <td>
            <button id="resetFlow">Reset Flow</button>
            <button id="randomizeParticles">Randomize Particles</button>
        </td>
    </tr>
</table>
	`
    let gridSizeInput = document.getElementById("gridSizeInput")
    let gridSizeValue = document.getElementById("gridSizeValue")

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

    let particlesInput = document.getElementById("particlesInput")
    let particlesValue = document.getElementById("particlesValue")

    let resetFlowButton = document.getElementById("resetFlow")
    let randomizeParticlesButton = document.getElementById("randomizeParticles")

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

    speedInput.addEventListener("input", (e) => {
        speedValue.innerText = Number(e.target.value).toFixed(1)
        speed = e.target.value
    })

    particlesInput.addEventListener("input", (e) => {
        particleCount = Number(e.target.value)
        particlesValue.innerText = String(particleCount)
        initializeParticles()
    })

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

    randomizeParticlesButton.addEventListener("click", (e) => {
        particles.forEach(particle => {
            particle.random()
        })
    })
})

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