AI Zen Garden - Rake Peaceful Patterns in Sand

This sketch turns your browser into a raked sand garden: dragging the mouse carves smooth, ridged grooves into a noise-textured sand background, clicking drops a stone, and pressing R smooths everything back to a fresh, randomly-stoned garden.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the sand color — background() sets the base tone of the whole garden - swap the warm tan for something cooler or darker to change the entire mood.
  2. Recolor the stones — fill(70) sets one flat gray color for every stone - change it to give the garden colorful boulders instead.
  3. Make the rake grooves glow white — The light-edge stroke color used in every rake stroke controls how bright the grooves look against the sand.
  4. Widen the rake grooves — The off offset spacing controls how far apart the five parallel rake lines are drawn - a bigger multiplier makes each stroke of the rake much wider.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch simulates raking a Japanese zen garden entirely in the browser: a warm, noise-textured sand bed fills the canvas, dark stones sit scattered across it, and dragging the mouse carves realistic-looking grooves complete with a lit edge and a shadowed edge. It relies on Perlin noise for the grainy sand texture, simple 2D vector math (a perpendicular vector) to draw parallel rake lines, and an array of plain JavaScript objects to track the stones so they can be redrawn on top of the sand.

The code is organized around a handful of tiny, single-purpose functions: makeSand() paints the textured background, initStones() and drawStones() manage the stone objects, and mouseDragged() does the real work of turning mouse movement into rake grooves. Studying it teaches you how to build an entirely event-driven sketch (no animation loop needed), how to rotate a direction vector 90 degrees to draw perpendicular lines, and how layering alpha-blended strokes can fake a 3D-looking groove with flat 2D shapes.

⚙️ How It Works

  1. When the page loads, setup() creates a full-window canvas, configures how Perlin noise and rounded line caps behave, then calls makeSand() to paint a textured sand background and initStones() to scatter 3-4 stones on top.
  2. draw() is left completely empty - this sketch never animates on its own. Instead, all the visuals are drawn once and only redrawn in response to something the user does.
  3. As you drag the mouse, mouseDragged() fires repeatedly. It measures how far and in what direction the mouse just moved, rotates that direction 90 degrees to get a perpendicular vector, and uses that perpendicular to offset five parallel lines - alternating a light stroke and a dark stroke - which together look like a raked groove with a highlighted edge and a shadowed edge.
  4. Clicking anywhere calls mousePressed(), which pushes a new stone object (with a random size) into the stones array and redraws every stone so the new one appears immediately.
  5. Pressing the R key triggers keyPressed(), which calls makeSand() and initStones() again, wiping out every groove and giving you a smooth, freshly-stoned garden to start over.
  6. If the browser window changes size, windowResized() resizes the canvas and rebuilds the sand and stones from scratch so nothing looks stretched or misplaced.

🎓 Concepts You'll Learn

Event-driven drawing (no animation loop needed)Perlin noise for organic texturePerpendicular vector math for offset linesArrays of objects as simple stateAlpha-blended strokes to fake depthResponsive canvas with windowResized

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. Because this whole sketch is event-driven (nothing needs to animate every frame), setup() is where almost all of the important configuration and the first drawing happens.

function setup(){createCanvas(windowWidth,windowHeight);noiseDetail(2,0.6);strokeCap(ROUND);makeSand();initStones();}
Line-by-line explanation (5 lines)
createCanvas(windowWidth,windowHeight);
Creates a canvas that fills the entire browser window.
noiseDetail(2,0.6);
Configures Perlin noise to use 2 layers ('octaves') of detail with a 0.6 falloff, which controls how smooth vs. bumpy the noise texture looks.
strokeCap(ROUND);
Makes the ends of every line drawn afterward rounded instead of squared off - important for the rake grooves to look soft rather than blocky.
makeSand();
Calls the function that paints the textured sand background.
initStones();
Calls the function that randomly places the starting stones on top of the sand.

draw()

p5.js still calls draw() up to 60 times per second even if it's empty, unless you call noLoop(). This sketch could call noLoop() in setup() to avoid that wasted work entirely, since it never needs continuous animation.

function draw(){}
Line-by-line explanation (1 lines)
function draw(){}
This function is completely empty. Normally draw() runs continuously to animate a sketch, but here every visual change (raking, placing stones, resetting) is handled directly inside the event functions like mouseDragged() and mousePressed(), so there's nothing left for draw() to do every frame.

makeSand()

