weird birds flocking

This sketch creates a mesmerizing infinite scrolling landscape filled with procedurally generated biomes—water, sand, forests, and mountains—while a flock of birds uses flocking behavior to glide, swirl, and follow your mouse across the ever-changing world. The combination of Perlin noise world generation, smooth scrolling, and Craig Reynolds' boid flocking algorithm produces a relaxing, living ecosystem.

🧪 Try This!

Experiment with the code by making these changes:

  1. Triple the bird count — Increase flockSize to 300 to see a much denser, more chaotic flock with stronger emergent swirling patterns.
  2. Disable mouse influence — Set mouseForce weight to 0 so the birds ignore your cursor and just flock with each other.
  3. Make biomes change twice as fast — Reduce sectionHeight so biome regions are narrower and change more frequently as the world scrolls.
  4. Slow the world scroll — Lower scrollSpeed to 0.3 so the landscape drifts by slowly, giving the scene a more meditative pace.
  5. Make birds bigger — Increase the bird size from 10 to 25 pixels so the wedge-shaped birds are more visible.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a hypnotic infinite world: a scrolling landscape that never repeats, populated by white birds that dart and swirl in coordinated flocks. The visual magic comes from three techniques working together. First, Perlin noise procedurally generates biomes—sky, water, sand, forests, and mountains—that flow seamlessly downward. Second, the Boid class implements Craig Reynolds' classic flocking algorithm, where each bird follows simple steering rules (separation, alignment, cohesion) that combine into emergent group behavior. Third, a clever scrolling system using p5.Graphics buffers and `copy()` creates the illusion of an infinite world without loading new data—the top section scrolls off and gets replaced with newly generated content at the bottom.

The code is organized into three layers: a scrolling world generator (setup, draw, generateWorldPattern, drawBiomeSection, getBiomeType), an infinite buffer management system (windowResized), and a Boid class with flocking behaviors (separation, alignment, cohesion, seek, flock). By studying this sketch you will learn how Perlin noise creates coherent procedural landscapes, how the boid algorithm can fake intelligence without explicit instructions, how to buffer and scroll graphics efficiently, and how vector math drives physics-like animation.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas and initializes a p5.Graphics buffer called worldGraphics that is three times the screen height—this triple-height buffer is key to smooth scrolling. It draws an initial world pattern onto this buffer using Perlin noise to decide where each biome goes, and creates 100 Boid objects (the birds).
  2. Every frame, draw() increments scrollOffset and calls image() to display the worldGraphics buffer shifted up by scrollOffset pixels, creating upward scrolling motion.
  3. Once scrollOffset reaches the screen height, the code copies the middle and bottom thirds of worldGraphics upward (discarding the top third that scrolled off), generates a brand new biome section in the bottom third using generateWorldPattern and Perlin noise, resets scrollOffset to 0, and repeats—this is how the world never ends.
  4. Meanwhile, each Boid is updated: the flock() method calculates three steering forces (separation to avoid neighbors, alignment to match their headings, cohesion to move toward their average position) plus a weak seek force toward the mouse. These forces are weighted and applied to the bird's acceleration, which updates velocity and position each frame.
  5. The boid's show() method rotates and draws a small wedge shape pointing in the direction the bird is moving, creating the illusion that the white triangles are alive and purposeful.
  6. Finally, edges() wraps each bird around the screen boundaries, so birds that fly off the left side reappear on the right, and vice versa, keeping them forever dancing within the visible canvas.

🎓 Concepts You'll Learn

Perlin noise and procedural generationBoid flocking and steering behaviorsVector math and forcesGraphics buffering and efficient scrollingTransformations and coordinate systemsEmergent behavior from simple rules

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It prepares the canvas, initializes all variables and colors, creates the off-screen graphics buffer, and populates the boids array. The worldHeight = height * 3 trick is clever: a three-part buffer lets the code discard the top third that scrolled off-screen and regenerate the bottom third with new biomes without disrupting the visible animation.

