very weird sketch

This sketch creates an interactive space-themed collector game where a player-controlled spaceship chases and collects randomly generated alien critters floating across the canvas. The critters move organically using Perlin noise and display in various colorful shapes—polygons, blobs, and stars—while the player earns points for each successful collection.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make critters rainbow — Add a hue-based color cycle so critters shift through the color spectrum over time, creating a more chaotic visual effect.
  2. Double the score reward — Collecting a critter now gives 20 points instead of 10, making high scores easier to reach.
  3. Slow critter movement — Critters move more slowly and predictably by reducing the Perlin noise update frequency—makes them easier targets.
  4. Bigger spaceship — The spaceship collision radius and visual size increases, making it easier to catch critters.
  5. Tighter mouse tracking — The spaceship snaps to your mouse more aggressively (less lag), making it feel more responsive.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playable collector game set in space. A red spaceship (controlled by your mouse or touch) chases colorful alien critters that float around the canvas using Perlin noise to move organically. When you touch a critter, your score increases and a new one spawns. The sketch combines object-oriented programming with p5.js animation, collision detection, and generative art—making it an excellent project to study if you want to build interactive games.

The code is organized into two classes: Player (the spaceship you control) and Critter (the aliens you collect). The main draw() loop updates and displays both, then checks for collisions. By reading this sketch, you will learn how to structure a game with classes, use Perlin noise for smooth organic movement, draw custom shapes with trigonometry, and handle mouse and touch input simultaneously.

⚙️ How It Works

  1. When setup() runs, it creates the canvas and initializes the player spaceship in the center, then spawns 10 critters at random positions across the canvas.
  2. Every frame, draw() clears the canvas to dark space blue, then updates and draws the player spaceship and all critters.
  3. The player's update() method moves the spaceship toward the mouse or touch position using smooth interpolation (multiplying the distance by 0.1 each frame).
  4. Each critter's update() method uses Perlin noise to generate smooth, wandering movement that never feels random or jittery—the noiseOffset value ensures each critter has its own unique movement pattern.
  5. Every frame, the code checks if the player has collided with any critter using the distance formula; if so, the critter resets to a new random location and the score increases by 10 points.
  6. The display() methods draw the spaceship as a rotating triangle with an animated orange thruster, and draw each critter as one of three random shape types: regular polygons, blobby noise-based shapes, or stars.

🎓 Concepts You'll Learn

Object-oriented programming with classesCollision detection using distance formulaPerlin noise for organic movementMouse and touch input handlingTransform matrices (translate, rotate)Custom shape drawing with trigonometryGame loops and state management

📝 Code Breakdown

Player class

The Player class manages the spaceship—its position, movement, collision detection, and score. Notice how the constructor() initializes all properties, update() handles movement logic, and display() handles drawing and animations. This pattern (constructor, update, display) is the standard way to organize game objects in p5.js.

🔬 This code chooses between touch and mouse input. What happens if you remove the if-statement and always use mouseX/mouseY instead? Try removing the touches.length check entirely—will touch still work?

    // Determine target based on mouse or touch
    let targetX, targetY;
    if (touches.length > 0) {
      targetX = touches[0].x;
      targetY = touches[0].y;
    } else {
      targetX = mouseX;
      targetY = mouseY;
    }

🔬 These three vertices form a triangle. What happens if you change the second vertex to (-this.size / 2, -this.size / 2) and the third to (-this.size / 2, this.size / 2)? The spaceship will look more squared-off.

    // Main body
    beginShape();
    vertex(this.size / 2, 0);
    vertex(-this.size / 2, -this.size / 3);
    vertex(-this.size / 2, this.size / 3);
    endShape(CLOSE);
class Player {
  constructor() {
    this.x = width / 2;
    this.y = height / 2;
    this.size = 30;
    this.speed = 5;
    this.color = color(255, 100, 100);
    this.score = 0;
  }