makeSand() is the texture generator for the whole garden. Using noise() instead of pure random() for the color means neighboring grains share similar shading, which is what makes the texture look like natural sand instead of TV static.

🔬 This loop draws 40,000 sand grains, each colored by a noise value. What happens visually if you drop the loop count to 4000? What if you multiply n's influence, like using n*100 instead of n*30, on the red channel?

for(let i=0;i<40000;i++){let x=random(width),y=random(height),n=noise(x*0.02,y*0.02);stroke(220+n*30,210+n*25,180+n*20,80);point(x,y);}
function makeSand(){background(230,220,190);strokeWeight(2);for(let i=0;i<40000;i++){let x=random(width),y=random(height),n=noise(x*0.02,y*0.02);stroke(220+n*30,210+n*25,180+n*20,80);point(x,y);}noStroke();}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Sand Grain Loop for(let i=0;i<40000;i++){let x=random(width),y=random(height),n=noise(x*0.02,y*0.02);stroke(220+n*30,210+n*25,180+n*20,80);point(x,y);}

Scatters 40,000 individual colored points across the canvas, each tinted by a Perlin noise value so nearby grains blend into soft clumps instead of pure random static.

background(230,220,190);
Fills the whole canvas with a warm, tan sand color as the base layer.
strokeWeight(2);
Sets the thickness of the upcoming sand grain points to 2 pixels.
let x=random(width),y=random(height),n=noise(x*0.02,y*0.02);
Picks a random spot on the canvas, then samples Perlin noise at that spot (scaled down by 0.02) to get a smooth value n roughly between 0 and 1.
stroke(220+n*30,210+n*25,180+n*20,80);
Colors the grain using the noise value n, so grains that are close together shift color together, creating gentle clumps of lighter and darker sand rather than uniform speckle. The 80 alpha keeps each grain subtle.
point(x,y);
Draws a single sand grain at that random position.
noStroke();
Turns stroke off afterward so later shapes (like stones) don't get an unwanted outline.

initStones()

initStones() shows a common pattern in creative coding: clear an array, loop to fill it with randomly-generated objects, then draw the whole collection. The same three-step pattern (clear, generate, draw) reappears any time you manage a group of similar items.

function initStones(){stones=[];for(let i=0;i<int(random(3,5));i++)stones.push({x:random(60,width-60),y:random(60,height-60),r:random(18,28)});drawStones();}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Random Stone Spawner for(let i=0;i<int(random(3,5));i++)stones.push({x:random(60,width-60),y:random(60,height-60),r:random(18,28)});

Creates a random number of stone objects (3 or 4) with random positions and sizes, and adds each one to the stones array.

stones=[];
Empties the stones array so any old stones from a previous garden are removed.
for(let i=0;i<int(random(3,5));i++)stones.push({x:random(60,width-60),y:random(60,height-60),r:random(18,28)});
Loops a random number of times (int(random(3,5)) gives either 3 or 4) and, each time, pushes a new stone object with a random x, y position (kept 60 pixels from the edges) and a random radius r between 18 and 28 into the stones array.
drawStones();
Immediately draws all the newly created stones onto the canvas.

drawStones()

drawStones() is called every time the stones array changes (after a click, after a rake stroke, after a reset) because raking or resetting the sand paints over the stones - redrawing them keeps them visible on top.

🔬 Each stone uses its own random radius s.r. What happens if you replace s.r with a fixed number like 40 so every stone becomes the same size?

for(let s of stones)circle(s.x,s.y,s.r);
function drawStones(){noStroke();fill(70);for(let s of stones)circle(s.x,s.y,s.r);}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Stone Rendering Loop for(let s of stones)circle(s.x,s.y,s.r);

Iterates over every stone object currently stored in the stones array and draws it as a filled circle.

noStroke();
Turns off outlines so the stones are solid, flat circles.
fill(70);
Sets the fill color to a dark gray for every stone drawn after this line.
for(let s of stones)circle(s.x,s.y,s.r);
Uses a for-of loop to go through each stone object 's' in the stones array and draw a circle at its x,y position with diameter s.r.

mouseDragged()

mouseDragged() is the heart of this sketch. The perpendicular vector trick (swap x/y, negate one) is a fundamental piece of 2D vector math used constantly in creative coding for things like offsetting strokes, building ribbons, or orienting objects relative to motion.

