Painted Waterfall
Paint on the waterfall to change the colors
Source Code for this Sketch
let colorField
let particles = []
let particleCount = 1000
let particleSize = 35
let colorTool = true
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() {
// initialize in a random position on screen
this.v = createVector(0, 0);
this.random()
this.color = color(90)
}
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
this.v.y += vec.y
// 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() {
fill(this.color)
circle(this.v.x, this.v.y, particleSize)
}
}
function initializeParticles() {
// Particle initialization
for (let i = 0; i < particleCount; i++) {
if (particles[i]) {
continue
}
particles[i] = new Particle()
}
particles = particles.slice(0, particleCount)
}
function initializeFlowField(x, y) {
return createVector(0, .4)
}
function initializeColorField(x, y) {
return createVector(0, 0)
}
function setup() {
createCanvas(800, 600, P2D, canvas)
chrisDefaults()
colorField = new FlowField(10, 10, 20, initializeColorField, createVector(0, 0))
colorField.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
}
if(!colorTool) {
vector.dx = 0
vector.dy = 0
}
return vector
}
function visitGrid(x, y, radius, func) {
radius /= colorField.scale
x/=colorField.scale
y/=colorField.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), colorField.width)
let endJ = min(floor(y+radius+1), colorField.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/colorField.scale
yTest = y/colorField.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*colorField.scale, j*colorField.scale, colorField.scale, colorField.scale)
colorField.field[i][j] = func(colorField.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/colorField.scale), 4)
let dfactor = 1-factor
return createVector(x*factor+dx*dfactor, y*factor+dy*dfactor)
}
function applyFlow(vector) {
// Don't let the color tool apply grey. This is a janky way to do it
// but we're not going to reuse this elsewhere (probably)
if (vector.isZero() && colorTool) {
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.
if(vector.isZero()) {
return createVector(0, 0)
}
let factor = 10
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 showEraseTool(x, y, radius) {
const offset = radius*.7+10
strokeWeight(1)
stroke(0)
fill(90)
circle(x+offset, y+offset, 20)
}
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)
noStroke()
// Draw flow colorField
let c
for (let i = 0; i < particles.length; i++) {
particles[i].flow(createVector(0, .4))
c = colorField.at(particles[i].x, particles[i].y)
particles[i].color = color(c.heading()+180, c.mag() < .00001 ? 0 : 100, 90-(c.mag() * 40))
particles[i].draw()
}
showToolRadius(mouseX, mouseY, toolRadius, 220)
if(colorTool) {
showParticleTool(mouseX, mouseY, toolRadius)
} else {
showEraseTool(mouseX, mouseY, toolRadius)
}
if (inRect(0, 0, width, height)) {
applyFlow(getMouseFlow())
}
}
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. Painting direction determines the color.</td></tr>
<tr>
<td>
<label for="particlesInput">Particles</label>
<input id="particlesInput" type="range"
min="1000" max="3000" step="10" value="1000">
<span id="particlesValue" class="mono">1000</span>
</td>
<td>Change the number of particles</td>
</tr>
<tr>
<td>
<button id="resetColors">Reset Colors</button>
<span style="width: 5em; display: inline-block;"></span>
<button id="colorTool">
<span style="color:red;">C</span><span style="color:purple;">o</span><span style="color:blue;">l</span><span style="color:green;">o</span><span style="color:orange;">r</span> tool
</button>
<button id="eraserTool">Eraser</button>
</td>
</tr>
</table>
`
let particlesInput = document.getElementById("particlesInput")
let particlesValue = document.getElementById("particlesValue")
let resetColorsButton = document.getElementById("resetColors")
let colorToolButton = document.getElementById("colorTool")
let eraserToolButton = document.getElementById("eraserTool")
particlesInput.addEventListener("input", (e) => {
particleCount = Math.round(Number(e.target.value))
particleSize = 5 + 10*3000/particleCount
particlesValue.innerText = String(particleCount)
initializeParticles()
})
resetColorsButton.addEventListener("click", (e) => {
colorField.visit(initializeColorField)
})
colorToolButton.addEventListener("click", (e) => {
colorTool = true
})
eraserToolButton.addEventListener("click", (e) => {
colorTool = false
})
})
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.