function setup() {
  createCanvas(windowWidth, windowHeight);
  skyColor = color(135, 206, 235);
  waterColor = color(0, 191, 255);
  sandColor = color(244, 164, 96);
  forestColor = color(34, 139, 34);
  mountainColor = color(139, 69, 19);
  worldHeight = height * 3;
  worldGraphics = createGraphics(width, worldHeight);
  generateWorldPattern(worldGraphics, 0, worldHeight, 0);
  for (let i = 0; i < flockSize; i++) {
    boids.push(new Boid());
  }
  userStartAudio();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Initialize biome colors skyColor = color(135, 206, 235);

Defines the color palette used throughout generateWorldPattern to paint each biome

calculation Create graphics buffer worldGraphics = createGraphics(width, worldHeight);

Creates an off-screen drawing surface three times the canvas height, enabling smooth infinite scrolling

for-loop Populate boid array for (let i = 0; i < flockSize; i++) {

Creates 100 Boid objects and adds each to the boids array, populating the flock

createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that stretches to fill the entire browser window
skyColor = color(135, 206, 235);
Stores a sky-blue color as a p5.Color object so it can be reused in the biome drawing functions
waterColor = color(0, 191, 255);
Stores a deep blue color for water biomes
sandColor = color(244, 164, 96);
Stores a sandy brown color for beach biomes
forestColor = color(34, 139, 34);
Stores a forest green color for tree biomes
mountainColor = color(139, 69, 19);
Stores a mountain brown color for peak biomes
worldHeight = height * 3;
Sets the buffer height to 3 times the screen height; the top third scrolls off, middle becomes top, bottom gets new content
worldGraphics = createGraphics(width, worldHeight);
Creates an off-screen p5.Graphics buffer (invisible drawing surface) the size of the world buffer—this is where the landscape is drawn and scrolled
generateWorldPattern(worldGraphics, 0, worldHeight, 0);
Draws the initial biome landscape onto worldGraphics from top (y=0) to bottom (y=worldHeight)
for (let i = 0; i < flockSize; i++) {
Loop that repeats flockSize times (100 by default)
boids.push(new Boid());
Creates a new Boid object and adds it to the boids array; each iteration creates one bird
userStartAudio();
Initializes the p5.sound library; required so audio can play after user interaction (not used in this sketch but good practice)

draw()

draw() is called 60 times per second and is the heartbeat of the entire sketch. Each frame it increments scrollOffset to move the world upward, checks if a regeneration is needed, draws the worldGraphics buffer to the screen at the offset position, and updates and renders every boid. The clever buffer-shifting technique (copy, regenerate, reset) ensures the world never ends without loading external data—it's all procedurally generated on the fly using Perlin noise.

function draw() {
  scrollOffset += scrollSpeed;
  if (scrollOffset >= height) {
    worldGraphics.copy(worldGraphics, 0, height, width, worldHeight - height, 0, 0, width, worldHeight - height);
    worldNoiseOffset += height * noiseScale;
    generateWorldPattern(worldGraphics, worldHeight - height, worldHeight, worldNoiseOffset);
    scrollOffset = 0;
  }
  image(worldGraphics, 0, -scrollOffset);
  for (let boid of boids) {
    boid.flock(boids);
    boid.update();
    boid.edges();
    boid.show();
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Update scroll position scrollOffset += scrollSpeed;

Increments the scroll position each frame, driving the upward movement of the world

conditional Regenerate world when scrolled if (scrollOffset >= height) {

Detects when the top third of the buffer has scrolled off-screen and triggers a regeneration cycle

calculation Shift buffer contents up worldGraphics.copy(worldGraphics, 0, height, width, worldHeight - height, 0, 0, width, worldHeight - height);

Copies the middle and bottom thirds of the buffer upward, discarding the top third and making room for new biomes

for-loop Update and render all boids for (let boid of boids) {

Iterates through every bird, updating its position and drawing it to the screen

scrollOffset += scrollSpeed;
Adds scrollSpeed (default 1) to scrollOffset each frame, so scrollOffset increases from 0 to height over 400 frames (at 60 fps)
if (scrollOffset >= height) {
When scrollOffset reaches the canvas height, the top third of worldGraphics has scrolled off-screen and needs to be replaced with new content
worldGraphics.copy(worldGraphics, 0, height, width, worldHeight - height, 0, 0, width, worldHeight - height);
Copies pixels from the middle and bottom thirds of worldGraphics (starting at y=height) and pastes them at the top (y=0), shifting all content upward and discarding what scrolled off
worldNoiseOffset += height * noiseScale;
Increments the noise offset by a value proportional to the scrolled distance; this keeps Perlin noise continuous across regenerated sections so biomes don't jump or repeat
generateWorldPattern(worldGraphics, worldHeight - height, worldHeight, worldNoiseOffset);
Draws a completely new biome pattern into the bottom third of worldGraphics (from y=worldHeight-height to y=worldHeight) using the updated noise offset
scrollOffset = 0;
Resets scrollOffset to 0 so the next scroll cycle can begin; the world now seamlessly continues scrolling upward
image(worldGraphics, 0, -scrollOffset);
Draws the worldGraphics buffer to the screen at position (0, -scrollOffset); the negative offset moves the entire world up, creating the scrolling illusion
for (let boid of boids) {
Loops through every Boid object in the boids array (100 by default)
boid.flock(boids);
Calls the flock method on this boid, which calculates steering forces from nearby birds and the mouse, preparing the boid to move
boid.update();
Updates the boid's position and velocity based on the acceleration calculated by flock()
boid.edges();
Wraps the boid around screen boundaries so birds that fly off one edge reappear on the opposite edge
boid.show();
Draws the boid as a small white wedge triangle pointing in its direction of motion

generateWorldPattern()

generateWorldPattern() is the procedural engine of the world. It uses Perlin noise—a smooth, coherent random function—to decide which biome appears at each vertical position. By sampling noise at a fixed x coordinate but varying y, it creates horizontal bands of biomes. The noiseYOffset parameter is crucial: it shifts the noise input based on how far the world has scrolled, ensuring new regenerated sections connect seamlessly to what scrolled off the top. This is why the world never repeats or has seams.

function generateWorldPattern(g, startY, endY, noiseYOffset) {
  g.noStroke();
  let sectionHeight = 50;
  for (let y = startY; y < endY; y += sectionHeight) {
    let biomeNoise = noise(width * noiseScale, (y + noiseYOffset) * noiseScale);
    let currentBiome = getBiomeType(biomeNoise);
    drawBiomeSection(g, 0, y, width, min(sectionHeight, endY - y), currentBiome, noiseYOffset);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Iterate through vertical sections for (let y = startY; y < endY; y += sectionHeight) {

Divides the area from startY to endY into 50-pixel-tall chunks, generating a biome for each

calculation Sample noise and pick biome let currentBiome = getBiomeType(biomeNoise);

Converts the Perlin noise value into a biome type (water, sand, forest, or mountains)

function generateWorldPattern(g, startY, endY, noiseYOffset) {
Takes a p5.Graphics buffer g and generates biome sections from y=startY to y=endY, using noiseYOffset to ensure continuity across regeneration cycles
g.noStroke();
Tells the graphics buffer to draw without strokes (outlines)—only fill colors will be visible
let sectionHeight = 50;
Divides the world into 50-pixel-tall horizontal strips; each strip is assigned one biome, creating visible horizontal biome bands
for (let y = startY; y < endY; y += sectionHeight) {
Loop that runs from startY to endY in 50-pixel increments, processing each strip
let biomeNoise = noise(width * noiseScale, (y + noiseYOffset) * noiseScale);
Samples Perlin noise at a single x coordinate (width * noiseScale, which is a fixed vertical slice) and a y coordinate that varies with position and includes noiseYOffset for continuity; returns a value 0–1
let currentBiome = getBiomeType(biomeNoise);
Passes the noise value to getBiomeType() which returns a string like 'water', 'sand', 'forest', or 'mountains' based on thresholds
drawBiomeSection(g, 0, y, width, min(sectionHeight, endY - y), currentBiome, noiseYOffset);
Calls drawBiomeSection to actually render this biome strip onto the graphics buffer from x=0 to x=width and from y=y to y=y+height

getBiomeType()

getBiomeType() is a simple threshold function that converts a continuous Perlin noise value into one of four discrete biome categories. By adjusting the thresholds (0.3, 0.5, 0.7), you can control the distribution of biomes—how much of the world is water, sand, forest, or mountains. This is a common procedural generation pattern: sample smooth noise and bucket it into discrete regions.

function getBiomeType(noiseValue) {
  if (noiseValue < 0.3) {
    return 'water';
  } else if (noiseValue < 0.5) {
    return 'sand';
  } else if (noiseValue < 0.7) {
    return 'forest';
  } else {
    return 'mountains';
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Noise-to-biome mapping if (noiseValue < 0.3) {

Maps the continuous noise range 0–1 into four discrete biome categories using threshold values

function getBiomeType(noiseValue) {
Takes a Perlin noise value (0–1) and returns a string describing which biome it represents
if (noiseValue < 0.3) {
If noise is in the range 0.0–0.3, assign water biome
return 'water';
Returns the string 'water' so drawBiomeSection knows to draw water
} else if (noiseValue < 0.5) {
Else if noise is in the range 0.3–0.5, assign sand biome
return 'sand';
Returns the string 'sand'
} else if (noiseValue < 0.7) {
Else if noise is in the range 0.5–0.7, assign forest biome
return 'forest';
Returns the string 'forest'
} else {
Else if noise is in the range 0.7–1.0, assign mountains biome
return 'mountains';
Returns the string 'mountains'

drawBiomeSection()

drawBiomeSection() is where the visual magic happens. Each biome is drawn differently: water uses a color gradient and ripple lines, sand uses solid fill and grain lines, forest places random trees with trunks and canopies, and mountains use Perlin noise to create jagged peaks that feel natural. All the noise-based details (ripples, grains, peaks) use noiseYOffset to stay coherent across scrolling regenerations. This function shows how to use a single switch statement to dispatch four completely different drawing routines based on the biome type.

🔬 This loop draws water as a gradient from dark blue at top to light blue at bottom, creating depth. What happens if you swap c1 and c2 in the lerpColor call? The gradient will reverse—dark will be at the bottom and light at the top. Try it and see if the water looks inverted or wrong.

      for (let row = 0; row < h; row++) {
        let inter = map(row, 0, h, 0, 1);
        let c = lerpColor(c1, c2, inter);
        g.stroke(c);
        g.line(x, y + row, x + w, y + row);
      }
function drawBiomeSection(g, x, y, w, h, biomeType, noiseYOffset) {
  g.noStroke();
  switch (biomeType) {
    case 'water':
      let c1 = waterColor;
      let c2 = color(100, 149, 237);
      for (let row = 0; row < h; row++) {
        let inter = map(row, 0, h, 0, 1);
        let c = lerpColor(c1, c2, inter);
        g.stroke(c);
        g.line(x, y + row, x + w, y + row);
      }
      g.stroke(color(waterColor, 100));
      for (let i = x; i < x + w; i += 5) {
        let n = noise(i * noiseScale, (y + h * 0.5 + noiseYOffset) * noiseScale);
        let yOffset = map(n, 0, 1, -5, 5);
        g.line(i, y + h * 0.5 + yOffset, i, y + h);
      }
      break;
    case 'sand':
      g.fill(sandColor);
      g.rect(x, y, w, h);
      g.stroke(sandColor);
      for (let i = x; i < x + w; i += 5) {
        let n = noise(i * noiseScale, (y + h * 0.8 + noiseYOffset) * noiseScale);
        let yOffset = map(n, 0, 1, -10, 10);
        g.line(i, y + h * 0.8 + yOffset, i, y + h);
      }
      break;
    case 'forest':
      g.fill(forestColor);
      g.rect(x, y, w, h);
      g.stroke(color(0, 100, 0));
      g.strokeWeight(2);
      g.fill(color(0, 150, 0));
      for (let i = x + 10; i < x + w - 10; i += random(20, 50)) {
        let treeHeight = random(20, min(50, h - 10));
        let treeY = y + h - treeHeight;
        g.line(i, treeY, i, y + h);
        g.ellipse(i, treeY, random(10, 20), random(10, 20));
      }
      g.strokeWeight(1);
      break;
    case 'mountains':
      g.fill(mountainColor);
      g.rect(x, y, w, h);
      g.fill(color(150, 75, 0));
      g.beginShape();
      g.vertex(x, y + h);
      for (let i = x; i <= x + w; i += 10) {
        let mountainNoise = noise(i * noiseScale, (y + noiseYOffset) * noiseScale);
        let peakHeight = map(mountainNoise, 0, 1, h * 0.2, h * 0.8);
        g.vertex(i, y + h - peakHeight);
      }
      g.vertex(x + w, y + h);
      g.endShape(CLOSE);
      break;
  }
}
Line-by-line explanation (48 lines)

🔧 Subcomponents:

for-loop Water gradient loop for (let row = 0; row < h; row++) {

Draws horizontal lines with a gradient from dark blue to light blue, creating water depth

for-loop Water ripple overlay for (let i = x; i < x + w; i += 5) {

Adds vertical ripple lines using Perlin noise to create texture on the water surface

for-loop Sand grain texture for (let i = x; i < x + w; i += 5) {

Adds vertical grainy lines to the sand to simulate sandy texture

for-loop Random tree placement for (let i = x + 10; i < x + w - 10; i += random(20, 50)) {

Places trees at random horizontal intervals across the forest biome

for-loop Mountain profile loop for (let i = x; i <= x + w; i += 10) {

Draws mountain peak vertices using Perlin noise to create natural-looking jagged peaks

function drawBiomeSection(g, x, y, w, h, biomeType, noiseYOffset) {
Draws a rectangular biome section onto graphics buffer g at position (x, y) with width w and height h; biomeType determines which visual style to use
g.noStroke();
Sets the graphics buffer to draw without outlines
switch (biomeType) {
Selects different drawing code based on which biome type (water, sand, forest, mountains) is being drawn
case 'water':
Handles the water biome case
let c1 = waterColor;
Stores the deep blue water color
let c2 = color(100, 149, 237);
Defines a lighter blue for shallow water
for (let row = 0; row < h; row++) {
Loop through each row of pixels in the water section from top to bottom
let inter = map(row, 0, h, 0, 1);
Maps the row position (0 to h) to a value 0–1 representing how far down the section we are
let c = lerpColor(c1, c2, inter);
Interpolates between dark blue (c1) and light blue (c2) based on inter; at top it's dark, at bottom it's light, simulating water depth
g.stroke(c);
Sets the stroke color for the line about to be drawn
g.line(x, y + row, x + w, y + row);
Draws a horizontal line across the full width at this row, using the interpolated color
g.stroke(color(waterColor, 100));
Sets stroke to semi-transparent water blue (alpha=100) for the ripple effect
for (let i = x; i < x + w; i += 5) {
Loop through x coordinates at 5-pixel intervals to place ripple lines
let n = noise(i * noiseScale, (y + h * 0.5 + noiseYOffset) * noiseScale);
Samples Perlin noise at the horizontal position i and vertical position y+h*0.5 (middle of water section) to determine ripple variation
let yOffset = map(n, 0, 1, -5, 5);
Maps the noise value to a vertical offset between -5 and +5 pixels
g.line(i, y + h * 0.5 + yOffset, i, y + h);
Draws a vertical ripple line from the middle of the water section downward, offset slightly by the noise value
case 'sand':
Handles the sand biome case
g.fill(sandColor);
Sets fill color to sandy brown
g.rect(x, y, w, h);
Draws a solid sandy rectangle filling the entire section
g.stroke(sandColor);
Sets stroke to the same sandy color
for (let i = x; i < x + w; i += 5) {
Loop through x coordinates at 5-pixel intervals to place grain lines
let n = noise(i * noiseScale, (y + h * 0.8 + noiseYOffset) * noiseScale);
Samples Perlin noise at this position to determine sand grain texture
let yOffset = map(n, 0, 1, -10, 10);
Maps noise to a larger vertical offset (-10 to +10) than water, creating grainier texture
g.line(i, y + h * 0.8 + yOffset, i, y + h);
Draws a vertical grain line in the bottom 20% of the sand section
case 'forest':
Handles the forest biome case
g.fill(forestColor);
Sets fill to forest green
g.rect(x, y, w, h);
Fills the entire section with solid green
g.stroke(color(0, 100, 0));
Sets stroke to dark green for tree trunks
g.strokeWeight(2);
Makes tree trunks 2 pixels wide
g.fill(color(0, 150, 0));
Sets fill to lighter green for tree foliage
for (let i = x + 10; i < x + w - 10; i += random(20, 50)) {
Places trees at random horizontal positions with random spacing (20–50 pixels apart), inset 10 pixels from edges
let treeHeight = random(20, min(50, h - 10));
Randomizes each tree's height between 20 and 50 pixels (or less if the section is very small)
let treeY = y + h - treeHeight;
Calculates where the tree top should start (at the bottom of the section minus the tree height)
g.line(i, treeY, i, y + h);
Draws a vertical line (tree trunk) from the top to the bottom of the tree
g.ellipse(i, treeY, random(10, 20), random(10, 20));
Draws a circle (tree canopy) at the top of the trunk with random width and height (10–20 pixels)
g.strokeWeight(1);
Resets stroke weight to 1 pixel for subsequent drawing
case 'mountains':
Handles the mountains biome case
g.fill(mountainColor);
Sets fill to mountain brown
g.rect(x, y, w, h);
Fills the entire section with solid brown base
g.fill(color(150, 75, 0));
Sets fill to lighter brown for mountain peaks
g.beginShape();
Starts a new shape (polygon) for the mountain silhouette
g.vertex(x, y + h);
Adds the bottom-left corner vertex
for (let i = x; i <= x + w; i += 10) {
Loop through x coordinates at 10-pixel intervals to place mountain peak vertices
let mountainNoise = noise(i * noiseScale, (y + noiseYOffset) * noiseScale);
Samples Perlin noise at this x position and section y position to determine peak height
let peakHeight = map(mountainNoise, 0, 1, h * 0.2, h * 0.8);
Maps noise to a height between 20% and 80% of the section, creating natural-looking peaks
g.vertex(i, y + h - peakHeight);
Adds a vertex at this x position and height, forming the mountain profile
g.vertex(x + w, y + h);
Adds the bottom-right corner vertex to close the shape
g.endShape(CLOSE);
Closes the shape (connects last vertex to first) and fills it with the mountain color

windowResized()

windowResized() is a p5.js lifecycle function that fires automatically when the window size changes. It ensures the canvas and world buffer scale gracefully. By preserving worldNoiseOffset, the biome pattern remains coherent even after resizing—the world doesn't restart or jump.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  worldHeight = height * 3;
  worldGraphics = createGraphics(width, worldHeight);
  generateWorldPattern(worldGraphics, 0, worldHeight, worldNoiseOffset);
  scrollOffset = 0;
}
Line-by-line explanation (6 lines)
function windowResized() {
This function is called automatically by p5.js whenever the browser window is resized
resizeCanvas(windowWidth, windowHeight);
Resizes the main canvas to fill the new window size
worldHeight = height * 3;
Recalculates worldHeight based on the new canvas height; the triple-height buffer scales with the window
worldGraphics = createGraphics(width, worldHeight);
Destroys the old worldGraphics buffer and creates a new one matching the new canvas dimensions
generateWorldPattern(worldGraphics, 0, worldHeight, worldNoiseOffset);
Regenerates the entire world pattern onto the new buffer, preserving the current worldNoiseOffset so the biome pattern stays continuous
scrollOffset = 0;
Resets scrollOffset to prevent visual jumping when the window first resizes

class Boid

The Boid class implements Craig Reynolds' classic boid flocking algorithm. Each bird has position, velocity, and acceleration. Every frame, flock() calculates three forces—separation (stay apart), alignment (match neighbors' heading), and cohesion (move toward neighbors)—plus a mouse-seeking force. These forces accumulate in acceleration, which updates velocity, which updates position. The result is emergent group behavior: no bird knows the flock's goals, yet they swirl and dance in perfect coordination. The edges() method wraps birds around screen boundaries, creating a toroidal world. This is a masterclass in how simple steering rules create the illusion of complex, lifelike intelligence.

class Boid {
  constructor() {
    this.position = createVector(random(width), random(height));
    this.velocity = p5.Vector.random2D();
    this.velocity.setMag(random(2, 4));
    this.acceleration = createVector();
    this.maxForce = 0.2;
    this.maxSpeed = 4;
    this.size = 10;
  }

  show() {
    strokeWeight(1);
    stroke(255);
    fill(255, 100);
    push();
    translate(this.position.x, this.position.y);
    rotate(this.velocity.heading());
    beginShape();
    vertex(this.size / 2, 0);
    vertex(-this.size / 2, this.size / 4);
    vertex(-this.size / 2, -this.size / 4);
    endShape(CLOSE);
    pop();
  }

  update() {
    this.position.add(this.velocity);
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    this.acceleration.mult(0);
  }

  applyForce(force) {
    this.acceleration.add(force);
  }

  flock(boids) {
    let separationForce = this.separation(boids);
    let alignmentForce = this.alignment(boids);
    let cohesionForce = this.cohesion(boids);
    let mouseForce = this.seek(createVector(mouseX, mouseY));

    separationForce.mult(1.5);
    alignmentForce.mult(1.0);
    cohesionForce.mult(1.0);
    mouseForce.mult(0.5);

    this.applyForce(separationForce);
    this.applyForce(alignmentForce);
    this.applyForce(cohesionForce);
    this.applyForce(mouseForce);
  }

  seek(target) {
    let desired = p5.Vector.sub(target, this.position);
    desired.setMag(this.maxSpeed);
    let steer = p5.Vector.sub(desired, this.velocity);
    steer.limit(this.maxForce);
    return steer;
  }

  separation(boids) {
    let desiredSeparation = 50;
    let steer = createVector();
    let count = 0;
    for (let other of boids) {
      let d = p5.Vector.dist(this.position, other.position);
      if (d > 0 && d < desiredSeparation) {
        let diff = p5.Vector.sub(this.position, other.position);
        diff.normalize();
        diff.div(d);
        steer.add(diff);
        count++;
      }
    }
    if (count > 0) {
      steer.div(count);
      steer.normalize();
      steer.mult(this.maxSpeed);
      steer.sub(this.velocity);
      steer.limit(this.maxForce);
    }
    return steer;
  }

  alignment(boids) {
    let neighborDistance = 100;
    let sum = createVector();
    let count = 0;
    for (let other of boids) {
      let d = p5.Vector.dist(this.position, other.position);
      if (d > 0 && d < neighborDistance) {
        sum.add(other.velocity);
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      sum.normalize();
      sum.mult(this.maxSpeed);
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(this.maxForce);
      return steer;
    } else {
      return createVector();
    }
  }

  cohesion(boids) {
    let neighborDistance = 100;
    let sum = createVector();
    let count = 0;
    for (let other of boids) {
      let d = p5.Vector.dist(this.position, other.position);
      if (d > 0 && d < neighborDistance) {
        sum.add(other.position);
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      return this.seek(sum);
    } else {
      return createVector();
    }
  }

  edges() {
    if (this.position.x < 0) this.position.x += width;
    if (this.position.x > width) this.position.x -= width;
    if (this.position.y < 0) this.position.y += height;
    if (this.position.y > height) this.position.y -= height;
  }
}
Line-by-line explanation (100 lines)

🔧 Subcomponents:

calculation Initialize boid properties constructor() {

Sets up initial position, velocity, and behavioral parameters for a new bird

for-loop Separation force calculation for (let other of boids) {

Checks distance to all other boids and calculates an avoidance force

for-loop Alignment force calculation for (let other of boids) {

Collects nearby boids' velocities and calculates a steering force toward their average heading

for-loop Cohesion force calculation for (let other of boids) {

Collects nearby boids' positions and calculates a steering force toward their center of mass

class Boid {
Defines the Boid class, which represents a single bird with position, velocity, and flocking behaviors
constructor() {
The constructor runs once when a new Boid is created and initializes all its properties
this.position = createVector(random(width), random(height));
Gives the bird a random starting position anywhere on the canvas
this.velocity = p5.Vector.random2D();
Gives the bird a random direction vector (length 1, pointing in a random direction)
this.velocity.setMag(random(2, 4));
Scales the velocity to a random speed between 2 and 4 pixels per frame
this.acceleration = createVector();
Initializes acceleration to (0, 0); it will be updated by flocking forces each frame
this.maxForce = 0.2;
Sets the maximum steering force any single behavior can apply; limits abrupt turns
this.maxSpeed = 4;
Sets the maximum speed the bird can achieve; prevents it from moving too fast
this.size = 10;
Defines the size of the bird's wedge shape in pixels
show() {
Renders the bird to the canvas
strokeWeight(1);
Sets the outline thickness to 1 pixel
stroke(255);
Sets the outline color to white
fill(255, 100);
Sets the fill color to semi-transparent white (alpha=100 out of 255)
push();
Saves the current transformation state (no translation or rotation yet)
translate(this.position.x, this.position.y);
Moves the origin to the bird's current position
rotate(this.velocity.heading());
Rotates the coordinate system so the bird's shape points in the direction it's moving
beginShape();
Starts defining a new polygon (the bird shape)
vertex(this.size / 2, 0);
Adds the front vertex (head) of the bird at distance size/2 to the right (forward)
vertex(-this.size / 2, this.size / 4);
Adds the bottom-tail vertex
vertex(-this.size / 2, -this.size / 4);
Adds the top-tail vertex
endShape(CLOSE);
Closes the shape (connects the last vertex back to the first) and fills/strokes it
pop();
Restores the transformation state, undoing translate and rotate for subsequent drawing
update() {
Updates the bird's position and velocity based on accumulated forces
this.position.add(this.velocity);
Moves the bird by its velocity vector each frame
this.velocity.add(this.acceleration);
Applies the accumulated acceleration to velocity (Newton's second law: v += a)
this.velocity.limit(this.maxSpeed);
Caps the velocity magnitude at maxSpeed so the bird never moves faster than 4 pixels/frame
this.acceleration.mult(0);
Resets acceleration to zero; forces will be recalculated next frame
applyForce(force) {
Adds a steering force to the bird's acceleration
this.acceleration.add(force);
Accumulates the force into acceleration; multiple forces can be applied before update()
flock(boids) {
Calculates all flocking forces and applies them to the bird
let separationForce = this.separation(boids);
Calls separation() to calculate an avoid-crowding force
let alignmentForce = this.alignment(boids);
Calls alignment() to calculate a steer-toward-average-heading force
let cohesionForce = this.cohesion(boids);
Calls cohesion() to calculate a move-toward-average-position force
let mouseForce = this.seek(createVector(mouseX, mouseY));
Calls seek() with the mouse position to calculate a force steering toward the cursor
separationForce.mult(1.5);
Weighs separation to 150% strength so birds avoid crowding more strongly
alignmentForce.mult(1.0);
Keeps alignment at 100% strength (natural weight)
cohesionForce.mult(1.0);
Keeps cohesion at 100% strength
mouseForce.mult(0.5);
Weakens mouse-following to 50% so it influences the flock but doesn't dominate
this.applyForce(separationForce);
Applies the weighted separation force to acceleration
this.applyForce(alignmentForce);
Applies the weighted alignment force
this.applyForce(cohesionForce);
Applies the weighted cohesion force
this.applyForce(mouseForce);
Applies the weighted mouse force
seek(target) {
Calculates a steering force that pulls the bird toward a target position
let desired = p5.Vector.sub(target, this.position);
Calculates the vector from the bird to the target
desired.setMag(this.maxSpeed);
Scales the desired direction to maxSpeed, representing the ideal velocity
let steer = p5.Vector.sub(desired, this.velocity);
Calculates steering as the difference between ideal velocity and current velocity
steer.limit(this.maxForce);
Limits the steering force to maxForce so turns are smooth, not abrupt
return steer;
Returns the steering force to be applied as acceleration
separation(boids) {
Calculates a force that steers the bird away from nearby flockmates
let desiredSeparation = 50;
Defines the minimum comfortable distance between birds (50 pixels)
let steer = createVector();
Initializes the separation force vector as (0, 0)
let count = 0;
Counts how many neighbors are too close
for (let other of boids) {
Loops through all boids in the flock
let d = p5.Vector.dist(this.position, other.position);
Calculates the distance to this other bird
if (d > 0 && d < desiredSeparation) {
If the other bird exists (d > 0) and is within the separation radius (d < 50)
let diff = p5.Vector.sub(this.position, other.position);
Calculates the vector from the other bird to this bird (away direction)
diff.normalize();
Scales the away direction to length 1 so all directions have equal weight
diff.div(d);
Divides by distance so closer birds exert stronger repulsion (inverse distance weighting)
steer.add(diff);
Accumulates this away force to the total steering force
count++;
Increments the count of nearby neighbors
if (count > 0) {
If at least one neighbor was too close
steer.div(count);
Averages the separation force over all nearby neighbors
steer.normalize();
Normalizes to unit length so the force magnitude is controlled by limit()
steer.mult(this.maxSpeed);
Scales the force to desired speed magnitude
steer.sub(this.velocity);
Converts desired direction into a steering force (desired - current)
steer.limit(this.maxForce);
Caps the force at maxForce to keep steering smooth
return steer;
Returns the separation force
alignment(boids) {
Calculates a force that steers the bird toward the average heading of nearby flockmates
let neighborDistance = 100;
Defines the radius within which birds influence this bird's heading (100 pixels)
let sum = createVector();
Initializes a vector to accumulate nearby birds' velocities
let count = 0;
Counts how many neighbors are within alignment range
for (let other of boids) {
Loops through all boids
let d = p5.Vector.dist(this.position, other.position);
Calculates distance to the other bird
if (d > 0 && d < neighborDistance) {
If the other bird exists and is within 100 pixels
sum.add(other.velocity);
Adds the other bird's velocity to the sum
count++;
Increments the neighbor count
sum.div(count);
Averages the nearby velocities to get the flock's heading
sum.normalize();
Normalizes to unit length for consistent force magnitude
sum.mult(this.maxSpeed);
Scales to desired speed
let steer = p5.Vector.sub(sum, this.velocity);
Calculates steering force (desired heading - current velocity)
steer.limit(this.maxForce);
Limits the force magnitude to maxForce
return steer;
Returns the alignment force
return createVector();
If no neighbors, returns zero force (no alignment needed)
cohesion(boids) {
Calculates a force that steers the bird toward the average position of nearby flockmates
let neighborDistance = 100;
Defines cohesion range (100 pixels)
let sum = createVector();
Accumulates positions of nearby neighbors
let count = 0;
Counts neighbors
for (let other of boids) {
Loops through all boids
let d = p5.Vector.dist(this.position, other.position);
Calculates distance
if (d > 0 && d < neighborDistance) {
If within range
sum.add(other.position);
Adds the other bird's position to the sum
count++;
Increments count
sum.div(count);
Averages to find the center of the flock (nearby)
return this.seek(sum);
Uses seek() to steer toward that center point
return createVector();
If no neighbors, returns zero force
edges() {
Wraps the bird around screen boundaries
if (this.position.x < 0) this.position.x += width;
If bird goes off the left edge, place it at the right edge
if (this.position.x > width) this.position.x -= width;
If bird goes off the right edge, place it at the left edge
if (this.position.y < 0) this.position.y += height;
If bird goes off the top, place it at the bottom
if (this.position.y > height) this.position.y -= height;
If bird goes off the bottom, place it at the top

📦 Key Variables

boids array

Stores all Boid objects (birds) in the flock; used by draw() to update and render each bird each frame

let boids = [];
flockSize number

The number of birds to create in setup(); controls flock density and complexity

let flockSize = 100;
worldGraphics p5.Graphics

An off-screen graphics buffer that holds the scrolling landscape; is reused and updated in draw()

let worldGraphics;
skyColor, waterColor, sandColor, forestColor, mountainColor p5.Color

Store the color palette used in biome drawing; defined in setup() and used throughout drawBiomeSection()

let skyColor, waterColor, sandColor, forestColor, mountainColor;
noiseScale number

Controls the scale of Perlin noise used for biome generation; smaller values = larger biomes, larger values = more fragmented terrain

let noiseScale = 0.01;
scrollOffset number

Tracks how many pixels the world has scrolled in the current cycle; incremented each frame, reset when >= height

let scrollOffset = 0;
scrollSpeed number

How many pixels to scroll per frame; controls the pace at which the landscape moves upward

let scrollSpeed = 1;
worldNoiseOffset number

Tracks the Perlin noise offset for procedural generation; ensures newly regenerated sections are continuous with previous ones

let worldNoiseOffset = 0;
worldHeight number

The height of the worldGraphics buffer; set to height * 3 in setup() to allow smooth scrolling without regeneration every frame

let worldHeight;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

PERFORMANCE separation(), alignment(), cohesion() methods in Boid class

Each bird loops through ALL 100+ other birds every frame to calculate forces. With 300 birds, this is 90,000+ distance calculations per frame—expensive.

💡 Use spatial partitioning (grid or quadtree) to only check nearby birds instead of all birds. Or limit each bird to checking only the nearest 10 neighbors using a sorted distance array.

BUG generateWorldPattern() function

If flockSize is set very high (e.g., 1000), the Perlin noise sampling at (width * noiseScale, ...) always uses the same x coordinate, making biomes repeat in vertical bands instead of varying naturally.

💡 Sample noise at a y-based x coordinate like ((y + noiseYOffset) * noiseScale * 0.1) or add some horizontal variation to make biomes shift across the width.

STYLE Boid class methods

The separation(), alignment(), and cohesion() methods are nearly identical loop structures—lots of repetition makes the code harder to maintain.

💡 Extract a helper method like getNeighborsInRadius(neighborDistance) that returns an array of nearby birds, then reuse it in all three methods to reduce duplication.

FEATURE Boid class

Birds are always white (255) and semi-transparent—they all look identical even though they have different velocities and behaviors.

💡 Color birds based on speed (fast = red, slow = blue) or direction (HSB colorMode with hue from heading) to add visual richness and help track individual bird behavior.

BUG edges() method in Boid

Birds wrap instantly from one side to the other, which can appear jarring visually. If a bird is drawn partially off-screen, it vanishes and reappears without continuity.

💡 Draw wrapped birds on both sides of the boundary (draw the same bird twice, once on each side) so they appear to smoothly exit one edge and enter the opposite edge.

🔄 Code Flow

Code flow showing setup, draw, generateworldpattern, getbiometype, drawbiomesection, windowresized, boid

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> biome-colors[Initialize biome colors] click biome-colors href "#sub-biome-colors" setup --> buffer-creation[Create graphics buffer] click buffer-creation href "#sub-buffer-creation" setup --> boid-initialization[Populate boid array] click boid-initialization href "#sub-boid-initialization" draw --> scroll-increment[Update scroll position] click scroll-increment href "#sub-scroll-increment" draw --> scroll-boundary-check[Regenerate world when scrolled] click scroll-boundary-check href "#sub-scroll-boundary-check" draw --> buffer-shift[Shift buffer contents up] click buffer-shift href "#sub-buffer-shift" draw --> boid-update-loop[Update and render all boids] click boid-update-loop href "#sub-boid-update-loop" boid-update-loop --> boid[Boid class] click boid href "#fn-boid" boid-update-loop --> separation-loop[Separation force calculation] click separation-loop href "#sub-separation-loop" boid-update-loop --> alignment-loop[Alignment force calculation] click alignment-loop href "#sub-alignment-loop" boid-update-loop --> cohesion-loop[Cohesion force calculation] click cohesion-loop href "#sub-cohesion-loop" draw --> section-loop[Iterate through vertical sections] click section-loop href "#sub-section-loop" section-loop --> biome-determination[Sample noise and pick biome] click biome-determination href "#sub-biome-determination" biome-determination --> biome-thresholds[Noise-to-biome mapping] click biome-thresholds href "#sub-biome-thresholds" section-loop --> drawbiomesection[drawBiomeSection] click drawbiomesection href "#fn-drawbiomesection" drawbiomesection --> water-gradient[Water gradient loop] click water-gradient href "#sub-water-gradient" drawbiomesection --> water-ripples[Water ripple overlay] click water-ripples href "#sub-water-ripples" drawbiomesection --> sand-texture[Sand grain texture] click sand-texture href "#sub-sand-texture" drawbiomesection --> forest-trees[Random tree placement] click forest-trees href "#sub-forest-trees" drawbiomesection --> mountain-peaks[Mountain profile loop] click mountain-peaks href "#sub-mountain-peaks" draw --> windowresized[windowResized] click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What kind of visuals can I expect from the weird birds flocking sketch?

The sketch creates a mesmerizing display of a flock of birds gliding through an endlessly scrolling landscape filled with changing biomes like skies, forests, mountains, and seas.

Can I interact with the weird birds flocking sketch, and if so, how?

The sketch is primarily a visual experience without direct user interaction, allowing viewers to relax and enjoy the continuous movement of the birds and landscapes.

What creative coding techniques are showcased in the weird birds flocking sketch?

This sketch demonstrates techniques such as particle systems for flocking behavior and Perlin noise for generating smooth, dynamic biomes in an infinite scrolling world.

Preview

weird birds flocking - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of weird birds flocking - Code flow showing setup, draw, generateworldpattern, getbiometype, drawbiomesection, windowresized, boid
Code Flow Diagram