Aquarium Attempt - xelsed.ai (Remix)

This interactive aquarium simulation lets players purchase and manage 12 different sea creature types using coins earned when creatures eat each other. The sketch features dynamic predator-prey relationships, depth-based rendering, animated ocean backgrounds, and a summoned Manta Ray boss that can absorb nearby fish.

🧪 Try This!

Experiment with the code by making these changes:

  1. Start with more coins — Buy expensive creatures immediately without waiting for predation to earn coins
  2. Double the Manta Ray's suction power — Make the boss suck harder and faster, watching fish spiral in more aggressively
  3. Turn off jellyfish glow
  4. Spawn a Manta Ray from the start — The boss will be in the aquarium immediately instead of being purchased
  5. Speed up all creatures
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a living aquarium where you manage sea creatures by spending coins earned through the ecosystem's predator-prey relationships. Jellyfish drift upward trailing colored particles, fish dart across the canvas, sharks hunt relentlessly, and a summoned Manta Ray boss can vacuum up entire schools. The sketch showcases advanced p5.js techniques: object-oriented design with 12 custom classes, array management and splicing for population control, depth-ordered rendering via multiple draw loops, HSB color mode for vivid hues, and interactive button UI for purchasing creatures.

The code is organized as a massive ecosystem: setup() initializes 12 creature lists plus UI buttons, draw() loops through each creature type in depth order while updating and displaying them, and each creature class handles its own movement physics, collision detection, and predatory behavior. By studying it you'll learn how to structure large interactive projects with dozens of independent objects, implement basic AI hunting behaviors, manage game state with coin currency, and orchestrate complex layered animations.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas filling the window, initializes 200 starting coins, spawns 14 jellies, 10 fish, 2 whales, 3 anglerfish, 2 sharks, 2 octopuses, 3 barracudas, 3 archerfish, 4 seahorses, 2 vampire fish, and 1 goliath tigerfish randomly positioned at species-appropriate depths. It also creates 12 purchase buttons stacked down the left side and 60 background bubbles plus 90 drifting particles.
  2. Every frame, draw() clears the canvas with a dark gradient background (rendered once into a cached buffer), updates and displays bubbles and particles behind everything, then loops through creature types in depth order: whales → anglerfish → octopus → seahorses → manta ray → barracudas → vampire fish → goliath tigerfish → regular fish → archerfish → sharks → jellies (with additive blending for glow).
  3. Each creature's update() moves it horizontally across the canvas and vertically using sine-wave wiggling. When creatures exit the screen they wrap around and reset to a new random depth lane. Predator classes (Barracuda, VampireFish, GoliathTigerfish) scan nearby creature arrays and eat anything smaller—awarding coins based on prey size.
  4. When you click in the water (not over UI buttons), a GlowOrb spawns and drifts upward while fading. Clicking a purchase button deducts coins and spawns a new creature if you have enough balance.
  5. The Manta Ray boss (summoned from the shop, not spawned initially) uses an 'applyGravityGulp()' method to pull all nearby fish toward its mouth, add swirl forces, and absorb them when they get close enough—building a rage meter that fills as it consumes.
  6. Jellyfish update() rewards coins when they exit the top (simulating harvest), and ArcherFish spit projectiles upward that award coins if they reach the top of the screen.

🎓 Concepts You'll Learn

Object-oriented design (ES6 classes)Array management and splice()Predator-prey mechanicsDepth-based rendering and layeringState machines (Octopus hopping, Manta Ray gulping)Physics: velocity, acceleration, dampingCollision detection using dist()UI buttons and event listenersHSB color modeParticle systems and trails

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to initialize all your game state: canvas, arrays of objects, buttons, and any static layers (like the background buffer). Notice how each creature type gets its own array (jellyfishList, fishList, etc.) and its own spawn loop—this makes the code repetitive but easy to extend with new species.

🔬 This loop spawns jellies at random x between 15% and 85% of canvas width. What happens if you change the x range to random(0, width) so they can spawn at the edges? Or what if you change y to always spawn at the top: y = 0?

  for (let i = 0; i < NUM_JELLYFISH; i++) {
    const x = random(width * 0.15, width * 0.85);
    const y = random(height);
    const baseSize = random(18, 40);
    const hue = random(190, 320);
    jellyfishList.push(new Jellyfish(x, y, baseSize, hue));
  }
