Daydreaming pink fish

This sketch creates a gently bobbing pink fish floating in a soft blue underwater scene, surrounded by rising bubbles and a rotating speech bubble that displays daydream thoughts every few seconds.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the floating motion — Increasing the multiplier inside sin() makes the fish bob faster.
  2. Add way more bubbles — Raising numBubbles fills the scene with a denser stream of rising bubbles.
  3. Make the fish orange — Changing the fill color used for the body swaps the fish's color instantly.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch brings a whimsical pink fish to life, floating in place while gently bobbing up and down as if daydreaming underwater. Around it, dozens of translucent bubbles rise endlessly from the bottom of the screen, and a speech bubble above the fish cycles through funny thoughts like 'Is that a worm?' and 'Just keep swimming.' The core techniques on display are sine-wave oscillation for smooth floating motion, ES6 classes for managing many independent bubble objects, and time-based logic using millis() to rotate text without slowing the frame rate.

The code is organized around a setup() that creates the canvas and populates a bubbles array, and a draw() loop that repositions the fish each frame, calls two custom drawing functions for the fish and its speech bubble, and updates every bubble. A separate Bubble class encapsulates each bubble's position, size, and speed, showing a clean pattern for reusable objects. Studying this sketch teaches you how to combine trigonometry, timers, custom functions, and classes into one cohesive animated scene.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, centers the fish, fills the bubbles array with 20 new Bubble objects (each starting below the screen at random positions), and records the current time for the speech bubble timer.
  2. Every frame, draw() repaints a semi-transparent light blue background, which subtly blends previous frames for a soft, misty water effect.
  3. The fish's vertical position is recalculated using sin(frameCount * 0.02) * 20, producing a smooth up-and-down floating motion without any manual animation state.
  4. drawPinkFish() is called to render the body, tail, fins, and eye as a series of ellipses and triangles positioned relative to the fish's current x and y.
  5. Each bubble in the array calls show() to draw itself and update() to rise upward; once a bubble drifts off the top of the screen, it resets to a new random position below the canvas, creating an endless looping stream.
  6. A timer checks how much time has passed since the last phrase change; once phraseDisplayDuration (3 seconds) elapses, currentPhraseIndex advances (looping back to 0 at the end) and drawSpeechBubble() renders the new phrase in a rounded bubble with a pointer above the fish.

🎓 Concepts You'll Learn

Sine wave oscillationES6 classesObject arraysTime-based logic with millis()Custom drawing functionswindowResized() responsivenessParticle-style animation

📝 Code Breakdown

setup()

setup() runs once when the sketch starts, making it the ideal place to size the canvas and initialize arrays of objects like the bubbles here.