🔬 This loop draws 5 parallel groove lines using i from -2 to 2. What happens if you widen the range to -5 through 5 for a much wider rake, or shrink the off multiplier from 5 to 2 for tightly packed grooves?

for(let i=-2;i<=2;i++){let off=i*5;stroke(180,160,130,140);line(pmouseX+nx*off,pmouseY+ny*off,mouseX+nx*off,mouseY+ny*off);stroke(120,100,80,70);line(pmouseX+nx*(off+1),pmouseY+ny*(off+1),mouseX+nx*(off+1),mouseY+ny*(off+1));}
function mouseDragged(){let dx=mouseX-pmouseX,dy=mouseY-pmouseY;let d=sqrt(dx*dx+dy*dy);if(d<1)return;let nx=-dy/d,ny=dx/d;strokeWeight(2);for(let i=-2;i<=2;i++){let off=i*5;stroke(180,160,130,140);line(pmouseX+nx*off,pmouseY+ny*off,mouseX+nx*off,mouseY+ny*off);stroke(120,100,80,70);line(pmouseX+nx*(off+1),pmouseY+ny*(off+1),mouseX+nx*(off+1),mouseY+ny*(off+1));}drawStones();}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Parallel Rake Line Loop for(let i=-2;i<=2;i++){let off=i*5;stroke(180,160,130,140);line(pmouseX+nx*off,pmouseY+ny*off,mouseX+nx*off,mouseY+ny*off);stroke(120,100,80,70);line(pmouseX+nx*(off+1),pmouseY+ny*(off+1),mouseX+nx*(off+1),mouseY+ny*(off+1));}

Draws five parallel lines spaced along the perpendicular direction, each paired with a light line and a darker offset line, to simulate a raked groove with highlight and shadow edges.

let dx=mouseX-pmouseX,dy=mouseY-pmouseY;
Calculates how far the mouse moved since the last frame, in both x and y directions.
let d=sqrt(dx*dx+dy*dy);
Uses the Pythagorean theorem to find the straight-line distance the mouse moved (the length of the dx,dy vector).
if(d<1)return;
If the mouse barely moved, exit early - this avoids dividing by a near-zero number in the next step and skips drawing pointless tiny strokes.
let nx=-dy/d,ny=dx/d;
Builds a unit-length vector perpendicular to the drag direction by swapping dx/dy and flipping one sign - this is the classic '90 degree rotation' trick for 2D vectors.
strokeWeight(2);
Sets the line thickness for the rake strokes.
for(let i=-2;i<=2;i++){let off=i*5;
Loops 5 times (i from -2 to 2) and multiplies each i by 5 to get an offset distance, spreading 5 parallel lines evenly across the rake width.
stroke(180,160,130,140);line(pmouseX+nx*off,pmouseY+ny*off,mouseX+nx*off,mouseY+ny*off);
Draws a lighter tan line from the previous mouse position to the current one, shifted sideways by 'off' along the perpendicular vector - this is one groove line.
stroke(120,100,80,70);line(pmouseX+nx*(off+1),pmouseY+ny*(off+1),mouseX+nx*(off+1),mouseY+ny*(off+1));
Draws a darker, slightly more offset line right next to the light one - together they create the illusion of a shadowed groove edge.
drawStones();
Redraws every stone on top, since the new rake lines may have just been painted over them.

mousePressed()

mousePressed() runs automatically once whenever a mouse button is clicked. Combined with an array, it's a simple way to let users build up a scene one click at a time.

function mousePressed(){stones.push({x:mouseX,y:mouseY,r:random(18,28)});drawStones();}
Line-by-line explanation (2 lines)
stones.push({x:mouseX,y:mouseY,r:random(18,28)});
Creates a new stone object at the exact position the mouse was clicked, with a random radius between 18 and 28, and adds it to the stones array.
drawStones();
Redraws all stones so the newly added one becomes visible right away.

keyPressed()

keyPressed() runs once every time any key is pressed. Checking the 'key' variable against specific characters is the standard way to build keyboard shortcuts in p5.js.

function keyPressed(){if(key==='r'||key==='R'){makeSand();initStones();}}
Line-by-line explanation (1 lines)

🔧 Subcomponents:

conditional Reset Key Check if(key==='r'||key==='R'){makeSand();initStones();}

Checks whether the pressed key was lowercase or uppercase R, and if so, repaints the sand and re-scatters the stones.

if(key==='r'||key==='R'){makeSand();initStones();}
p5.js automatically fills the 'key' variable with the character just pressed. This checks for either lowercase or uppercase R, and if matched, calls makeSand() to smooth the sand back to its original texture and initStones() to place a brand new random set of stones.

windowResized()

windowResized() is a built-in p5.js callback that fires automatically whenever the browser window changes size, letting you rebuild a responsive sketch instead of leaving it stretched or cropped.

function windowResized(){resizeCanvas(windowWidth,windowHeight);makeSand();initStones();}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth,windowHeight);
Resizes the canvas to match the browser window's new width and height.
makeSand();
Repaints the sand texture so it fully covers the new canvas size.
initStones();
Re-scatters a fresh set of stones, since the old stone positions might no longer make sense on the resized canvas.

