createCanvas(600, 600, WEBGL)
fbo = new Framebuffer(window)
blurShader = createShader(vert, frag)
function mousePressed() {
for (let i = 0; i < 22; i++) {
while (Math.hypot(x, y) < 100) {
y = random(-1,1)*height/2
const diagram = voronoi.compute(sites, bbox)
const contours = diagram.cells.map((cell) => {
return cell.halfedges.map((he) => he.getStartpoint()).map((v) => createVector(v.x, v.y))
}).map((contour) => inset(contour, 8))
const trees = contours.map((contour) => {
const trunkLoc = normalize(contour.map(() => 1))
const addBranches = (source, depth) => {
const numChildren = depth === 0 ? 15 : random([1, 2, 2, 2, 3, 3, 3])
for (let i = 0; i < numChildren; i++) {
const direction = source.map(
(s) => 0.17*((s-0.5) + random(-1,1)*0.3)
const nextLoc = normalize(source.map((s,i) => s + direction[i]))
if (locDist(nextLoc, source) < 0.01) continue
from: getPoint(contour, source),
to: getPoint(contour, nextLoc),
addBranches(nextLoc, depth+1)
trunk: getPoint(contour, trunkLoc),
const eyeZ = (height/2) / tan(PI/6)
perspective(PI/3, width/height, near, far)
const blurIntensity = 0.01
for (const tree of trees) {
for (const { from, to, depth } of tree.branches) {
const angle = atan2(to.y-from.y, to.x-from.x)
translate((from.x+to.x)/2, (from.y+to.y)/2, 0)
cylinder((3 - 0.5*depth)*0.5, Math.hypot(to.x-from.x, to.y-from.y), 5, 5)
translate(tree.trunk.x, tree.trunk.y, h*0.4)
const thickness = bboxArea(tree.contour) * 0.0003
_renderer.getTexture(fbo.depth).setInterpolation(
blurShader.setUniform('uImg', fbo.color)
blurShader.setUniform('uDepth', fbo.depth)
blurShader.setUniform('uSize', [width, height])
blurShader.setUniform('uIntensity', blurIntensity)
blurShader.setUniform('uNumSamples', 25)
blurShader.setUniform('uTargetZ', targetDepth)
blurShader.setUniform('uNear', near)
blurShader.setUniform('uFar', far)
rect(0, 0, width, -height)
function bboxArea(contour) {
const minX = Math.min(...contour.map((v) => v.x))
const maxX = Math.max(...contour.map((v) => v.x))
const minY = Math.min(...contour.map((v) => v.y))
const maxY = Math.max(...contour.map((v) => v.y))
return (maxX - minX) * (maxY - minY)
function inset(contour, dist) {
const center = contour.reduce((acc, next) => acc.copy().add(next)).mult(1/contour.length)
return contour.map((c) => c.copy().add(center.copy().sub(c).normalize().mult(dist)))
function normalize(weights) {
const clamped = weights.map((w) => constrain(w, 0, 0.9))
const sum = max(0.01,clamped.reduce((acc, next) => acc+next))
return clamped.map((w) => w/sum)
function getPoint(contour, weights) {
.map((w,i) => contour[i].copy().mult(w))
.reduce((acc, next) => acc.add(next))
return Math.hypot(...a.map((v,i) => v-b[i]))