  update() {
    // Determine target based on mouse or touch
    let targetX, targetY;
    if (touches.length > 0) {
      targetX = touches[0].x;
      targetY = touches[0].y;
    } else {
      targetX = mouseX;
      targetY = mouseY;
    }

    // Move player towards target
    this.x += (targetX - this.x) * 0.1;
    this.y += (targetY - this.y) * 0.1;

    // Keep player within bounds
    this.x = constrain(this.x, 0, width);
    this.y = constrain(this.y, 0, height);
  }

  display() {
    push();
    translate(this.x, this.y);

    // Rotate towards movement direction for a spaceship feel
    let angle = atan2(mouseY - this.y, mouseX - this.x);
    rotate(angle);

    // Draw the player ship (a triangle with a tail)
    fill(this.color);
    stroke(255);
    strokeWeight(2);

    // Main body
    beginShape();
    vertex(this.size / 2, 0);
    vertex(-this.size / 2, -this.size / 3);
    vertex(-this.size / 2, this.size / 3);
    endShape(CLOSE);

    // Tail (thruster effect)
    fill(255, 150, 0); // Orange for thruster
    noStroke();
    let tailWidth = map(sin(frameCount * 0.2), -1, 1, 5, 15);
    rect(-this.size, -tailWidth / 2, this.size / 2, tailWidth);

    pop();

    // Display score
    fill(255);
    noStroke();
    textSize(24);
    textAlign(RIGHT, TOP);
    text(`Score: ${this.score}`, width - 20, 20);
  }

  // Check if player is colliding with a critter
  collides(critter) {
    let d = dist(this.x, this.y, critter.x, critter.y);
    return d < this.size / 2 + critter.size / 2;
  }

