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.

See all p5.js sketches