function setup() {
  createCanvas(windowWidth, windowHeight);

  coins = 200;

  bgLayer = createGraphics(windowWidth, windowHeight);
  drawBackground(bgLayer);

  // Jellyfish
  for (let i = 0; i < NUM_JELLYFISH; i++) {
    const x = random(width * 0.15, width * 0.85);
    const y = random(height);
    const baseSize = random(18, 40);
    const hue = random(190, 320);
    jellyfishList.push(new Jellyfish(x, y, baseSize, hue));
  }

  // Fish
  for (let i = 0; i < NUM_FISH; i++) {
    const x = random(width);
    const y = random(height * 0.1, height * 0.9);
    const size = random(15, 35);
    const hue = random() < 0.5 ? random(0, 60) : random(150, 250);
    const direction = random() < 0.5 ? 1 : -1;
    fishList.push(new Fish(x, y, size, hue, direction));
  }

  // ... (all 12 creature type loops omitted for brevity) ...

  // Particles
  for (let i = 0; i < NUM_PARTICLES; i++) {
    particles.push(new Particle(true));
  }

  // Bubbles
  for (let i = 0; i < NUM_BUBBLES; i++) {
    bubbleList.push(new Bubble(random(width), random(height), random(1, 4), random(0.1, 0.5)));
  }

  // --- Shop buttons ---

  buyJellyfishButton = createButton(`Buy Jellyfish (${JELLYFISH_COST} coins)`);
  buyJellyfishButton.class('shop-button');
  buyJellyfishButton.position(20, 20);
  buyJellyfishButton.mousePressed(() => {
    if (coins >= JELLYFISH_COST) {
      coins -= JELLYFISH_COST;
      const x = random(width * 0.15, width * 0.85);
      const y = height + random(100, 300);
      const baseSize = random(18, 40);
      const hue = random(190, 320);
      jellyfishList.push(new Jellyfish(x, y, baseSize, hue));
    } else {
      alert("Not enough coins to buy a jellyfish!");
    }
  });

  // ... (11 more purchase button declarations follow the same pattern) ...
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Canvas setup createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas that fills the browser window

for-loop Spawn all creature types for (let i = 0; i < NUM_JELLYFISH; i++) { jellyfishList.push(new Jellyfish(...)); }

Loops through each creature constant, instantiates N creatures with random properties, and pushes them into their species-specific array

calculation Create purchase UI buttons buyJellyfishButton = createButton(`Buy Jellyfish (${JELLYFISH_COST} coins)`);

Creates a clickable button that deducts coins and spawns a new creature if the player has enough balance

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that matches the browser window size, which will be the drawing surface for all aquarium creatures
coins = 200;
Sets the player's starting coin balance to 200—enough to buy several cheap creatures or a few expensive ones
bgLayer = createGraphics(windowWidth, windowHeight);
Creates an off-screen graphics buffer (the same size as the main canvas) where the ocean background will be drawn once and reused every frame
drawBackground(bgLayer);
Calls a helper function to fill the buffer with a gradient ocean, light rays, and other static scenery that won't change
for (let i = 0; i < NUM_JELLYFISH; i++) { const x = random(width * 0.15, width * 0.85);
Spawns NUM_JELLYFISH (14) new Jellyfish objects at random x positions between 15% and 85% of canvas width (keeping them away from edges)
jellyfishList.push(new Jellyfish(x, y, baseSize, hue));
Adds the newly created Jellyfish to the jellyfishList array so it will be updated and drawn every frame
buyJellyfishButton.mousePressed(() => { if (coins >= JELLYFISH_COST) { coins -= JELLYFISH_COST; jellyfishList.push(new Jellyfish(...)); } });
Registers a click listener on the button—when clicked, it checks if the player has enough coins, deducts the cost, and spawns a new jellyfish

draw()

draw() is the main animation loop, called 60 times per second. It's responsible for clearing the screen, updating every creature's position and state, drawing them in the right depth order, and updating the UI. The depth-order comment shows which creatures get drawn when—the first loop draws farthest back, the last loop draws on top. This creates the illusion of depth. Notice how jellyfish are drawn last with additive blending to make them glow on top of everything.

🔬 This sequence of loops controls what gets drawn on top of what. Whales are drawn first (farthest back), then anglerfish, then octopus. What happens if you move the octopus loop before the whale loop, so octopuses get drawn underneath? Or what if you move sharks to the very beginning so they're always behind everything?

  for (let w of whaleList) {
    w.update();
    w.display();
  }

  for (let a of anglerfishList) {
    a.update();
    a.display();
  }

  for (let o of octopusList) {
    o.update();
    o.display();
  }
function draw() {
  colorMode(RGB, 255);
  background(0);
  image(bgLayer, 0, 0, width, height);

  colorMode(HSB, 360, 100, 100, 100);

  // Bubbles behind everything
  for (let b of bubbleList) {
    b.update();
    b.display();
  }

  // Particles behind creatures
  for (let p of particles) {
    p.update();
    p.display();
  }

  // click‑spawned glow orbs (just behind creatures)
  for (let g of glowOrbList) {
    g.update();
    g.display();
  }

  // Depth ordering:
  // whales →
  // anglerfish →
  // octopus →
  // seahorses →
  // manta ray boss →
  // barracuda / vampire / goliath →
  // regular fish →
  // archerfish →
  // sharks →
  // jellyfish

  for (let w of whaleList) {
    w.update();
    w.display();
  }

  for (let a of anglerfishList) {
    a.update();
    a.display();
  }

  for (let o of octopusList) {
    o.update();
    o.display();
  }

  for (let h of seahorseList) {
    h.update();
    h.display();
  }

  for (let mr of mantaRayList) {
    mr.update();
    mr.display();
  }

  for (let b of barracudaList) {
    b.update();
    b.display();
  }

  for (let vf of vampireFishList) {
    vf.update();
    vf.display();
  }

  for (let gt of goliathTigerfishList) {
    gt.update();
    gt.display();
  }

  for (let f of fishList) {
    f.update();
    f.display();
  }

  for (let af of archerFishList) {
    af.update();
    af.display();
  }

  for (let s of sharkList) {
    s.update();
    s.display();
  }

  // Jellyfish glow
  blendMode(ADD);
  for (let j of jellyfishList) {
    j.update();
    j.display();
  }
  blendMode(BLEND);

  // Coins UI
  fill(60, 80, 100);
  textSize(24);
  textAlign(RIGHT, TOP);
  text(`Coins: ${coins}`, width - 20, 20);

  // Button labels
  buyJellyfishButton.html(`Buy Jellyfish (${JELLYFISH_COST} coins)`);
  buyFishButton.html(`Buy Fish (${FISH_COST} coins)`);
  buyWhaleButton.html(`Buy Whale (${WHALE_COST} coins)`);
  buyAnglerfishButton.html(`Buy Anglerfish (${ANGLERFISH_COST} coins)`);
  buySharkButton.html(`Buy Shark (${SHARK_COST} coins)`);
  buyOctopusButton.html(`Buy Octopus (${OCTOPUS_COST} coins)`);
  buyBarracudaButton.html(`Buy Barracuda (${BARRACUDA_COST} coins)`);
  buyArcherFishButton.html(`Buy Archerfish (${ARCHERFISH_COST} coins)`);
  buySeahorseButton.html(`Buy Seahorse (${SEAHORSE_COST} coins)`);
  buyVampireFishButton.html(`Buy Vampire Fish (${VAMPIREFISH_COST} coins)`);
  buyGoliathTigerfishButton.html(`Buy Goliath Tigerfish (${GOLIATH_TIGERFISH_COST} coins)`);
  buyMantaRayButton.html(
    mantaRayList.length === 0
      ? `Summon Manta Ray Boss (${MANTARAY_COST} coins)`
      : `Manta Ray Boss (active)`
  );
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Draw background buffer image(bgLayer, 0, 0, width, height);

Displays the pre-rendered ocean background (gradient, light rays, gradient vignette) every frame without redrawing it

calculation Switch to HSB color mode colorMode(HSB, 360, 100, 100, 100);

Changes from RGB to HSB (Hue, Saturation, Brightness) so creature hues can vary naturally while saturation and brightness stay vibrant

for-loop Depth-ordered creature loops for (let w of whaleList) { w.update(); w.display(); } for (let a of anglerfishList) { ... }

Updates and draws each creature type in a specific depth order so larger creatures in back don't overdraw smaller creatures in front

conditional Jellyfish additive blending blendMode(ADD); for (let j of jellyfishList) { j.update(); j.display(); } blendMode(BLEND);

Temporarily switches to ADD blending mode so jellyfish glow layers accumulate light instead of overwriting, then switches back to normal BLEND

calculation Draw coin counter text(`Coins: ${coins}`, width - 20, 20);

Displays the current coin balance in the top-right corner of the screen

calculation Update button labels buyJellyfishButton.html(`Buy Jellyfish (${JELLYFISH_COST} coins)`);

Updates each button's text label every frame (this is inefficient but harmless—labels rarely change)

colorMode(RGB, 255);
Switches to RGB color mode so the background image can be drawn in normal RGB values (0–255 for red, green, blue)
background(0);
Clears the canvas with pure black—necessary because we're about to draw the background image on top
image(bgLayer, 0, 0, width, height);
Draws the pre-rendered ocean background buffer (created in setup and stored in bgLayer) to fill the entire canvas
colorMode(HSB, 360, 100, 100, 100);
Switches to HSB color mode (360° hue range, 0–100 saturation, 0–100 brightness)—all creatures use HSB for natural color variation
for (let b of bubbleList) { b.update(); b.display(); }
Loops through every bubble, updates its position (rising upward), and draws it—bubbles are farthest back so drawn first
for (let w of whaleList) { w.update(); w.display(); }
Updates each whale's position and collision detection, then draws it—whales are drawn early (far back) in the depth order
blendMode(ADD);
Switches to additive blending mode so jellyfish glow rays add their color to whatever is behind them instead of replacing it
for (let j of jellyfishList) { j.update(); j.display(); }
Updates each jellyfish's pulsing, drifting, and tentacle animation, then draws it with the current additive blend mode for glowing effect
blendMode(BLEND);
Switches back to normal (BLEND) mode so UI text doesn't glow
text(`Coins: ${coins}`, width - 20, 20);
Draws the coin counter text in the top-right corner using HSB color mode (so the text is cyan-tinted)

mousePressed()

mousePressed() is a built-in p5.js function that fires whenever the user clicks the mouse. By checking mouseX and returning early if the click is over the UI, we prevent interference with buttons. This is a simple form of event handling—the button's own click listener (registered in setup) takes priority.

🔬 This line creates a GlowOrb with a random size (25–55) and random hue (180–260). What happens if you change the size range to random(5, 100) for tiny sparkles and giant orbs? Or change the hue to random(0, 360) so orbs can be any color instead of just cyan-purple?

  glowOrbList.push(new GlowOrb(mouseX, mouseY, random(25, 55), random(180, 260)));
function mousePressed() {
  if (mouseX < 200) return;
  glowOrbList.push(new GlowOrb(mouseX, mouseY, random(25, 55), random(180, 260)));
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Ignore clicks over UI if (mouseX < 200) return;

Prevents spawning glow orbs if the click happens over the left 200 pixels where buttons are stacked

calculation Spawn interactive glow orb glowOrbList.push(new GlowOrb(mouseX, mouseY, random(25, 55), random(180, 260)));

Creates a new GlowOrb at the click position with a random size and hue, then adds it to the list so it will drift upward and fade

if (mouseX < 200) return;
Checks if the click happened in the left 200 pixels of the screen (where the purchase buttons are). If so, return early and don't spawn an orb—the button's own click handler runs instead.
glowOrbList.push(new GlowOrb(mouseX, mouseY, random(25, 55), random(180, 260)));
Creates a new GlowOrb object at the mouse position with a random radius between 25–55 pixels and a random hue between 180–260 (cyan to purple range), then adds it to glowOrbList so it gets updated and drawn every frame

drawBackground(g)

drawBackground() is called once in setup() to pre-render a static ocean background into a graphics buffer. This is a performance optimization: instead of redrawing the gradient, light rays, and vignette every frame, we draw them once and reuse the image. The function uses g.push()/pop() to isolate state changes, RGB color mode for precise ocean blues, and even the raw Canvas 2D API (ctx.createRadialGradient) for advanced effects p5.js doesn't wrap.

🔬 This loop draws 6 light rays. What happens if you change 6 to 12 to add more rays? Or change the spread calculation to g.width * (0.15 + random(-0.02, 0.02)) to make the rays wider? Or change the alpha from 18 to 50 to make the rays brighter?

  for (let i = 0; i < 6; i++) {
    const cx = g.width * ((i + 0.5) / 6 + random(-0.03, 0.03));
    const spread = g.width * (0.08 + random(-0.02, 0.02));
    const alpha = 18;
    g.fill(140, 200, 255, alpha);
    g.beginShape();
    g.vertex(cx - spread, 0);
    g.vertex(cx + spread, 0);
    g.vertex(cx + spread * 0.18, g.height * 0.85);
    g.vertex(cx - spread * 0.18, g.height * 0.85);
    g.endShape(CLOSE);
  }
function drawBackground(g) {
  g.push();
  g.colorMode(RGB, 255);

  const topCol = g.color(10, 30, 80);
  const midCol = g.color(3, 12, 40);
  const bottomCol = g.color(0, 3, 15);

  for (let y = 0; y < g.height; y++) {
    const t = y / g.height;
    let c;
    if (t < 0.5) {
      c = g.lerpColor(topCol, midCol, t / 0.5);
    } else {
      c = g.lerpColor(midCol, bottomCol, (t - 0.5) / 0.5);
    }
    g.stroke(c);
    g.line(0, y, g.width, y);
  }

  g.noStroke();
  g.fill(120, 180, 255, 40);
  g.ellipse(g.width / 2, -g.height * 0.15, g.width * 1.8, g.height * 0.7);

  g.blendMode(ADD);
  g.noStroke();
  for (let i = 0; i < 6; i++) {
    const cx = g.width * ((i + 0.5) / 6 + random(-0.03, 0.03));
    const spread = g.width * (0.08 + random(-0.02, 0.02));
    const alpha = 18;
    g.fill(140, 200, 255, alpha);
    g.beginShape();
    g.vertex(cx - spread, 0);
    g.vertex(cx + spread, 0);
    g.vertex(cx + spread * 0.18, g.height * 0.85);
    g.vertex(cx - spread * 0.18, g.height * 0.85);
    g.endShape(CLOSE);
  }
  g.blendMode(BLEND);

  const ctx = g.drawingContext;
  const gradient = ctx.createRadialGradient(
    g.width / 2,
    g.height * 0.2,
    g.width * 0.1,
    g.width / 2,
    g.height * 0.5,
    g.width * 0.9
  );
  gradient.addColorStop(0, "rgba(0,0,0,0)");
  gradient.addColorStop(1, "rgba(0,0,0,0.7)");
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, g.width, g.height);

  g.pop();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Ocean gradient (top to bottom) for (let y = 0; y < g.height; y++) { const t = y / g.height; let c; if (t < 0.5) { c = g.lerpColor(topCol, midCol, t / 0.5); } else { c = g.lerpColor(midCol, bottomCol, (t - 0.5) / 0.5); } g.stroke(c); g.line(0, y, g.width, y); }

Draws a gradient from light blue at top → darker blue in middle → nearly black at bottom by drawing 1-pixel horizontal lines

for-loop Underwater light rays for (let i = 0; i < 6; i++) { const cx = g.width * ((i + 0.5) / 6 + random(-0.03, 0.03)); g.fill(140, 200, 255, alpha); g.beginShape(); g.vertex(...); g.endShape(CLOSE); }

Draws 6 slightly randomized trapezoids (light ray shapes) fanning down from the top with additive blending to simulate sunlight shafts

calculation Radial vignette shadow const gradient = ctx.createRadialGradient(...); gradient.addColorStop(0, "rgba(0,0,0,0)"); gradient.addColorStop(1, "rgba(0,0,0,0.7)"); ctx.fillStyle = gradient; ctx.fillRect(0, 0, g.width, g.height);

Uses Canvas API (not p5.js) to draw a radial gradient that fades from transparent in the center to dark at the edges, creating depth

function drawBackground(g) {
Takes a parameter g (a graphics buffer) instead of drawing to the main canvas—this function will be called once in setup() to pre-render a reusable background
g.push();
Saves the current drawing state (color mode, stroke, fill, etc.) so changes inside this function won't affect the main sketch
g.colorMode(RGB, 255);
Sets the buffer to RGB color mode (not HSB) so we can specify precise dark blue ocean colors using RGB values
const topCol = g.color(10, 30, 80);
Creates a dark blue color (RGB: 10, 30, 80) for the top of the ocean where light reaches
for (let y = 0; y < g.height; y++) {
Loops through every pixel row from top to bottom, creating a gradient by drawing many 1-pixel horizontal lines
const t = y / g.height;
Calculates a 0–1 fraction representing how far down the canvas we are (0 = top, 1 = bottom)
if (t < 0.5) { c = g.lerpColor(topCol, midCol, t / 0.5); }
For the top half (0–50%), interpolate between topCol (light blue) and midCol (darker blue) using lerpColor for a smooth blend
for (let i = 0; i < 6; i++) { const cx = g.width * ((i + 0.5) / 6 + random(-0.03, 0.03));
Loops 6 times to create 6 light rays. Each ray is centered at a different x position (spaced across the canvas) with slight random jitter
g.blendMode(ADD);
Sets the blend mode to ADD so the light rays add their brightness to the background instead of overwriting it
g.vertex(cx - spread, 0); g.vertex(cx + spread, 0);
Draws the top edge of the trapezoid light ray (wide at the top where the sun is)
const gradient = ctx.createRadialGradient(...);
Uses the raw Canvas 2D API (via g.drawingContext) to create a radial gradient that can't be made with p5.js alone
g.pop();
Restores the drawing state that was saved at the beginning, leaving the sketch's settings unchanged

Jellyfish.update() and Jellyfish.display()

The Jellyfish class demonstrates advanced animation: pulsing via sine waves mapped to size and speed, smooth drifting using Perlin noise instead of random(), motion trails that fade with distance, and multi-layered procedural drawing (bell, tentacles, oral arms, glow). The key insight is that each jelly has its own phase offsets and speed parameters, making 14 unique jellies feel alive and varied even though they all use the same code.

🔬 This code adds the jelly's position to a trail array, then removes the oldest point if the trail gets too long. What happens if you increase this.trailLength to 30? The trail will be much longer, showing more of the jelly's path. Or what if you never call shift()—what does the trail look like then?

    this.trail.push(this.pos.copy());
    if (this.trail.length > this.trailLength) {
      this.trail.shift();
    }
class Jellyfish {
  constructor(x, y, baseSize, hue) {
    this.pos = createVector(x, y);
    this.baseSize = baseSize;
    this.hue = hue;
    this.minSpeed = random(0.15, 0.35);
    this.maxSpeed = this.minSpeed + random(0.25, 0.6);
    this.horizontalDriftAmount = random(25, 60);
    this.horizontalDriftSpeed = random(0.002, 0.006);
    this.noiseSeed = random(1000);
    this.pulseSpeed = random(0.03, 0.06);
    this.pulseOffset = random(TWO_PI);
    this.bellScale = 1;
    this.numTentacles = floor(random(7, 13));
    this.tentacleLength = random(90, 150);
    this.tentacleWiggleSpeed = random(0.015, 0.03);
    this.tentacleWiggleAmount = random(10, 24);
    this.tentaclePhaseOffsets = [];
    for (let i = 0; i < this.numTentacles; i++) {
      this.tentaclePhaseOffsets.push(random(TWO_PI));
    }
    this.numOralArms = floor(random(3, 5));
    this.oralArmLength = random(70, 110);
    this.oralArmWiggleSpeed = random(0.02, 0.04);
    this.oralArmWiggleAmount = random(8, 16);
    this.oralArmPhaseOffsets = [];
    for (let i = 0; i < this.numOralArms; i++) {
      this.oralArmPhaseOffsets.push(random(TWO_PI));
    }
    this.currentX = x;
    this.trail = [];
    this.trailLength = floor(random(8, 15));
    this.trailAlphaReduction = 100 / this.trailLength;
  }

  update() {
    this.trail.push(this.pos.copy());
    if (this.trail.length > this.trailLength) {
      this.trail.shift();
    }
    const t = frameCount * this.pulseSpeed + this.pulseOffset;
    const pulsePhase = (sin(t) + 1) / 2;
    this.bellScale = 1 + 0.18 * sin(t);
    const swimSpeed = lerp(this.minSpeed, this.maxSpeed, pulsePhase);
    this.pos.y -= swimSpeed;
    const n = noise(this.noiseSeed, frameCount * this.horizontalDriftSpeed);
    const drift = map(n, 0, 1, -this.horizontalDriftAmount, this.horizontalDriftAmount);
    this.currentX = this.pos.x + drift;
    if (this.pos.y < -this.baseSize * 4) {
      this.pos.y = height + random(this.baseSize, height * 0.5);
      this.pos.x = random(width * 0.15, width * 0.85);
      coins += floor(this.baseSize * 0.5);
    }
  }

  display() {
    push();
    translate(this.currentX, this.pos.y);
    const r = this.baseSize * this.bellScale;
    // Trail
    for (let i = 0; i < this.trail.length; i++) {
      const alpha = 100 - i * this.trailAlphaReduction;
      fill(this.hue, 50, 100, alpha);
      noStroke();
      const trailSize = map(i, 0, this.trail.length, 0.8 * r, 0.2 * r);
      ellipse(this.trail[i].x - this.currentX, this.trail[i].y - this.pos.y, trailSize);
    }
    this.drawTentacles(r);
    this.drawOralArms(r);
    this.drawBell(r);
    pop();
  }

  drawTentacles(r) {
    const segments = 18;
    const baseY = r * 0.6;
    for (let i = 0; i < this.numTentacles; i++) {
      const tIndex = (i - (this.numTentacles - 1) / 2) / max(1, (this.numTentacles - 1) / 2);
      const anchorX = tIndex * r * 0.9;
      let prevX = anchorX;
      let prevY = baseY;
      const phaseOffset = this.tentaclePhaseOffsets[i];
      for (let j = 1; j <= segments; j++) {
        const portion = j / segments;
        const targetY = baseY + this.tentacleLength * portion;
        const sway = sin(frameCount * this.tentacleWiggleSpeed + phaseOffset + j * 0.35);
        const swayAmount = this.tentacleWiggleAmount * portion * portion;
        const x = anchorX + sway * swayAmount;
        const y = targetY + this.bellScale * -3 * (1 - portion);
        const w = map(j, 0, segments, 2.2, 0.4);
        strokeWeight(w);
        const alpha = map(j, 0, segments, 60, 10);
        stroke(this.hue, 55, 100, alpha);
        line(prevX, prevY, x, y);
        prevX = x;
        prevY = y;
      }
    }
  }

  drawBell(r) {
    noStroke();
    const baseHue = this.hue;
    const glowLayers = 4;
    for (let i = glowLayers; i >= 1; i--) {
      const glowRadius = r * (1.4 + i * 0.35);
      const alpha = 7 * i;
      fill(baseHue, 70, 100, alpha);
      ellipse(0, 0, glowRadius * 2.0, glowRadius * 1.3);
    }
    fill(baseHue, 70, 100, 75);
    const bellWidth = r * 2.0;
    const bellHeight = r * 1.4;
    const segmentCount = 12;
    const segmentDepth = r * 0.1;
    beginShape();
    for (let i = 0; i < segmentCount; i++) {
      const angle1 = map(i, 0, segmentCount, -PI, 0);
      const angle2 = map(i + 0.5, 0, segmentCount, -PI, 0);
      const angle3 = map(i + 1, 0, segmentCount, -PI, 0);
      const x1 = cos(angle1) * bellWidth * 0.5;
      const y1 = sin(angle1) * bellHeight * 0.5;
      const x2 = cos(angle2) * (bellWidth * 0.5 - segmentDepth);
      const y2 = sin(angle2) * (bellHeight * 0.5 + segmentDepth * 0.5);
      const x3 = cos(angle3) * bellWidth * 0.5;
      const y3 = sin(angle3) * bellHeight * 0.5;
      if (i === 0) vertex(x1, y1);
      bezierVertex(x1, y1 + bellHeight * 0.2, x2, y2 + bellHeight * 0.2, x3, y3);
    }
    endShape(CLOSE);
    fill(baseHue, 80, 75, 90);
    ellipse(0, r * 0.45, r * 1.5, r * 0.8);
    fill(baseHue + 15, 45, 100, 70);
    ellipse(0, -r * 0.15, r * 1.1, r * 0.7);
    stroke(baseHue + 10, 40, 100, 45);
    strokeWeight(1.4);
    noFill();
    const innerCount = 4;
    for (let i = 0; i < innerCount; i++) {
      const a = map(i, 0, innerCount - 1, -PI / 4, PI / 4);
      const x1 = sin(a) * r * 0.3;
      const y1 = r * 0.1;
      const x2 = sin(a) * r * 0.5;
      const y2 = r * 0.9;
      bezier(x1, y1, x1, y1 + r * 0.35, x2, y2 - r * 0.25, x2, y2);
    }
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Pulsing swim speed const pulsePhase = (sin(t) + 1) / 2; const swimSpeed = lerp(this.minSpeed, this.maxSpeed, pulsePhase);

Uses a sine wave to make the jelly pulse between min and max swim speeds, creating lifelike undulating motion

calculation Perlin noise horizontal drift const n = noise(this.noiseSeed, frameCount * this.horizontalDriftSpeed); const drift = map(n, 0, 1, -this.horizontalDriftAmount, this.horizontalDriftAmount);

Uses Perlin noise to create smooth, natural-looking left-right drifting (not random jitter)

conditional Exit screen and reward if (this.pos.y < -this.baseSize * 4) { this.pos.y = height + random(...); coins += floor(this.baseSize * 0.5); }

When jelly floats off the top, it respawns at the bottom and awards coins proportional to its size

for-loop Motion trail rendering for (let i = 0; i < this.trail.length; i++) { ellipse(...); }

Draws fading circles at past positions to show the jelly's path and create a ghostly motion effect

for-loop Tentacle wiggles for (let i = 0; i < this.numTentacles; i++) { for (let j = 1; j <= segments; j++) { line(...); } }

For each tentacle, draws 18 line segments that sway using frame-dependent sine waves for realistic fluttering

this.pos = createVector(x, y);
Stores the jelly's position as a p5.Vector so we can easily add/subtract movement
this.minSpeed = random(0.15, 0.35);
Each jelly gets a random minimum swim speed—jellies with low minSpeed drift lazily, high minSpeed jellies are more energetic
this.trail.push(this.pos.copy());
Every frame, adds a copy of the current position to the trail array (not a reference, a copy, so the jelly can move without changing past trail points)
const t = frameCount * this.pulseSpeed + this.pulseOffset;
Creates a unique time value for this jelly based on frameCount and its own pulseSpeed—each jelly pulses at a different speed with its own offset
const pulsePhase = (sin(t) + 1) / 2;
Converts the sine wave (which ranges -1 to +1) into a 0–1 range so it can be used as a lerp() weight
this.bellScale = 1 + 0.18 * sin(t);
Makes the bell size pulse between 0.82× and 1.18× its normal size, creating a breathing/pulsing animation
const swimSpeed = lerp(this.minSpeed, this.maxSpeed, pulsePhase);
Interpolates between the jelly's min and max swim speeds using the pulsing value—when pulsePhase is 0, swimSpeed = minSpeed; when 1, swimSpeed = maxSpeed
this.pos.y -= swimSpeed;
Moves the jelly upward by reducing its y position each frame—negative y is up on the canvas
const n = noise(this.noiseSeed, frameCount * this.horizontalDriftSpeed);
Generates a Perlin noise value using a fixed seed (so each jelly has consistent drift) and frame-based time (so it drifts smoothly, not randomly)
const drift = map(n, 0, 1, -this.horizontalDriftAmount, this.horizontalDriftAmount);
Converts the noise value (0–1) into a left-right drift offset (-driftAmount to +driftAmount pixels)
this.currentX = this.pos.x + drift;
Adds the drift to the jelly's x position for display, but doesn't change pos.x—so the drift is purely visual, not affecting collision
if (this.pos.y < -this.baseSize * 4) {
Checks if the jelly has drifted so far above the canvas (past -baseSize * 4 pixels) that it's completely off-screen
coins += floor(this.baseSize * 0.5);
Awards coins equal to half the jelly's size (rounded down)—larger jellies are worth more, rewarding the player for keeping big ones alive
for (let i = 0; i < this.trail.length; i++) { const alpha = 100 - i * this.trailAlphaReduction;
Loops through each point in the trail, calculating its alpha (transparency) so older points (higher index) are more transparent
const trailSize = map(i, 0, this.trail.length, 0.8 * r, 0.2 * r);
Makes the trail circles shrink as they get older—first circle is 80% of jelly size, last circle is 20%, creating a fading-into-distance effect

Fish.update() and Fish.display()

Fish are simpler than jellies but teach the same principles: horizontal wrapping (with screen edges), vertical oscillation using sine waves, and sprite flipping via scale(direction, 1). Fish are hunted by predators, so they don't earn coins—instead, they're the prey that predators will eat in update() methods that scan the fishList array.

class Fish {
  constructor(x, y, size, hue, direction) {
    this.pos = createVector(x, y);
    this.size = size;
    this.hue = hue;
    this.direction = direction;
    this.speed = random(0.8, 2.5);
    this.yOffset = y;
    this.yAmplitude = random(5, 15);
    this.wiggleSpeed = random(0.05, 0.15);
    this.bodyWiggleOffset = random(TWO_PI);
  }

  update() {
    this.pos.x += this.speed * this.direction;
    this.pos.y = this.yOffset + sin(frameCount * this.wiggleSpeed) * this.yAmplitude;

    if (this.direction === 1 && this.pos.x > width + this.size * 2) {
      this.pos.x = -this.size * 2;
      this.yOffset = random(height * 0.1, height * 0.9);
      this.speed = random(0.8, 2.5);
    } else if (this.direction === -1 && this.pos.x < -this.size * 2) {
      this.pos.x = width + this.size * 2;
      this.yOffset = random(height * 0.1, height * 0.9);
      this.speed = random(0.8, 2.5);
    }
  }

  display() {
    push();
    translate(this.pos.x, this.pos.y);
    scale(this.direction, 1);
    const bodyAngle = sin(frameCount * this.speed * 0.05 + this.bodyWiggleOffset) * 0.05;
    rotate(bodyAngle);

    fill(this.hue, 80, 90, 90);
    noStroke();
    ellipse(0, 0, this.size * 2, this.size);

    for (let i = -1; i <= 1; i += 0.5) {
      for (let j = -0.5; j <= 0.5; j += 0.25) {
        fill(this.hue, 75, 85, 10);
        ellipse(i * this.size * 0.7, j * this.size * 0.4, this.size * 0.2, this.size * 0.1);
      }
    }

    fill(this.hue, 70, 80, 90);
    beginShape();
    vertex(-this.size, 0);
    vertex(-this.size * 1.8, -this.size * 0.4);
    vertex(-this.size * 1.8, this.size * 0.4);
    endShape(CLOSE);

    beginShape();
    vertex(-this.size * 0.5, -this.size * 0.8);
    vertex(this.size * 0.5, -this.size * 0.8);
    vertex(this.size * 0.3, -this.size * 0.3);
    vertex(-this.size * 0.3, -this.size * 0.3);
    endShape(CLOSE);

    beginShape();
    vertex(this.size * 0.5, this.size * 0.3);
    vertex(this.size * 0.8, this.size * 0.6);
    vertex(this.size * 0.8, this.size * 0.1);
    endShape(CLOSE);

    beginShape();
    vertex(-this.size * 0.5, this.size * 0.4);
    vertex(-this.size * 0.2, this.size * 0.7);
    vertex(-this.size * 0.2, this.size * 0.2);
    endShape(CLOSE);

    stroke(this.hue, 80, 60, 80);
    strokeWeight(1);
    noFill();
    arc(this.size * 0.8, 0, this.size * 0.3, this.size * 0.6, PI / 2, -PI / 2);

    fill(0);
    noStroke();
    ellipse(this.size * 0.6, -this.size * 0.2, this.size * 0.2, this.size * 0.2);

    pop();
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Horizontal swimming this.pos.x += this.speed * this.direction;

Moves the fish left or right each frame based on its speed and direction (1 = right, -1 = left)

calculation Vertical sine-wave bobbing this.pos.y = this.yOffset + sin(frameCount * this.wiggleSpeed) * this.yAmplitude;

Makes the fish bob up and down smoothly using a sine wave that oscillates around its yOffset

conditional Screen wrapping and reset if (this.direction === 1 && this.pos.x > width + this.size * 2) { this.pos.x = -this.size * 2; this.yOffset = random(...); this.speed = random(...); }

When fish exits the screen, teleport it to the opposite side, randomize its depth lane, and give it a new speed

calculation Body rotation animation const bodyAngle = sin(frameCount * this.speed * 0.05 + this.bodyWiggleOffset) * 0.05; rotate(bodyAngle);

Makes the fish tilt left and right slightly as it swims, synced to its speed

this.pos.x += this.speed * this.direction;
Adds (speed × direction) to x each frame—direction is 1 (right) or -1 (left), so multiplying by it flips the direction
this.pos.y = this.yOffset + sin(frameCount * this.wiggleSpeed) * this.yAmplitude;
Sets y to oscillate between (yOffset - yAmplitude) and (yOffset + yAmplitude) using a sine wave that updates with frameCount
if (this.direction === 1 && this.pos.x > width + this.size * 2) {
Checks if the fish is moving right (direction === 1) AND has passed the right edge of the canvas (x > width + buffer)
this.pos.x = -this.size * 2;
Teleports the fish to the left side of the screen, slightly off-canvas (negative x), ready to swim across again
this.yOffset = random(height * 0.1, height * 0.9);
Randomizes the fish's vertical lane when it wraps—it will cross at a different height next time
scale(this.direction, 1);
Flips the fish horizontally if direction === -1 (scaleX = -1 flips the x-axis, making it face left)
const bodyAngle = sin(frameCount * this.speed * 0.05 + this.bodyWiggleOffset) * 0.05;
Creates a rotation angle that oscillates between -0.05 and +0.05 radians (about ±3°)—faster fish (higher speed) tilt more rapidly
ellipse(0, 0, this.size * 2, this.size);
Draws the main fish body as an ellipse twice as wide as it is tall, centered at the origin (translate moved us to the fish's position)
for (let i = -1; i <= 1; i += 0.5) { for (let j = -0.5; j <= 0.5; j += 0.25) { ellipse(...); } }
Draws a grid of tiny, barely-visible ellipses on the body for scale texture—they have very low alpha (10) so they're subtle
fill(0); ellipse(this.size * 0.6, -this.size * 0.2, this.size * 0.2, this.size * 0.2);
Draws a black eye near the front of the fish's head

MantaRay.update() and MantaRay.applyGravityGulp()

MantaRay is the most complex creature, demonstrating state machines (idle/gulping), vector math (normalized directions), distance-based physics (pull decreases with distance), and advanced forces (radial pull + tangential swirl). The 'gravity gulp' is a boss-level ability that shows how to combine multiple force vectors to create emergent behavior: fish don't just move toward the manta—they spiral inward, shrink as they approach, and vanish when absorbed. The rage meter is a damage tracker that could trigger special attacks if extended.

🔬 This creates the vortex swirl. What happens if you remove these three lines entirely? The fish will still be pulled in, but won't spiral. Or what if you double this.swirlStrength to 2.4? The spiral becomes much tighter and faster.

      const swirlAmount = this.swirlStrength * (1 - dist / this.gulpRadius);
      f.pos.x += -ny * swirlAmount;
      f.pos.y += nx * swirlAmount;
class MantaRay {
  constructor(x, y, size, hue) {
    this.pos = createVector(x, y);
    this.baseX = x;
    this.baseY = y;
    this.size = size;
    this.hue = hue;
    this.bobAmp = size * 0.15;
    this.bobSpeed = 0.01 + random(0, 0.005);
    this.moveAmp = size * 2.0;
    this.moveSpeed = 0.0015 + random(0, 0.001);
    this.state = "idle";
    this.cooldown = floor(random(600, 900));
    this.gulpDuration = 180;
    this.gulpTimer = 0;
    this.gulpRadius = this.size * 6;
    this.absorbRadius = this.size * 0.9;
    this.maxPull = 4.0;
    this.minPull = 0.4;
    this.swirlStrength = 1.2;
    this.rage = 0;
    this.maxRage = 200;
  }

  update() {
    this.pos.x = this.baseX + sin(frameCount * this.moveSpeed) * this.moveAmp;
    this.pos.y = this.baseY + sin(frameCount * this.bobSpeed) * this.bobAmp;

    if (this.state === "idle") {
      this.cooldown--;
      if (this.cooldown <= 0) {
        this.state = "gulping";
        this.gulpTimer = this.gulpDuration;
      }
    } else if (this.state === "gulping") {
      this.applyGravityGulp();
      this.gulpTimer--;
      if (this.gulpTimer <= 0) {
        this.state = "idle";
        this.cooldown = floor(random(600, 900));
      }
    }
  }

  applyGravityGulp() {
    for (let i = fishList.length - 1; i >= 0; i--) {
      const f = fishList[i];
      const dx = this.pos.x - f.pos.x;
      const dy = this.pos.y - f.pos.y;
      const distSq = dx * dx + dy * dy;
      const dist = sqrt(distSq) + 0.0001;

      if (dist > this.gulpRadius) continue;

      const nx = dx / dist;
      const ny = dy / dist;

      const pull = map(dist, this.absorbRadius, this.gulpRadius, this.maxPull, this.minPull);
      const clampedPull = constrain(pull, this.minPull, this.maxPull);

      f.pos.x += nx * clampedPull;
      f.pos.y += ny * clampedPull;

      const swirlAmount = this.swirlStrength * (1 - dist / this.gulpRadius);
      f.pos.x += -ny * swirlAmount;
      f.pos.y += nx * swirlAmount;

      if (dist < this.gulpRadius * 0.7) {
        f.size *= 0.995;
        f.size = max(f.size, 4);
      }

      if (dist < this.absorbRadius) {
        this.rage = constrain(this.rage + f.size, 0, this.maxRage);
        fishList.splice(i, 1);
      }
    }
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Bobbing and horizontal sinusoidal motion this.pos.x = this.baseX + sin(frameCount * this.moveSpeed) * this.moveAmp; this.pos.y = this.baseY + sin(frameCount * this.bobSpeed) * this.bobAmp;

Makes the Manta Ray sway left-right and bob up-down around its base position using separate sine waves

conditional Idle/gulping state machine if (this.state === "idle") { this.cooldown--; if (this.cooldown <= 0) { this.state = "gulping"; this.gulpTimer = this.gulpDuration; } } else if (this.state === "gulping") { ... }

Alternates between idle (waiting) and gulping (feeding) states, tracking cooldown and gulpTimer to manage transitions

for-loop Gravity gulp suction force for (let i = fishList.length - 1; i >= 0; i--) { ... const pull = map(dist, this.absorbRadius, this.gulpRadius, this.maxPull, this.minPull); f.pos.x += nx * clampedPull; f.pos.y += ny * clampedPull; ... }

Pulls all nearby fish toward the Manta Ray's mouth using inverse-distance forces, creates a whirling vortex, shrinks fish, and absorbs them when close enough

this.pos.x = this.baseX + sin(frameCount * this.moveSpeed) * this.moveAmp;
Moves the Manta Ray left and right around its baseX by adding a sine wave oscillation—moveSpeed controls frequency, moveAmp controls amplitude
if (this.state === "idle") { this.cooldown--;
In idle state, count down toward the next gulp—when cooldown reaches 0, switch to gulping
const dx = this.pos.x - f.pos.x; const dy = this.pos.y - f.pos.y;
Calculates the vector from each fish to the Manta Ray (will be used for suction direction)
const dist = sqrt(distSq) + 0.0001;
Calculates the actual distance using sqrt of the squared distance (avoids calling dist() which is slower); adds 0.0001 to prevent division by zero
if (dist > this.gulpRadius) continue;
Skips this fish if it's outside the gulp radius—only fish close enough are affected
const nx = dx / dist; const ny = dy / dist;
Normalizes the direction vector so it has length 1—this gives us a unit direction toward the Manta Ray
const pull = map(dist, this.absorbRadius, this.gulpRadius, this.maxPull, this.minPull);
Maps distance to pull force: close fish (near absorbRadius) get maxPull (fast suction), far fish (near gulpRadius) get minPull (weak pull)
f.pos.x += nx * clampedPull; f.pos.y += ny * clampedPull;
Moves the fish toward the Manta Ray by adding the normalized direction × pull force
const swirlAmount = this.swirlStrength * (1 - dist / this.gulpRadius);
Calculates tangential (perpendicular) force that's strongest at the gulp edge and weakens toward the center—creates a rotating vortex
f.pos.x += -ny * swirlAmount; f.pos.y += nx * swirlAmount;
Adds the perpendicular force (rotated 90°: -ny, nx)—this makes fish spiral around the Manta Ray instead of just moving straight in
if (dist < this.gulpRadius * 0.7) { f.size *= 0.995;
When fish enter the middle zone of the gulp, they start shrinking (multiplied by 0.995 each frame) as they're being digested
if (dist < this.absorbRadius) { this.rage = constrain(this.rage + f.size, 0, this.maxRage); fishList.splice(i, 1); }
When fish reach the absorption radius (mouth), add their size to the rage meter, then remove them from fishList completely

📦 Key Variables

jellyfishList array

Holds all Jellyfish objects currently in the aquarium—updated every frame and drawn in additive blend mode

let jellyfishList = [];
particles array

Holds drifting background particles that float upward—spawned once in setup and recycled as they exit the top

let particles = [];
fishList array

Holds all Fish objects—the main prey species hunted by predators like Barracuda, VampireFish, and Manta Ray

let fishList = [];
coins number

Player's current coin balance—increased when creatures eat each other or exit the screen; decreased when buttons are pressed

let coins = 0;
bgLayer graphics buffer (p5.Graphics)

Off-screen buffer holding the static ocean background (gradient, light rays, vignette)—rendered once and reused every frame

let bgLayer;
glowOrbList array

Holds GlowOrb objects spawned by clicking in the water—they drift upward and fade out

let glowOrbList = [];
mantaRayList array

Holds the Manta Ray boss (max 1)—only appears after summoning via the shop button

let mantaRayList = [];
NUM_JELLYFISH number (constant)

How many jellyfish spawn when the sketch loads

const NUM_JELLYFISH = 14;
JELLYFISH_COST number (constant)

How many coins it costs to buy a jellyfish from the shop

const JELLYFISH_COST = 50;

🔧 Potential Improvements (7)

Here are some ways this code could be enhanced:

PERFORMANCE draw() function - button label updates

Every frame, all 12 buttons have their HTML label rewritten via .html() even though the text rarely changes (only changes when coins update or Manta Ray state changes). This is wasteful.

💡 Store the last coin count and only update labels when coins change: if (coins !== lastCoins) { buyJellyfishButton.html(...); lastCoins = coins; }

BUG Barracuda.update() - predator hunting

When a barracuda eats a fish, it does break after eating one, but the loop continues iterating from i-1. If multiple fish are at the same location, only one dies per frame.

💡 The break statement correctly exits the inner loop but this is acceptable behavior (one prey per frame). However, better would be to use a 'eaten' flag so the same fish can't be eaten by multiple predators in the same frame.

STYLE setup() function - purchase button declarations

12 nearly-identical purchase button blocks (buyJellyfishButton, buyFishButton, etc.) are declared manually. The code is extremely repetitive.

💡 Refactor into an array of creature types with metadata (name, cost, list, createFunction), then loop through to create buttons dynamically. This would reduce 500+ lines of boilerplate to ~50.

FEATURE Global - creature interactions

Creatures never interact with each other beyond predation. Jellies don't collide with fish, whales ignore barracudas, etc. The ecosystem feels like separate species groups rather than one unified world.

💡 Add collision shapes and avoidance behaviors—fish could try to swim away from large predators, creatures could bunch up in safe zones, larger creatures could protect smaller ones.

BUG mousePressed() function - UI exclusion

The hardcoded boundary check (mouseX < 200) assumes buttons stay in a 200-pixel-wide left column. If the browser resizes or buttons are repositioned, clicks might trigger unwanted glow orbs over buttons.

💡 Store button positions dynamically and check bounds: mouseX < (buyJellyfishButton.position().x + buyJellyfishButton.width) instead of hardcoding 200.

PERFORMANCE GoliathTigerfish.update() - predator scanning

GoliathTigerfish scans 5 different creature arrays (fishList, barracudaList, etc.) every frame. With multiple goliaths, this is O(n²) nested iteration.

💡 Create a 'preyLists' array computed once per frame containing all prey, so each predator only iterates once: for (let prey of preyLists) instead of 5 separate loops.

STYLE All creature classes - constructor initialization

Most creatures randomly initialize 8–12 parameters (speed, wiggleSpeed, amplitude, phase offsets, etc.). The constructors are dense and hard to read.

💡 Extract creature parameter ranges into a config object: const FISH_CONFIG = { speedRange: [0.8, 2.5], wiggleSpeedRange: [0.05, 0.15], ... } then use it to keep constructors DRY and centralizing tuning.

🔄 Code Flow

Code flow showing setup, draw, mousePressed, drawBackground, jellyfish, fish, mantaray

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> background-draw[background-draw] draw --> depth-loop-order[depth-loop-order] draw --> coins-display[coins-display] draw --> button-label-update[button-label-update] draw --> ui-exclusion-check[ui-exclusion-check] draw --> glow-orb-spawn[glow-orb-spawn] draw --> jellyfish-additive-blend[jellyfish-additive-blend] draw --> jellyfish[jellyfish] draw --> fish[fish] draw --> mantaray[mantaray] setup --> canvas-creation[canvas-creation] setup --> creature-spawn-loops[creature-spawn-loops] setup --> purchase-buttons[purchase-buttons] setup --> drawBackground[drawBackground] setup --> colormode-switch[colormode-switch] background-draw --> gradient-background[gradient-background] background-draw --> light-rays[light-rays] background-draw --> vignette[vignette] jellyfish --> jelly-pulsing-movement[jelly-pulsing-movement] jellyfish --> jelly-horizontal-drift[jelly-horizontal-drift] jellyfish --> jelly-wrap-coin[jelly-wrap-coin] jellyfish --> jelly-trail-draw[jelly-trail-draw] jellyfish --> jelly-tentacle-draw[jelly-tentacle-draw] fish --> fish-horizontal-movement[fish-horizontal-movement] fish --> fish-vertical-wiggle[fish-vertical-wiggle] fish --> fish-screen-wrap[fish-screen-wrap] fish --> fish-body-tilt[fish-body-tilt] mantaray --> manta-movement[manta-movement] mantaray --> manta-state-machine[manta-state-machine] mantaray --> manta-gravity-gulp[manta-gravity-gulp] click setup href "#fn-setup" click draw href "#fn-draw" click background-draw href "#sub-background-draw" click depth-loop-order href "#sub-depth-loop-order" click coins-display href "#sub-coins-display" click button-label-update href "#sub-button-label-update" click ui-exclusion-check href "#sub-ui-exclusion-check" click glow-orb-spawn href "#sub-glow-orb-spawn" click jellyfish-additive-blend href "#sub-jellyfish-additive-blend" click jellyfish href "#fn-jellyfish" click fish href "#fn-fish" click mantaray href "#fn-mantaray" click canvas-creation href "#sub-canvas-creation" click creature-spawn-loops href "#sub-creature-spawn-loops" click purchase-buttons href "#sub-purchase-buttons" click drawBackground href "#fn-drawBackground" click colormode-switch href "#sub-colormode-switch" click gradient-background href "#sub-gradient-background" click light-rays href "#sub-light-rays" click vignette href "#sub-vignette" click jelly-pulsing-movement href "#sub-jelly-pulsing-movement" click jelly-horizontal-drift href "#sub-jelly-horizontal-drift" click jelly-wrap-coin href "#sub-jelly-wrap-coin" click jelly-trail-draw href "#sub-jelly-trail-draw" click jelly-tentacle-draw href "#sub-jelly-tentacle-draw" click fish-horizontal-movement href "#sub-fish-horizontal-movement" click fish-vertical-wiggle href "#sub-fish-vertical-wiggle" click fish-screen-wrap href "#sub-fish-screen-wrap" click fish-body-tilt href "#sub-fish-body-tilt" click manta-movement href "#sub-manta-movement" click manta-state-machine href "#sub-manta-state-machine" click manta-gravity-gulp href "#sub-manta-gravity-gulp"

❓ Frequently Asked Questions

What visual experience does the Aquarium Attempt sketch provide?

The Aquarium Attempt sketch creates a vibrant underwater scene filled with various aquatic creatures like jellyfish, fish, and other marine life, all animated to simulate a lively aquarium environment.

How can users interact with the Aquarium Attempt sketch?

Users can interact with the sketch by clicking to spawn glow orbs and using in-game currency to purchase different types of marine animals, enhancing the aquarium experience.

What creative coding concepts are showcased in the Aquarium Attempt sketch?

This sketch demonstrates concepts such as object-oriented programming through the use of arrays for different marine creatures and interactive elements that respond to user input.

Preview

Aquarium Attempt - xelsed.ai (Remix) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Aquarium Attempt - xelsed.ai (Remix) - Code flow showing setup, draw, mousePressed, drawBackground, jellyfish, fish, mantaray
Code Flow Diagram