  // Increase score and reset critter
  collectCritter(critter) {
    this.score += 10; // Award points
    critter.reset(); // Make a new critter at a new location
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional Touch or mouse target detection if (touches.length > 0) {

Checks if the device is being touched; if so, use touch position; otherwise use mouse position

calculation Smooth following movement this.x += (targetX - this.x) * 0.1;

Moves the spaceship toward the target by 10% of the remaining distance each frame, creating smooth easing

conditional Collision check return d < this.size / 2 + critter.size / 2;

Returns true if the distance between spaceship and critter centers is less than their combined radii

this.x = width / 2;
Positions the spaceship at the horizontal center of the canvas
this.y = height / 2;
Positions the spaceship at the vertical center of the canvas
this.size = 30;
Sets the spaceship's diameter to 30 pixels, used for drawing and collision detection
this.color = color(255, 100, 100);
Creates a reddish color (RGB: red=255, green=100, blue=100) for the spaceship body
if (touches.length > 0) {
Checks if any fingers are currently touching the screen; allows mobile gameplay without a mouse
this.x += (targetX - this.x) * 0.1;
Calculates the distance to the target, multiplies by 0.1 (10%), and moves that fraction toward it each frame—this creates smooth, eased motion instead of snapping instantly
this.x = constrain(this.x, 0, width);
Clamps the x position between 0 and canvas width, preventing the spaceship from drifting off-screen
let angle = atan2(mouseY - this.y, mouseX - this.x);
Calculates the angle from the spaceship to the mouse using atan2, so the spaceship visually points toward where you're aiming
rotate(angle);
Rotates the spaceship to point toward the mouse before drawing it
vertex(this.size / 2, 0);
Places the tip of the spaceship triangle at the right, scaled to half the size value
let tailWidth = map(sin(frameCount * 0.2), -1, 1, 5, 15);
Creates an animated thruster by mapping a sine wave (which oscillates -1 to 1) to a width between 5 and 15 pixels
let d = dist(this.x, this.y, critter.x, critter.y);
Calculates the Euclidean distance between the spaceship's center and the critter's center in pixels
return d < this.size / 2 + critter.size / 2;
Returns true if distance is less than the sum of both radii (spaceship radius + critter radius)—this is the standard circle collision test
this.score += 10;
Adds 10 points to the player's score whenever a critter is collected

Critter class

The Critter class manages the alien creatures—their appearance, movement, and animation. Key techniques here include Perlin noise (which creates smooth, organic movement), trigonometry (cos/sin place vertices on circles), screen wrapping (teleporting off-screen critters to the opposite side), and procedural shape generation (three different shape types drawn with loops). The reset() method is clever: it's called both during construction and when collected, so the same critter object is reused by just changing its properties.

🔬 This code uses different multipliers (0.01 vs 0.02) for X and Y noise to create independent movement. What happens if you change both to 0.01? The critters will move along a diagonal—try it and watch their paths change from meandering to drifting.

  update() {
    // Generate organic movement using Perlin noise
    let moveX = map(noise(this.noiseOffset + frameCount * 0.01), 0, 1, -2, 2);
    let moveY = map(noise(this.noiseOffset + frameCount * 0.02), 0, 1, -2, 2);

    this.x += moveX;
    this.y += moveY;

🔬 This loop draws a regular polygon by placing vertices evenly around a circle. What happens if you divide the angle increment by 2 (change += to += TWO_PI / points / 2)? You'll get twice as many points—a smoother polygon. Try it!

    for (let a = 0; a < TWO_PI; a += TWO_PI / points) {
      let sx = cos(a) * radius;
      let sy = sin(a) * radius;
      vertex(sx, sy);
    }
class Critter {
  constructor() {
    this.reset(); // Initialize with random properties
    this.noiseOffset = random(1000); // Unique noise offset for movement
  }

  reset() {
    this.x = random(width);
    this.y = random(height);
    this.size = random(20, 60);
    this.color = color(random(255), random(255), random(255));
    this.shapeType = floor(random(3)); // 0: polygon, 1: blob, 2: star
    this.numPoints = floor(random(3, 10)); // For polygons/stars
    this.spinSpeed = random(-0.05, 0.05);
    this.angle = random(TWO_PI);
    this.pulsate = random(0.5, 1.5); // For blob/pulsation effect
  }

  update() {
    // Generate organic movement using Perlin noise
    let moveX = map(noise(this.noiseOffset + frameCount * 0.01), 0, 1, -2, 2);
    let moveY = map(noise(this.noiseOffset + frameCount * 0.02), 0, 1, -2, 2);

    this.x += moveX;
    this.y += moveY;

    // Keep critters within bounds, wrapping around
    if (this.x < 0) this.x = width;
    if (this.x > width) this.x = 0;
    if (this.y < 0) this.y = height;
    if (this.y > height) this.y = 0;

    this.angle += this.spinSpeed;
  }

  display() {
    push();
    translate(this.x, this.y);
    rotate(this.angle);

    fill(this.color);
    stroke(255);
    strokeWeight(1);

    switch (this.shapeType) {
      case 0: // Polygon
        this.drawPolygon(this.size / 2, this.numPoints);
        break;
      case 1: // Blob (noisy polygon)
        this.drawBlob(this.size / 2);
        break;
      case 2: // Star
        this.drawStar(this.size / 2, this.numPoints);
        break;
    }
    pop();
  }

  // Helper function to draw a regular polygon
  drawPolygon(radius, points) {
    beginShape();
    for (let a = 0; a < TWO_PI; a += TWO_PI / points) {
      let sx = cos(a) * radius;
      let sy = sin(a) * radius;
      vertex(sx, sy);
    }endShape(CLOSE);
  }

  // Helper function to draw a blobby shape using noise
  drawBlob(radius) {
    beginShape();
    let noiseSeed = this.noiseOffset;
    for (let a = 0; a < TWO_PI; a += radians(5)) { // Smaller steps for smoother blob
      let r = radius * this.pulsate * map(noise(noiseSeed + cos(a), noiseSeed + sin(a), frameCount * 0.005), 0, 1, 0.7, 1.3);
      let sx = cos(a) * r;
      let sy = sin(a) * r;
      vertex(sx, sy);
    }
    endShape(CLOSE);
  }

  // Helper function to draw a star
  drawStar(radius, points) {
    let innerRadius = radius * 0.5;
    let angle = TWO_PI / (points * 2);
    beginShape();
    for (let a = 0; a < TWO_PI; a += angle) {
      let r = (a % (angle * 2) < angle) ? radius : innerRadius;
      let sx = cos(a) * r;
      let sy = sin(a) * r;
      vertex(sx, sy);
    }
    endShape(CLOSE);
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

method Reset to new random state this.reset();

Reinitializes all critter properties to random values, called when collected or at startup

calculation Perlin noise movement let moveX = map(noise(this.noiseOffset + frameCount * 0.01), 0, 1, -2, 2);

Uses Perlin noise to generate smooth, organic movement that changes gradually over time

conditional Screen wrapping if (this.x < 0) this.x = width;

When a critter drifts off one edge, it instantly reappears on the opposite edge

switch-case Shape type selector switch (this.shapeType) {

Chooses which drawing function (polygon, blob, or star) to call based on the critter's shape type

this.noiseOffset = random(1000);
Assigns a unique random starting point for this critter's Perlin noise—ensures each critter moves independently with its own pattern
this.x = random(width);
Places the critter at a random x position anywhere across the canvas width
this.size = random(20, 60);
Randomly sizes the critter between 20 and 60 pixels in diameter
this.shapeType = floor(random(3));
Randomly selects a shape type: 0 (polygon), 1 (blob), or 2 (star)
this.numPoints = floor(random(3, 10));
Randomly chooses between 3 and 9 vertices or points for the polygon or star
this.spinSpeed = random(-0.05, 0.05);
Assigns a random rotation speed—negative values spin counter-clockwise, positive clockwise
let moveX = map(noise(this.noiseOffset + frameCount * 0.01), 0, 1, -2, 2);
Perlin noise returns a smooth value 0 to 1; map() converts it to a range -2 to 2 pixels per frame. The noiseOffset ensures this critter's pattern differs from others; frameCount * 0.01 makes the noise progress slowly over time
if (this.x < 0) this.x = width;
Wrapping boundary: when the critter leaves the left edge, teleport it to the right edge
this.angle += this.spinSpeed;
Incrementally rotates the critter each frame by adding spinSpeed to its angle
for (let a = 0; a < TWO_PI; a += TWO_PI / points) {
Loops around a full circle (TWO_PI radians), incrementing by an equal angle for each vertex—dividing TWO_PI by the number of points ensures even spacing
let sx = cos(a) * radius;
Calculates the x-coordinate of a vertex on a circle using cosine (horizontal component)
let sy = sin(a) * radius;
Calculates the y-coordinate of a vertex on a circle using sine (vertical component)
let r = radius * this.pulsate * map(noise(noiseSeed + cos(a), noiseSeed + sin(a), frameCount * 0.005), 0, 1, 0.7, 1.3);
For blob shapes, this line calculates a radius that varies using 3D Perlin noise (noiseSeed + cos(a), noiseSeed + sin(a), frameCount), creating bumpy, pulsating organic shapes
let r = (a % (angle * 2) < angle) ? radius : innerRadius;
For stars, this alternates between the outer radius and inner radius every other vertex—the modulo operator (%) and ternary operator (?) create the classic 5-pointed star effect

setup()

setup() runs once when the sketch starts. It initializes the canvas, creates the player, and populates the critters array. Notice it uses a for-loop to avoid typing critters.push() ten times—a common pattern when initializing arrays of game objects.

function setup() {
  createCanvas(windowWidth, windowHeight);
  player = new Player();

  // Initialize critters
  for (let i = 0; i < numCritters; i++) {
    critters.push(new Critter());
  }

  // Set up touch input for player movement on mobile
  if (touches.length > 0) {
    player.x = touches[0].x;
    player.y = touches[0].y;
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

initialization Canvas creation createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire browser window

for-loop Critter initialization loop for (let i = 0; i < numCritters; i++) {

Creates 10 critter objects and adds them to the critters array

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas sized to fit the entire browser window—windowWidth and windowHeight are p5.js variables that update if the window resizes
player = new Player();
Constructs a new Player object and stores it in the global player variable
for (let i = 0; i < numCritters; i++) {
Loops from 0 to numCritters-1 (10 times), running the loop body 10 times
critters.push(new Critter());
Creates a new Critter object and adds it to the end of the critters array using push()
if (touches.length > 0) {
Checks if the device is currently being touched; this helps mobile players start the game with the spaceship under their finger

draw()

draw() is the main game loop, running 60 times per second. Each frame it clears the canvas, updates and draws the player, updates and draws all critters, checks collisions, and adds atmospheric dust. The order matters: background() must run first to clear the canvas, and collisions must be checked after critter positions are updated.

🔬 This loop checks collisions AFTER drawing the critter. What if you move the collision check BEFORE the display? The critter would reset and be drawn at a new location on the same frame you collect it—try it and see if it feels smoother or more confusing.

  // Update and display critters, check for collisions
  for (let i = critters.length - 1; i >= 0; i--) {
    let critter = critters[i];
    critter.update();
    critter.display();

    if (player.collides(critter)) {
      player.collectCritter(critter); // Collect and reset critter
    }
  }
function draw() {
  background(20, 20, 60); // Dark space background

  // Update and display player
  player.update();
  player.display();

  // Update and display critters, check for collisions
  for (let i = critters.length - 1; i >= 0; i--) {
    let critter = critters[i];
    critter.update();
    critter.display();

    if (player.collides(critter)) {
      player.collectCritter(critter); // Collect and reset critter
    }
  }

  // Add some space dust/stars for atmosphere
  noStroke();
  fill(255, 100);
  for (let i = 0; i < 50; i++) {
    let x = random(width);
    let y = random(height);
    let s = random(1, 3);
    ellipse(x, y, s, s);
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

initialization Clear background background(20, 20, 60);

Fills the canvas with dark space blue, erasing everything drawn last frame to prevent trails

method-calls Update and draw player player.update(); player.display();

Moves the spaceship toward the mouse and draws it at its new position

for-loop Critter update and collision loop for (let i = critters.length - 1; i >= 0; i--) {

Iterates through all critters in reverse order, updating and drawing each, and checking for collisions with the player

for-loop Atmospheric dust particles for (let i = 0; i < 50; i++) {

Draws 50 small semi-transparent white circles at random positions each frame for visual atmosphere

background(20, 20, 60);
Fills the entire canvas with dark blue (RGB: 20, 20, 60), wiping out the previous frame's drawings so objects don't leave trails
player.update();
Calls the player's update() method, which moves the spaceship toward the mouse or touch position
player.display();
Calls the player's display() method, which draws the spaceship and the score text
for (let i = critters.length - 1; i >= 0; i--) {
Loops through critters in reverse order (from last to first)—reverse iteration is useful if you ever need to remove items from the array during the loop
let critter = critters[i];
Retrieves the critter at index i and stores it in a temporary variable for easier access
critter.update();
Calls the critter's update() method, which applies Perlin noise movement and increments its rotation
critter.display();
Calls the critter's display() method, which draws the critter as a polygon, blob, or star at its current position and rotation
if (player.collides(critter)) {
Checks whether the spaceship is touching this critter using the distance formula
player.collectCritter(critter);
If there's a collision, calls the player's collectCritter() method, which adds 10 points and resets the critter to a new random location
fill(255, 100);
Sets the fill color to white (255, 255, 255) with 100 alpha (transparency), so dust particles are semi-see-through
for (let i = 0; i < 50; i++) {
Loops 50 times, drawing 50 dust particles per frame at random positions
let s = random(1, 3);
Picks a random size between 1 and 3 pixels for this dust particle
ellipse(x, y, s, s);
Draws a small circle at the random position (x, y) with diameter s, creating a twinkling star effect

windowResized()

windowResized() is a p5.js callback that fires whenever the browser window is resized. This sketch resizes the canvas to fit the new window size. Since critters wrap around edges, they don't need repositioning—they automatically adjust as the boundaries change.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-position player and critters if needed, but for this game, they wrap around.
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions when the browser window is resized

touchStarted()

touchStarted() is a p5.js callback that fires when a finger touches the screen. By returning false, we tell the browser to not handle the touch event itself—this prevents unwanted scrolling or zooming while playing on mobile.

function touchStarted() {
  return false;
}
Line-by-line explanation (1 lines)
return false;
Prevents the browser's default touch behavior (like scrolling or pinch-zooming) from interfering with the game

touchEnded()

touchEnded() fires when a finger is lifted off the screen. Like touchStarted(), returning false prevents the browser from handling the event and triggering unwanted behaviors.

function touchEnded() {
  return false;
}
Line-by-line explanation (1 lines)
return false;
Prevents the browser's default touch-end behavior, ensuring the game stays responsive

drawPolygon(radius, points)

drawPolygon() uses trigonometry to place vertices evenly around a circle. The angle `a` steps from 0 to TWO_PI (360 degrees), and at each angle, cos(a) and sin(a) give x and y coordinates on a unit circle, scaled by the radius. This pattern—stepping around a circle with cos/sin—is the foundation for drawing almost any circular or curved shape in code.

🔬 This loop places vertices equally around a circle. What happens if you change += to *= 1.1? Instead of equal spacing, vertices will cluster closer together on one side. Or try changing TWO_PI / points to TWO_PI / points / 2 to double the vertex count.

    for (let a = 0; a < TWO_PI; a += TWO_PI / points) {
      let sx = cos(a) * radius;
      let sy = sin(a) * radius;
      vertex(sx, sy);
    }
  drawPolygon(radius, points) {
    beginShape();
    for (let a = 0; a < TWO_PI; a += TWO_PI / points) {
      let sx = cos(a) * radius;
      let sy = sin(a) * radius;
      vertex(sx, sy);
    }
    endShape(CLOSE);
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Vertex placement loop for (let a = 0; a < TWO_PI; a += TWO_PI / points) {

Steps around a full circle, placing vertices at evenly-spaced angles

beginShape();
Starts recording vertices—everything until endShape() becomes a single shape
for (let a = 0; a < TWO_PI; a += TWO_PI / points) {
Loops from 0 to TWO_PI (0 to 2π radians = a full circle), incrementing by TWO_PI/points each time—this divides the circle into equal wedges
let sx = cos(a) * radius;
Uses cosine to calculate the x-coordinate of a point on a circle: cos(angle) gives a value from -1 to 1, multiplied by radius to scale it
let sy = sin(a) * radius;
Uses sine to calculate the y-coordinate of a point on a circle: sin(angle) gives a value from -1 to 1, multiplied by radius to scale it
vertex(sx, sy);
Records this point as a vertex of the polygon
endShape(CLOSE);
Finishes the shape and closes it by drawing a line from the last vertex back to the first

drawBlob(radius)

drawBlob() creates organic, blobby shapes using 3D Perlin noise. Instead of placing vertices at a constant radius (like drawPolygon), it varies the radius at each angle based on noise values—creating bumps and indentations. The 3D noise input (noiseSeed + cos(a), noiseSeed + sin(a), frameCount * 0.005) allows the bump pattern to vary smoothly around the perimeter and animate over time.

  drawBlob(radius) {
    beginShape();
    let noiseSeed = this.noiseOffset;
    for (let a = 0; a < TWO_PI; a += radians(5)) { // Smaller steps for smoother blob
      let r = radius * this.pulsate * map(noise(noiseSeed + cos(a), noiseSeed + sin(a), frameCount * 0.005), 0, 1, 0.7, 1.3);
      let sx = cos(a) * r;
      let sy = sin(a) * r;
      vertex(sx, sy);
    }
    endShape(CLOSE);
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Noise-based vertex loop for (let a = 0; a < TWO_PI; a += radians(5)) {

Steps around the circle in 5-degree increments, creating many vertices for a smooth blob

calculation Noise-driven radius let r = radius * this.pulsate * map(noise(...), 0, 1, 0.7, 1.3);

Calculates a radius that varies based on Perlin noise, creating bumpy, organic blob surfaces

let noiseSeed = this.noiseOffset;
Stores the critter's unique noise offset for use in generating the blob shape
for (let a = 0; a < TWO_PI; a += radians(5)) {
Loops around the circle in smaller 5-degree steps (radians(5) = 5 degrees converted to radians)—more steps = smoother blob outline
let r = radius * this.pulsate * map(noise(noiseSeed + cos(a), noiseSeed + sin(a), frameCount * 0.005), 0, 1, 0.7, 1.3);
Calculates the radius at this angle using 3D Perlin noise: noiseSeed + cos(a) and sin(a) vary the noise based on angle, frameCount makes it animate, map() scales 0–1 to 0.7–1.3, and pulsate multiplies the effect
let sx = cos(a) * r;
X-coordinate of a vertex on the blob, using the noise-driven radius instead of a constant one
let sy = sin(a) * r;
Y-coordinate of a vertex on the blob

drawStar(radius, points)

drawStar() draws a star by alternating between two radii—outer and inner. The ternary operator (? :) and modulo (%) operator work together to alternate: as the angle steps around the circle, the modulo check cycles between true and false, selecting outer then inner then outer. This is an elegant way to create symmetrical alternating patterns.

🔬 The innerRadius is always half the outer radius. What happens if you change 0.5 to 0.8? The inner points become much more prominent, and the star becomes flatter. Or try 0.2 for deeply inset points.

    let innerRadius = radius * 0.5;
    let angle = TWO_PI / (points * 2);
  drawStar(radius, points) {
    let innerRadius = radius * 0.5;
    let angle = TWO_PI / (points * 2);
    beginShape();
    for (let a = 0; a < TWO_PI; a += angle) {
      let r = (a % (angle * 2) < angle) ? radius : innerRadius;
      let sx = cos(a) * r;
      let sy = sin(a) * r;
      vertex(sx, sy);
    }
    endShape(CLOSE);
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Inner and outer radius setup let innerRadius = radius * 0.5; let angle = TWO_PI / (points * 2);

Sets up the two radii for alternating outer and inner points, and calculates the angle step

for-loop Star vertex loop for (let a = 0; a < TWO_PI; a += angle) {

Steps around the circle, placing alternating inner and outer vertices

conditional Radius alternation let r = (a % (angle * 2) < angle) ? radius : innerRadius;

Uses the modulo and ternary operators to alternate between outer and inner radii

let innerRadius = radius * 0.5;
Sets the inner points of the star to half the radius of the outer points, creating the classic star shape
let angle = TWO_PI / (points * 2);
Calculates the angle between each vertex: a star with 5 points needs 10 vertices (5 outer + 5 inner), so angle = TWO_PI / 10
for (let a = 0; a < TWO_PI; a += angle) {
Loops around the circle, incrementing by the angle step each time
let r = (a % (angle * 2) < angle) ? radius : innerRadius;
Uses modulo (%) to cycle between 0 and angle*2, then compares to angle: if true, use outer radius; if false, use inner radius—this creates the alternating pattern
let sx = cos(a) * r;
X-coordinate of a star vertex, using either the outer or inner radius
let sy = sin(a) * r;
Y-coordinate of a star vertex

📦 Key Variables

player Player object

Stores the spaceship that the player controls—holds position, size, color, score, and methods for movement and collision

let player;
critters array of Critter objects

Stores all active alien critters in the game—updated and drawn each frame

let critters = [];
numCritters number

Controls how many critters are spawned at the start—change this to make the game easier or harder

let numCritters = 10;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

PERFORMANCE draw() dust particle loop

Generating 50 random dust particles every frame is expensive—random() and ellipse() are called 50 times per frame unnecessarily

💡 Pre-generate dust particle positions once in setup() and store them in an array, then just draw them each frame. You could also use translate() and a single precomputed path, or use createGraphics() to draw dust once and reuse the image.

BUG Player.display() rotation calculation

The spaceship rotates toward the CURRENT mouseX/mouseY even if the spaceship is lagged behind due to the 0.1 easing factor. This can make the ship point in a different direction than it's actually heading

💡 Calculate the rotation based on the spaceship's actual velocity direction: let angle = atan2(this.y - prevY, this.x - prevX) where prevX and prevY are the ship's position from the last frame.

FEATURE Critter class

All critters wrap around screen edges instantly, which looks unnatural and can surprise the player

💡 Add a check to keep critters bouncing or gently wrapping instead of teleporting: if (this.x < 0) this.x = 0; if (this.x > width) this.x = width; (same for y). Or add a small buffer zone so wrapping happens slightly off-screen.

STYLE Critter.reset()

The shapeType is generated with magic numbers (0, 1, 2) without clear meaning—making the code harder to understand

💡 Define constants at the top: const SHAPE_POLYGON = 0, SHAPE_BLOB = 1, SHAPE_STAR = 2; then use this.shapeType = floor(random(3)) or choose from [SHAPE_POLYGON, SHAPE_BLOB, SHAPE_STAR] explicitly.

BUG Critter.drawBlob() noise calculation

The 3D noise input uses noiseSeed + cos(a) and noiseSeed + sin(a)—these can produce the same noise value for opposite angles (e.g., cos(0) = 1, cos(π) = -1 but noise takes absolute inputs), reducing variation

💡 Use unique noise seeds: let noiseSeed1 = this.noiseOffset + cos(a), let noiseSeed2 = this.noiseOffset + sin(a), then call noise(noiseSeed1, noiseSeed2, frameCount * 0.005) to avoid aliasing.

🔄 Code Flow

Code flow showing player, critter, setup, draw, windowresized, touchstarted, touchended, drawpolygon, drawblob, drawstar

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

graph TD start[Start] --> setup[setup] setup --> create-canvas[create-canvas] create-canvas --> init-critters-loop[init-critters-loop] init-critters-loop --> draw[draw loop] draw --> draw-background[draw-background] draw-background --> draw-player[draw-player] draw-player --> player-update-target[player-update-target] player-update-target --> player-update-movement[player-update-movement] player-update-movement --> draw-critters-loop[draw-critters-loop] draw-critters-loop --> critter-reset[critter-reset] critter-reset --> critter-perlin-movement[critter-perlin-movement] critter-perlin-movement --> critter-wrapping[critter-wrapping] critter-wrapping --> critter-shape-switch[critter-shape-switch] critter-shape-switch --> draw-dust[draw-dust] draw-dust --> draw draw-critters-loop -->|for-loop| draw-critters-loop draw-critters-loop -->|collision check| player-collision[player-collision] draw-dust -->|for-loop| draw-dust click setup href "#fn-setup" click create-canvas href "#sub-create-canvas" click init-critters-loop href "#sub-init-critters-loop" click draw href "#fn-draw" click draw-background href "#sub-draw-background" click draw-player href "#sub-draw-player" click player-update-target href "#sub-player-update-target" click player-update-movement href "#sub-player-update-movement" click draw-critters-loop href "#sub-draw-critters-loop" click critter-reset href "#sub-critter-reset" click critter-perlin-movement href "#sub-critter-perlin-movement" click critter-wrapping href "#sub-critter-wrapping" click critter-shape-switch href "#sub-critter-shape-switch" click draw-dust href "#sub-draw-dust" click player-collision href "#sub-player-collision"

❓ Frequently Asked Questions

What visual experience does the 'very weird sketch' provide?

The sketch creates a dynamic visual experience with a player-controlled spaceship that moves towards the mouse or touch input, featuring colorful graphics and a score display.

How can users interact with the 'very weird sketch'?

Users can interact by moving their mouse or using touch controls to guide the spaceship, collecting critters and increasing their score.

What creative coding concepts are showcased in this sketch?

The sketch demonstrates principles of object-oriented programming through the Player class, along with movement interpolation and collision detection.

Preview

very weird sketch - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of very weird sketch - Code flow showing player, critter, setup, draw, windowresized, touchstarted, touchended, drawpolygon, drawblob, drawstar
Code Flow Diagram