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.