🔬 This loop fills the bubbles array. What happens visually if numBubbles is changed to 100? What if it's changed to 2?

  for (let i = 0; i < numBubbles; i++) {
    bubbles.push(new Bubble());
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  // Set initial fish position to the center of the canvas
  fishX = width / 2;
  fishY = height / 2;

  // Create bubble objects and add them to the array
  for (let i = 0; i < numBubbles; i++) {
    bubbles.push(new Bubble());
  }

  // Initialize lastPhraseChangeTime
  lastPhraseChangeTime = millis();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Bubble Creation Loop for (let i = 0; i < numBubbles; i++) { bubbles.push(new Bubble()); }

Creates numBubbles new Bubble objects and adds them to the bubbles array

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window.
fishX = width / 2;
Places the fish horizontally at the center of the canvas.
fishY = height / 2;
Places the fish vertically at the center of the canvas (this gets overridden each frame in draw()).
for (let i = 0; i < numBubbles; i++) { bubbles.push(new Bubble()); }
Loops numBubbles times, creating a new Bubble object each time and adding it to the bubbles array.
lastPhraseChangeTime = millis();
Records the current time in milliseconds so the speech bubble timer has a starting point.

draw()

draw() runs continuously at roughly 60 frames per second. This sketch shows how to combine per-frame animation (sine wave motion, bubble movement) with time-based events (the phrase timer) that don't need to happen every single frame.

🔬 This block controls how often the fish's thought bubble changes. What happens if phraseDisplayDuration is set to 500 instead of 3000?

  if (millis() - lastPhraseChangeTime > phraseDisplayDuration) {
    currentPhraseIndex++;
    if (currentPhraseIndex >= dreamPhrases.length) {
      currentPhraseIndex = 0; // Loop back to the first phrase
    }
    lastPhraseChangeTime = millis(); // Reset timer
  }
function draw() {
  // Background color - a soft, dreamy blue-green for water
  background(173, 216, 230, 100); // Light blue with some transparency for a soft feel
  noStroke(); // No outlines for shapes

  // Gentle vertical movement for the fish to make it seem like it's floating
  // Using sin wave for smooth oscillation
  fishY = height / 2 + sin(frameCount * 0.02) * 20; // Oscillate up and down by 20 pixels

  // Draw the pink fish at its current position
  drawPinkFish(fishX, fishY, fishSize);

  // Draw and update all bubbles
  for (let bubble of bubbles) {
    bubble.show(); // Display the bubble
    bubble.update(); // Move the bubble up
  }

  // Speech bubble logic
  if (millis() - lastPhraseChangeTime > phraseDisplayDuration) {
    currentPhraseIndex++;
    if (currentPhraseIndex >= dreamPhrases.length) {
      currentPhraseIndex = 0; // Loop back to the first phrase
    }
    lastPhraseChangeTime = millis(); // Reset timer
  }

  // Draw the speech bubble
  drawSpeechBubble(fishX, fishY - fishSize * 0.6, dreamPhrases[currentPhraseIndex]);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Bubble Show & Update Loop for (let bubble of bubbles) { bubble.show(); // Display the bubble bubble.update(); // Move the bubble up }

Draws every bubble and then moves it upward, iterating over the whole bubbles array each frame

conditional Phrase Timer Check if (millis() - lastPhraseChangeTime > phraseDisplayDuration) {

Checks whether enough time has passed to switch to the next daydream phrase

conditional Phrase Index Wraparound if (currentPhraseIndex >= dreamPhrases.length) { currentPhraseIndex = 0; // Loop back to the first phrase }

Resets the phrase index back to 0 once it runs past the last phrase in the array

background(173, 216, 230, 100);
Repaints a semi-transparent light blue over the whole canvas each frame; the transparency lets previous frames blend slightly, giving a soft watery feel.
fishY = height / 2 + sin(frameCount * 0.02) * 20;
Uses the sine function on the ever-increasing frameCount to produce a smooth, repeating wave, moving the fish up and down by up to 20 pixels.
drawPinkFish(fishX, fishY, fishSize);
Calls the custom function that draws the fish's body, tail, fins, and eye at the current position and size.
for (let bubble of bubbles) { bubble.show(); // Display the bubble bubble.update(); // Move the bubble up }
Loops through every Bubble object in the array, drawing it and then moving it upward one step.
if (millis() - lastPhraseChangeTime > phraseDisplayDuration) {
Compares how much real time has elapsed since the last phrase change against the desired display duration (3000ms).
currentPhraseIndex++;
Advances to the next phrase in the dreamPhrases array.
drawSpeechBubble(fishX, fishY - fishSize * 0.6, dreamPhrases[currentPhraseIndex]);
Draws the speech bubble above the fish's head, showing the currently selected daydream phrase.

drawPinkFish()

This function shows how to build a complex shape out of simple primitives (ellipse and triangle) by positioning everything relative to a single x, y anchor point and a size variable, making the whole fish easy to move and scale.

🔬 This triangle draws the tail fin. What happens if you make the multipliers 1.25 much larger, like 2.5, so the tail sticks out further?

  // Fish tail (triangle)
  triangle(
    x - size * 0.75, y, // Left point at the back of the body
    x - size * 1.25, y - size * 0.4, // Top point of the tail fin
    x - size * 1.25, y + size * 0.4  // Bottom point of the tail fin
  );
function drawPinkFish(x, y, size) {
  // Fish body
  fill(255, 192, 203); // Pink color
  ellipse(x, y, size * 1.5, size * 0.8); // Wider than tall ellipse for the body

  // Fish tail (triangle)
  triangle(
    x - size * 0.75, y, // Left point at the back of the body
    x - size * 1.25, y - size * 0.4, // Top point of the tail fin
    x - size * 1.25, y + size * 0.4  // Bottom point of the tail fin
  );

  // Fish top fin (triangle)
  triangle(
    x - size * 0.2, y - size * 0.4,
    x + size * 0.2, y - size * 0.4,
    x, y - size * 0.6
  );

  // Fish bottom fin (triangle)
  triangle(
    x - size * 0.2, y + size * 0.4,
    x + size * 0.2, y + size * 0.4,
    x, y + size * 0.6
  );

  // Fish eye
  fill(255); // White eye background
  ellipse(x + size * 0.4, y - size * 0.2, size * 0.2); // Position relative to fish body
  fill(0); // Black pupil
  ellipse(x + size * 0.4, y - size * 0.2, size * 0.1);

  // NO MOUTH - Removed the arc for the mouth
}
Line-by-line explanation (7 lines)
fill(255, 192, 203); // Pink color
Sets the fill color to pink for the fish's body.
ellipse(x, y, size * 1.5, size * 0.8); // Wider than tall ellipse for the body
Draws an oval wider than it is tall, using size to scale both dimensions relative to the fish's base size.
triangle( x - size * 0.75, y, // Left point at the back of the body x - size * 1.25, y - size * 0.4, // Top point of the tail fin x - size * 1.25, y + size * 0.4 // Bottom point of the tail fin );
Draws the tail as a triangle positioned behind (to the left of) the fish's body, all coordinates computed relative to x and y.
fill(255); // White eye background
Switches fill color to white before drawing the eye's outer circle.
ellipse(x + size * 0.4, y - size * 0.2, size * 0.2); // Position relative to fish body
Draws the white part of the eye, offset to the upper-right of the fish's center.
fill(0); // Black pupil
Switches fill color to black for the pupil.
ellipse(x + size * 0.4, y - size * 0.2, size * 0.1);
Draws a smaller black circle on top of the white eye to form the pupil.

drawSpeechBubble()

This function demonstrates dynamic sizing: instead of using fixed dimensions, it measures the text with textWidth() and textAscent()/textDescent() so the speech bubble automatically fits any phrase, long or short.

🔬 This code sizes the bubble based on text width. What happens visually if cornerRadius is set to 0? What about a very large value like 50?

  let padding = 15;
  let bubbleW = textW + padding * 2;
  let bubbleH = textH + padding * 2;
  let cornerRadius = 10;
function drawSpeechBubble(x, y, textContent) {
  // Bubble background
  fill(255, 255, 255, 200); // White with some transparency
  stroke(0); // Black border
  strokeWeight(1);

  // Calculate text dimensions
  textSize(16);
  textFont('Arial'); // Use a standard font
  let textW = textWidth(textContent);
  let textH = textAscent() + textDescent();

  // Padding around the text
  let padding = 15;
  let bubbleW = textW + padding * 2;
  let bubbleH = textH + padding * 2;
  let cornerRadius = 10;

  // Bubble position (centered above x, top of bubble at y)
  let bubbleX = x - bubbleW / 2;
  let bubbleY = y - bubbleH;

  // Draw rounded rectangle for the bubble body
  rect(bubbleX, bubbleY, bubbleW, bubbleH, cornerRadius);

  // Draw pointer triangle
  triangle(
    x, y, // Pointing towards the fish
    bubbleX + bubbleW / 2 - 10, bubbleY + bubbleH, // Left base of pointer
    bubbleX + bubbleW / 2 + 10, bubbleY + bubbleH  // Right base of pointer
  );

  // Text inside the bubble
  fill(0); // Black text color
  noStroke();
  textAlign(CENTER, CENTER);
  text(textContent, bubbleX + bubbleW / 2, bubbleY + bubbleH / 2);

  noStroke(); // Reset stroke
}
Line-by-line explanation (7 lines)
let textW = textWidth(textContent);
Measures how wide the given phrase will render at the current text size, so the bubble can be sized to fit it exactly.
let textH = textAscent() + textDescent();
Calculates the total height of a line of text by adding the space above and below the baseline.
let bubbleW = textW + padding * 2;
Adds padding on both sides of the text to compute the bubble's total width.
let bubbleX = x - bubbleW / 2;
Centers the bubble horizontally on the given x coordinate by shifting left half the bubble's width.
rect(bubbleX, bubbleY, bubbleW, bubbleH, cornerRadius);
Draws the bubble's body as a rectangle with rounded corners, sized to fit the text exactly.
triangle( x, y, // Pointing towards the fish bubbleX + bubbleW / 2 - 10, bubbleY + bubbleH, // Left base of pointer bubbleX + bubbleW / 2 + 10, bubbleY + bubbleH // Right base of pointer );
Draws a small triangle pointing from the bottom of the bubble down toward the fish, like a classic speech bubble tail.
text(textContent, bubbleX + bubbleW / 2, bubbleY + bubbleH / 2);
Draws the phrase text centered inside the bubble.

windowResized()

windowResized() is a built-in p5.js callback that fires automatically when the browser window changes size, letting you keep responsive sketches that adapt without a page reload.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // No need to reset fish position here as draw() updates it relative to height/2
  // Bubbles will naturally adapt as their reset function uses window width/height
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window whenever it changes size, keeping the sketch full-screen.

📦 Key Variables

fishX number

Stores the fish's horizontal position, set once in setup() and kept constant through draw().

let fishX;
fishY number

Stores the fish's vertical position, recalculated every frame using a sine wave for floating motion.

let fishY;
fishSize number

Base size used to scale every part of the fish (body, fins, tail, eye).

let fishSize = 100;
bubbles array

Holds all Bubble objects that are drawn and updated every frame.

let bubbles = [];
numBubbles number

Determines how many Bubble objects are created in setup().

let numBubbles = 20;
dreamPhrases array

List of text strings the fish 'thinks' that cycle through the speech bubble.

let dreamPhrases = ["Zzzz...", "What if I could fly?"];
currentPhraseIndex number

Tracks which phrase in dreamPhrases is currently displayed.

let currentPhraseIndex = 0;
lastPhraseChangeTime number

Stores the millis() timestamp of the last phrase change, used to time when to switch to the next phrase.

let lastPhraseChangeTime = 0;
phraseDisplayDuration number

How long, in milliseconds, each phrase stays visible before switching to the next one.

let phraseDisplayDuration = 3000;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG drawPinkFish()

The eye is always drawn on the right side of the fish, which only looks correct if the fish always faces right; since the fish never turns or moves horizontally, this isn't currently a visible bug, but it would break if horizontal movement were added later.

💡 Store a facing direction variable and mirror the eye/tail positions based on it so the fish could someday swim left or right convincingly.

PERFORMANCE drawSpeechBubble()

textSize() and textFont() are called every single frame even though they never change, which is a small but unnecessary repeated cost.

💡 Move textSize(16) and textFont('Arial') into setup() since they only need to be set once.

STYLE draw() speech bubble logic

The phrase-cycling logic is embedded directly in draw(), mixing timing logic with the main render loop and making draw() harder to scan.

💡 Extract the phrase-updating code into its own function, e.g. updateDreamPhrase(), and call it from draw() for better readability.

FEATURE Bubble class

Bubbles only ever move straight up with no horizontal drift, making their motion look mechanical.

💡 Add a slight horizontal wobble using sin(frameCount + this.offset) so bubbles sway side to side as they rise, adding more organic movement.

🔄 Code Flow

Code flow showing setup, draw, drawpinkfish, drawspeechbubble, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> bubble-update-loop[bubble-update-loop] draw --> phrase-timer-check[phrase-timer-check] bubble-update-loop --> bubble-creation-loop[bubble-creation-loop] bubble-update-loop --> bubble-update-loop phrase-timer-check --> phrase-loop-reset[phrase-loop-reset] phrase-timer-check --> draw phrase-loop-reset --> draw click setup href "#fn-setup" click draw href "#fn-draw" click bubble-creation-loop href "#sub-bubble-creation-loop" click bubble-update-loop href "#sub-bubble-update-loop" click phrase-timer-check href "#sub-phrase-timer-check" click phrase-loop-reset href "#sub-phrase-loop-reset"

❓ Frequently Asked Questions

What visual experience does the Daydreaming Pink Fish sketch offer?

The sketch creates a serene underwater scene featuring a floating pink fish and animated bubbles, all set against a soft blue-green background that evokes a dreamy atmosphere.

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

The current version of the sketch is not interactive; it is a passive visual experience that showcases the fish and bubbles moving gracefully.

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

This sketch demonstrates concepts such as smooth oscillation for movement using sine waves and the creation of animated objects like bubbles, along with the use of speech bubbles to convey thoughts.

Preview

Daydreaming pink fish - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Daydreaming pink fish - Code flow showing setup, draw, drawpinkfish, drawspeechbubble, windowresized
Code Flow Diagram