River
Draw a river, and watch the scenery come to life. Try writing something in cursive, or sign your name.
Note: Rendering can take a moment.
Source Code for this Sketch
class River {
constructor() {
this.points = []
this.done = false
this.timer = 0
}
update() {
if (mouseIsPressed && !this.done) {
this.points.push(createVector(mouseX, mouseY))
window.clearTimeout(this.timer)
}
if (this.points.length > 20) {
let that = this
this.timer = window.setTimeout(() => {
that.done = true
}, 1000)
}
}
draw() {
strokeWeight(1)
stroke(0)
for (let i = 0; i < this.points.length; i++) {
point(this.points[i].x, this.points[i].y)
}
}
progress(percent) {
let i = Math.floor(percent*(this.points.length-1))
let remainder = percent*(this.points.length-1)-i
let x =lerp(this.points[i].x, this.points[i+1].x, remainder)
let y =lerp(this.points[i].y, this.points[i+1].y, remainder)
return {x, y}
}
}
class Particle {
constructor() {
this.offsetX = random(-16, 16)
this.offsetY = random(-16, 16)
// this.color = color(218, 100, 50)
this.color = colorRange(210)
this.progress = random(0,1)
}
draw() {
let {x, y} = river.progress(this.progress)
strokeWeight(2)
stroke(this.color)
point(x+this.offsetX, y+this.offsetY)
this.progress+= .0004
if(this.progress >=1) {
this.progress = 0
}
}
}
function colorRange(hue, saturation, lightness) {
if (typeof saturation === 'undefined') {
saturation = 90
}
if (typeof lightness === 'undefined') {
lightness = 50
}
return color(randomGaussian(hue, 7), randomGaussian(saturation, 5), randomGaussian(lightness, 5))
}
function drawRiver(vec1, vec2, detail, dots) {
const hue = 210
const scatter = 16
strokeWeight(3)
let x = 0
let y = 0
let dx = 0
let dy = 0
// Number of steps between points in the pen stroke
for (let i = 0; i < detail; i++) {
x = lerp(vec1.x, vec2.x, i / detail)
y = lerp(vec1.y, vec2.y, i / detail)
// Random particles at each step
for (let j = 0; j < dots; j++) {
// TODO arrange these in a circle instead of a square so we get a
// consistent width as the river turns
dx = random(-scatter, scatter)
dy = random(-scatter, scatter)
stroke(colorRange(hue))
point(x + dx, y + dy)
}
}
}
function drawRiverbank(vec1, vec2) {
const riverbank = 40
noStroke()
fill(32, 47, 28)
for (let i = 0; i < 20; i++) {
x = lerp(vec1.x, vec2.x, i / 20)
y = lerp(vec1.y, vec2.y, i / 20)
// TODO make this a circle
rect(x-riverbank/2, y-riverbank/2, riverbank, riverbank)
}
}
function drawTent(x, y) {
let hue = random(0, 30)*12
fill(hue, 100, 50)
triangle(x-7, y, x+7, y, x, y-14)
fill(hue, 80, 30)
triangle(x-4, y, x+4, y, x, y-8)
}
function drawTree(x, y) {
if (treeType === 0) {
// pine
fill(32, 47, 28)
rect(x-2, y, 4,4)
fill(colorRange(90, 70, 30))
triangle(x-6, y, x+6, y, x, random(y-12, y-40))
} else if (treeType === 1) {
// poplar
fill(32, 47, 60)
rect(x-2, y-4, 4,8)
fill(colorRange(70, 70, 35))
ellipse(x, y-20, 10, 40)
} else {
// oak
fill(32, 47, 28)
rect(x-2, y-4, 4,8)
fill(colorRange(60, 70, 35))
arc(x, y, random(30, 40), random(40, 60), 180, 0, PIE)
}
}
let river = new River()
let doOnce = false
let treeField
let treeType = 0
let frames = 0
let particles = []
function setup() {
createCanvas(800, 400, P2D, canvas)
chrisDefaults()
treeType = Math.floor(random(0,3))
// debugging
// river.points = JSON.parse('[{"isPInst":true,"x":146,"y":-4.899993896484375,"z":0},{"isPInst":true,"x":146,"y":-4.899993896484375,"z":0},{"isPInst":true,"x":146,"y":-4.899993896484375,"z":0},{"isPInst":true,"x":151,"y":7.100006103515625,"z":0},{"isPInst":true,"x":155,"y":21.100006103515625,"z":0},{"isPInst":true,"x":161,"y":40.100006103515625,"z":0},{"isPInst":true,"x":163,"y":48.100006103515625,"z":0},{"isPInst":true,"x":166,"y":54.100006103515625,"z":0},{"isPInst":true,"x":170,"y":62.100006103515625,"z":0},{"isPInst":true,"x":174,"y":66.10000610351562,"z":0},{"isPInst":true,"x":186,"y":75.10000610351562,"z":0},{"isPInst":true,"x":196,"y":82.10000610351562,"z":0},{"isPInst":true,"x":215,"y":92.10000610351562,"z":0},{"isPInst":true,"x":222,"y":95.10000610351562,"z":0},{"isPInst":true,"x":240,"y":102.10000610351562,"z":0},{"isPInst":true,"x":264,"y":106.10000610351562,"z":0},{"isPInst":true,"x":279,"y":109.10000610351562,"z":0},{"isPInst":true,"x":294,"y":112.10000610351562,"z":0},{"isPInst":true,"x":310,"y":117.10000610351562,"z":0},{"isPInst":true,"x":343,"y":125.10000610351562,"z":0},{"isPInst":true,"x":351,"y":127.10000610351562,"z":0},{"isPInst":true,"x":374,"y":133.10000610351562,"z":0},{"isPInst":true,"x":391,"y":138.10000610351562,"z":0},{"isPInst":true,"x":421,"y":144.10000610351562,"z":0},{"isPInst":true,"x":444,"y":148.10000610351562,"z":0},{"isPInst":true,"x":465,"y":153.10000610351562,"z":0},{"isPInst":true,"x":494,"y":160.10000610351562,"z":0},{"isPInst":true,"x":513,"y":166.10000610351562,"z":0},{"isPInst":true,"x":532,"y":171.10000610351562,"z":0},{"isPInst":true,"x":557,"y":180.10000610351562,"z":0},{"isPInst":true,"x":572,"y":186.10000610351562,"z":0},{"isPInst":true,"x":594,"y":197.10000610351562,"z":0},{"isPInst":true,"x":607,"y":204.10000610351562,"z":0},{"isPInst":true,"x":618,"y":212.10000610351562,"z":0},{"isPInst":true,"x":626,"y":219.10000610351562,"z":0},{"isPInst":true,"x":635,"y":231.10000610351562,"z":0},{"isPInst":true,"x":642,"y":241.10000610351562,"z":0},{"isPInst":true,"x":645,"y":245.10000610351562,"z":0},{"isPInst":true,"x":650,"y":257.1000061035156,"z":0},{"isPInst":true,"x":654,"y":267.1000061035156,"z":0},{"isPInst":true,"x":660,"y":281.1000061035156,"z":0},{"isPInst":true,"x":663,"y":292.1000061035156,"z":0},{"isPInst":true,"x":667,"y":302.1000061035156,"z":0},{"isPInst":true,"x":676,"y":321.1000061035156,"z":0},{"isPInst":true,"x":685,"y":337.1000061035156,"z":0},{"isPInst":true,"x":692,"y":346.1000061035156,"z":0},{"isPInst":true,"x":698,"y":353.1000061035156,"z":0},{"isPInst":true,"x":713,"y":368.1000061035156,"z":0},{"isPInst":true,"x":717,"y":371.1000061035156,"z":0},{"isPInst":true,"x":731,"y":381.1000061035156,"z":0},{"isPInst":true,"x":743,"y":390.1000061035156,"z":0},{"isPInst":true,"x":762,"y":403.1000061035156,"z":0},{"isPInst":true,"x":787,"y":420.1000061035156,"z":0},{"isPInst":true,"x":805,"y":431.1000061035156,"z":0},{"isPInst":true,"x":833,"y":445.1000061035156,"z":0},{"isPInst":true,"x":850,"y":454.1000061035156,"z":0},{"isPInst":true,"x":875,"y":464.1000061035156,"z":0},{"isPInst":true,"x":892,"y":472.1000061035156,"z":0},{"isPInst":true,"x":912,"y":482.1000061035156,"z":0},{"isPInst":true,"x":932,"y":494.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0},{"isPInst":true,"x":955,"y":513.1000061035156,"z":0}]')
// river.done = true
for(let i = 0; i < 100; i++) {
particles.push(new Particle())
}
treeField = new FlowField(10, 10, 10, (x, y) => {
let treeCount = Math.round(noise(x, y)*10)-(5+treeType)
treeCount = max(0, treeCount)
return treeCount
}, 0)
treeField.resizeToCanvas()
}
function draw() {
frames++
if(frames>60) {
frames = 0
}
if (river.done) {
if (!doOnce) {
doOnce = true
// Ground texture
noStroke()
if(treeType === 0) {
fill(85, 70, 50)
} else if (treeType === 1) {
fill(75, 80, 60)
} else {
fill(70, 70, 60)
}
rect(0, 0, width, height)
// Grass
for (let i = 0; i < 10000; i++) {
strokeWeight(random(.5, 3))
if (treeType === 1) {
stroke(colorRange(90, 70, 50))
} else if (treeType === 2) {
stroke(colorRange(70, 70, 50))
} else {
stroke(colorRange(80, 70, 50))
}
point(random(0, width), random(0, height))
}
// Riverbank (mud)
for (let i = 0; i < river.points.length - 2; i++) {
drawRiverbank(river.points[i], river.points[i + 1])
}
// River
for (let i = 0; i < river.points.length - 2; i++) {
drawRiver(river.points[i], river.points[i + 1], 20, 40)
}
// Remove trees from the river
treeField.visit((x, y, trees) => {
for (let i = 0; i < river.points.length; i++) {
if (river.points[i].x-x*treeField.scale < 30 &&
x*treeField.scale-river.points[i].x < 30 &&
river.points[i].y-y*treeField.scale < 30 &&
// Trees grow up, so push the dead zone below the river
y*treeField.scale-river.points[i].y < 60) {
return -1
}
}
})
// Draw trees
treeField.visit((x, y, trees) => {
noStroke()
if(trees === 0) {
if(Math.floor(random(0, 2000)) === 500) {
drawTent(x*treeField.scale, y*treeField.scale)
}
}
for (let i = 0; i < trees; i++) {
let dx = random(-treeField.scale/2, treeField.scale/2)
let dy = random(-2, 2)
drawTree(x * treeField.scale +dx, y * treeField.scale + dy, trees)
}
})
}
// River sparkles
if(frames % 5 === 0) {
for (let i = 0; i < river.points.length - 2; i++) {
drawRiver(river.points[i], river.points[i + 1], 3, 1)
}
}
// River flow
for(let i = 0; i< particles.length; i++) {
particles[i].draw()
}
} else {
background(100)
noStroke()
fill(50)
textSize(30)
text("Draw a line with your", width/4, height/2-15)
text("mouse or finger", width/4, height/2+15)
river.update()
river.draw()
}
}
function changeTrees() {
treeType++
if (treeType > 2) {
treeType = 0
}
treeField.visit((x, y, trees) => {
return treeField.init(x, y)
})
doOnce = false
}
function reset() {
river.points = []
treeType = Math.floor(random(0,3))
treeField.visit((x, y, trees) => {
return treeField.init(x, y)
})
river.done = false
doOnce = false
}
document.addEventListener("DOMContentLoaded", (e) => {
let p5ui = document.getElementById("p5ui")
p5ui.innerHTML = `
<div>
<button id="treesButton">Change Trees</button>
<button id="resetButton">Reset Canvas</button>
</div>
`
let treesButton = document.getElementById("treesButton")
treesButton.addEventListener("click", (e) => {
changeTrees()
})
let resetButton = document.getElementById("resetButton")
resetButton.addEventListener("click", (e) => {
reset()
})
})
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.