AI Daydreaming Fish - Dreams of the Ocean

This sketch draws a little orange fish swimming back and forth inside a circular bowl, releasing tiny bubbles that drift upward and disappear. Above the bowl, a cloud-shaped thought bubble cycles through three dreams - 'The Ocean', 'Freedom', and 'A Bigger Bowl' - giving the fish a wistful, storytelling quality.

🧪 Try This!

Experiment with the code by making these changes:

  1. Give the fish a bigger bowl — Increasing bowlR makes the glass circle (and the fish's swimming range) much larger.
  2. Speed up the swim — Raising the sine wave's frequency multiplier makes the fish dart back and forth much faster.
  3. Turn the fish blue — Changing the fill() color before the body is drawn instantly recolors the fish.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch brings a tiny cartoon fish to life inside a glass bowl, using sine-wave motion to make it glide smoothly side to side, an array of particle objects to simulate rising bubbles, and simple text rendering inside an overlapping-ellipse 'cloud' to show the fish's changing thoughts. It's a great example of how a handful of p5.js primitives - circle(), ellipse(), triangle(), and text() - can be combined with math and timing tricks to create something that feels alive and even a little emotional.

Everything lives inside a single draw() loop that runs once per frame: it repaints the background, repositions the fish using trigonometry, spawns and updates bubble particles stored in an array, and picks which dream sentence to display based on how many frames have elapsed. By studying this code you'll learn how sin() creates back-and-forth motion, how push()/splice() manage a growing and shrinking list of particles, and how frameCount can be used as a simple timer to cycle through content.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and configures text to be centered wherever it's drawn.
  2. Every frame, draw() paints a light blue background and then draws a white circular outline to represent the fish bowl.
  3. The fish's horizontal position is calculated with sin(frameCount * 0.01), which smoothly oscillates it left and right forever without needing any stored velocity variable.
  4. On roughly 2% of frames, a new bubble object is pushed into the bubbles array near the fish's mouth; every existing bubble then moves upward and is removed once it floats above the bowl.
  5. A thought-bubble cloud made of overlapping ellipses is drawn above the bowl, and the current dream string is chosen by dividing frameCount by 180 and using modulo to loop through the dreams array, so the text changes every 3 seconds at 60fps.
  6. This entire sequence repeats every frame, creating the illusion of a fish swimming, breathing bubbles, and thinking, all without pausing the animation loop.

🎓 Concepts You'll Learn

Animation loop (draw)Trigonometric motion with sin()Array management with push() and splice()Randomness with random()Alpha transparency for layered shapesUsing frameCount as a timerModulo operator for cycling through an array

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts, making it the right place to configure the canvas and any drawing defaults (like text alignment and size) that should apply for the rest of the sketch.

function setup(){createCanvas(windowWidth,windowHeight);textAlign(CENTER,CENTER);textSize(20);}
Line-by-line explanation (3 lines)
createCanvas(windowWidth,windowHeight);
Creates a canvas that fills the entire browser window, so the bowl and fish scale to the visitor's screen.
textAlign(CENTER,CENTER);
Sets text alignment so that when text() is called, the string is centered both horizontally and vertically around the given coordinate - important for the dream bubble text.
textSize(20);
Sets the default font size in pixels for any text drawn later in draw().

draw()

draw() is the animation heartbeat of a p5.js sketch, running continuously (usually 60 times per second). Here it demonstrates several classic techniques at once: sine-wave motion for organic movement, an array-based particle system for the bubbles, and a frameCount-driven timer for cycling text - all patterns you'll reuse constantly in creative coding.

🔬 This loop moves every bubble upward and deletes it once it drifts off the top of the bowl. What happens if you change b.y-=b.v to b.y-=b.v*3 so bubbles rise three times faster?

  for(let i=bubbles.length-1;i>=0;i--){
    let b=bubbles[i];b.y-=b.v;circle(b.x,b.y,b.r*2);
    if(b.y<by-bowlR/2-40)bubbles.splice(i,1);
  }

🔬 The dream text switches every 180 frames (about 3 seconds). What happens if you change 180 to 60? Or to 600?

  ellipse(fx+20,fy-40,18,14);ellipse(fx+10,fy-20,12,9);
  fill(50);let dream=dreams[floor(frameCount/180)%dreams.length];text(dream,tx,ty-3);
function draw(){
  background(180,220,255);
  let bowlR=200, bx=width/2, by=height/2+60;
  noFill();stroke(255);strokeWeight(4);circle(bx,by,bowlR);
  let fx=bx+40*sin(frameCount*0.01), fy=by+10;
  noStroke();fill(255,140,0);ellipse(fx,fy,70,40);
  triangle(fx-35,fy,fx-55,fy-15,fx-55,fy+15);
  fill(0);circle(fx+20,fy-5,5);
  if(random(1)<0.02)bubbles.push({x:fx+35,y:fy-5,r:6+random(4),v:1+random(1)});
  stroke(200);noFill();
  for(let i=bubbles.length-1;i>=0;i--){
    let b=bubbles[i];b.y-=b.v;circle(b.x,b.y,b.r*2);
    if(b.y<by-bowlR/2-40)bubbles.splice(i,1);
  }
  let tx=bx, ty=by-160;
  noStroke();fill(255,255,255,230);
  ellipse(tx,ty,160,90);ellipse(tx-40,ty+5,60,50);ellipse(tx+40,ty+10,60,50);
  ellipse(fx+20,fy-40,18,14);ellipse(fx+10,fy-20,12,9);
  fill(50);let dream=dreams[floor(frameCount/180)%dreams.length];text(dream,tx,ty-3);
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Random Bubble Spawn if(random(1)<0.02)bubbles.push({x:fx+35,y:fy-5,r:6+random(4),v:1+random(1)});

Has roughly a 2% chance each frame of adding a new bubble object near the fish's mouth, with randomized size and speed.

for-loop Bubble Update & Cleanup Loop for(let i=bubbles.length-1;i>=0;i--){

Loops backward through the bubbles array so each bubble can rise upward and be safely removed with splice() once it floats above the bowl, without skipping elements.

calculation Dream Text Selection let dream=dreams[floor(frameCount/180)%dreams.length];

Uses frameCount divided by 180 (about 3 seconds at 60fps) and the modulo operator to cycle endlessly through the three dream strings.

background(180,220,255);
Repaints the whole canvas with a soft blue color every frame, erasing the previous frame so the animation doesn't smear.
let bowlR=200, bx=width/2, by=height/2+60;
Defines the bowl's radius and center position, placing it slightly below the vertical middle of the screen.
noFill();stroke(255);strokeWeight(4);circle(bx,by,bowlR);
Draws a thick white outline circle with no fill to represent the glass bowl.
let fx=bx+40*sin(frameCount*0.01), fy=by+10;
Calculates the fish's x position using a sine wave so it drifts smoothly left and right; fy stays a fixed offset below the bowl's center.
noStroke();fill(255,140,0);ellipse(fx,fy,70,40);
Draws the fish's orange oval body with no outline.
triangle(fx-35,fy,fx-55,fy-15,fx-55,fy+15);
Draws a triangular tail attached to the back of the fish's body.
fill(0);circle(fx+20,fy-5,5);
Draws a small black dot near the front of the fish to act as its eye.
if(random(1)<0.02)bubbles.push({x:fx+35,y:fy-5,r:6+random(4),v:1+random(1)});
Rolls a random number each frame; about 2% of the time it adds a new bubble object (with random size and rising speed) to the bubbles array near the fish's mouth.
stroke(200);noFill();
Sets a light gray outline and no fill so bubbles are drawn as hollow circles.
for(let i=bubbles.length-1;i>=0;i--){
Starts a backward loop through the bubbles array - looping backward makes it safe to remove items mid-loop without skipping the next one.
let b=bubbles[i];b.y-=b.v;circle(b.x,b.y,b.r*2);
Grabs the current bubble, moves it upward by subtracting its speed (v) from its y position, then draws it as a circle sized by its radius (r).
if(b.y<by-bowlR/2-40)bubbles.splice(i,1);
Once a bubble has risen far enough above the bowl to be off-screen, it's removed from the array with splice() so the array doesn't grow forever.
let tx=bx, ty=by-160;
Sets the position for the thought-bubble cloud, centered horizontally above the bowl.
noStroke();fill(255,255,255,230);
Sets a semi-transparent white fill (alpha 230 out of 255) so the thought cloud looks soft rather than fully opaque.
ellipse(tx,ty,160,90);ellipse(tx-40,ty+5,60,50);ellipse(tx+40,ty+10,60,50);
Draws three overlapping ellipses to form the puffy main shape of the thought-cloud bubble.
ellipse(fx+20,fy-40,18,14);ellipse(fx+10,fy-20,12,9);
Draws two small connector ellipses trailing from the fish's head up to the thought bubble, mimicking classic 'thinking' comic bubbles.
fill(50);let dream=dreams[floor(frameCount/180)%dreams.length];text(dream,tx,ty-3);
Sets a dark gray text color, picks which dream string to show based on elapsed frames, and draws it centered inside the thought cloud.

windowResized()

windowResized() is a built-in p5.js event function that fires automatically on browser resize. Pairing it with resizeCanvas() is the standard way to keep a full-window sketch responsive.

function windowResized(){resizeCanvas(windowWidth,windowHeight);}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth,windowHeight);
Automatically called by p5.js whenever the browser window is resized; this line resizes the canvas to match the new window dimensions so the sketch stays full-screen.

📦 Key Variables

bubbles array

Stores every active bubble as an object with x, y, radius (r), and rise speed (v); grows via push() when new bubbles spawn and shrinks via splice() when bubbles float off-screen.

let bubbles=[];
dreams array

Holds the three dream strings the fish cycles through in its thought bubble.

let dreams=['The Ocean','Freedom','A Bigger Bowl'];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

STYLE draw()

The entire sketch lives inside one long draw() function that mixes bowl drawing, fish drawing, bubble physics, and text logic together, making it harder to read and modify.

💡 Split it into helper functions like drawBowl(), drawFish(fx, fy), updateAndDrawBubbles(), and drawThoughtBubble(dream) for clearer organization.

BUG draw() bubble spawn

Bubble spawn position and bowl boundary use hard-coded offsets (like -40) that don't scale with bowlR, so resizing the bowl via the bowlR variable won't correctly keep bubbles contained.

💡 Express bubble removal bounds and spawn offsets as fractions of bowlR (e.g. by-bowlR/2-bowlR*0.2) so they stay proportional if bowlR changes.

PERFORMANCE draw()

A new object literal is created and pushed to the bubbles array on every frame's random check, and the array is scanned every frame even when empty - fine at this scale but could add up with many bubbles.

💡 For larger particle counts, consider capping bubbles.length or using a fixed-size pool of reusable bubble objects instead of constantly allocating new ones.

FEATURE draw() dream text

The dream only changes automatically based on frameCount, so a visitor has no way to interact with the fish.

💡 Add mousePressed() to let the viewer click the bowl and immediately cycle to the next dream, making the piece interactive.

🔄 Code Flow

Code flow showing setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> bubbleSpawn[bubble-spawn] draw --> bubbleUpdateLoop[bubble-update-loop] draw --> dreamSelection[dream-selection] bubbleSpawn -->|2% chance| draw bubbleUpdateLoop -->|loop backward through bubbles array| draw dreamSelection -->|frameCount % 3| draw click setup href "#fn-setup" click draw href "#fn-draw" click bubbleSpawn href "#sub-bubble-spawn" click bubbleUpdateLoop href "#sub-bubble-update-loop" click dreamSelection href "#sub-dream-selection"

❓ Frequently Asked Questions

What visual elements can be seen in the AI Daydreaming Fish sketch?

The sketch features a whimsical fish swimming in a circular bowl, surrounded by floating thought bubbles that display its dreams, along with tiny bubbles rising to the surface.

Is there any user interaction available in the AI Daydreaming Fish sketch?

The sketch is primarily animated, with no direct user interaction; however, users can enjoy the visual experience and watch the fish's dreams unfold.

What creative coding concepts are showcased in the AI Daydreaming Fish sketch?

This sketch demonstrates the use of animation, randomization, and layering of shapes to create a dynamic and whimsical underwater scene.

Preview

AI Daydreaming Fish - Dreams of the Ocean - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Daydreaming Fish - Dreams of the Ocean - Code flow showing setup, draw, windowresized
Code Flow Diagram