📦 Key Variables

stones array

Stores every stone as a simple object with x, y, and r (radius) properties. Used by drawStones(), initStones(), and mousePressed() to track and render all the stones currently in the garden.

let stones=[];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw()

draw() is empty but p5.js still calls it up to 60 times per second by default, wasting CPU cycles for a sketch that never needs continuous animation.

💡 Call noLoop() at the end of setup() to stop the automatic draw loop entirely, since every visual update already happens inside mouseDragged(), mousePressed(), keyPressed(), and windowResized().

BUG initStones()

random(60,width-60) and random(60,height-60) will throw unexpected results or errors if the window is narrower or shorter than 120 pixels, since the min would exceed the max.

💡 Clamp the range with something like random(60, max(61, width-60)) or use a percentage-based margin so very small windows don't break stone placement.

STYLE entire sketch.js

The whole file is written as single-line, heavily minified function bodies (no line breaks, terse variable names like dx/dy/nx/ny), which makes it hard for beginners to read, debug, or extend.

💡 Reformat each function across multiple lines with blank space and slightly more descriptive names (e.g. perpX/perpY instead of nx/ny) purely for readability - it won't change behavior but will make the code much easier to learn from.

FEATURE sketch.js overall

There's no on-screen hint that dragging rakes sand, clicking places stones, or that R resets the garden, so first-time visitors may not discover the interactions.

💡 Draw a small, semi-transparent instructions overlay in a corner (e.g. using text()) that fades out after a few seconds or after the first successful drag.

🔄 Code Flow

Code flow showing setup, draw, makesand, initstones, drawstones, mousedragged, mousepressed, keypressed, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> makesand[makesand] makesand --> sand-grain-loop[sand-grain-loop] sand-grain-loop --> drawstones[drawstones] draw --> mousedragged[mousedragged] mousedragged --> rake-line-loop[rake-line-loop] rake-line-loop --> drawstones draw --> mousepressed[mousepressed] mousepressed --> initstones[initstones] initstones --> stone-spawn-loop[stone-spawn-loop] stone-spawn-loop --> drawstones draw --> keypressed[keypressed] keypressed --> reset-key-check[reset-key-check] reset-key-check -->|if 'r' pressed| makesand draw --> windowresized[windowresized] click setup href "#fn-setup" click draw href "#fn-draw" click makesand href "#fn-makesand" click initstones href "#fn-initstones" click drawstones href "#fn-drawstones" click mousedragged href "#fn-mousedragged" click mousepressed href "#fn-mousepressed" click keypressed href "#fn-keypressed" click windowresized href "#fn-windowresized" click sand-grain-loop href "#sub-sand-grain-loop" click stone-spawn-loop href "#sub-stone-spawn-loop" click stone-render-loop href "#sub-stone-render-loop" click rake-line-loop href "#sub-rake-line-loop" click reset-key-check href "#sub-reset-key-check"

❓ Frequently Asked Questions

What visual experience does the AI Zen Garden sketch provide?

The AI Zen Garden sketch creates a serene digital landscape where users can rake patterns in a sandy background, simulating the tranquility of a traditional Japanese zen garden.

How can users interact with the AI Zen Garden sketch?

Users can drag their mouse to create grooves in the sand, click to place stones, and press 'R' to reset the garden and start anew.

What creative coding techniques are showcased in this p5.js sketch?

This sketch demonstrates techniques such as noise generation for natural patterns, mouse interaction for user engagement, and dynamic object placement to enhance the visual experience.

Preview

AI Zen Garden - Rake Peaceful Patterns in Sand - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Zen Garden - Rake Peaceful Patterns in Sand - Code flow showing setup, draw, makesand, initstones, drawstones, mousedragged, mousepressed, keypressed, windowresized
Code Flow Diagram