Try to get the highest score!

This sketch is a 20-second arcade game where you swing a glowing sword across two eerie, eye-tracking space monsters to score points before time runs out. It layers a slowly animated Perlin-noise nebula and twinkling starfield behind the action, and uses custom line-circle collision math to detect sword slashes.

🧪 Try This!

Experiment with the code by making these changes:

  1. Give yourself more time to play — Increasing gameDuration extends the round length, giving you longer to rack up a higher score.
  2. Recolor the sword — Changing the stroke color of the sword line instantly changes its glow color from orange-yellow to cyan.
  3. Fill the sky with more stars — Bumping up numStars makes the background starfield much denser and busier.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns p5.js into a fast arcade game: two monster heads with glowing, mouse-tracking eyes float over an animated starfield and Perlin-noise nebula, and you have 20 seconds to slash them with a mouse-drag sword to rack up points. It combines several techniques worth studying together - custom collision detection between a line segment and a circle, drawingContext shadowBlur for neon glow effects, atan2() for eyes that look at the cursor, and noise() driven shapes for a living background.

The code is organized around a small game-state machine (start screen, playing, game over) driven by gameActive and gameOver flags, a setup() that builds the starfield and two entity objects, and a draw() loop that renders everything and checks for sword hits every frame. Reading through it teaches you how to structure a simple game loop, how to fake glowing light with the canvas shadow API, and how to write your own geometry helper functions instead of relying on a physics library.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, fills a stars array with 500 randomly placed twinkling dots, defines five nebula colors, and creates two entity objects with starting positions, colors, and eye settings.
  2. Every frame, draw() first paints the starfield and an animated noise-based nebula, then checks gameActive - if the game hasn't started yet it shows a start screen and exits early.
  3. Once the game is active, draw() calculates the remaining time from gameStartTime and gameDuration, displays the timer and score, and flips gameOver to true when time runs out.
  4. While the game is running, each entity that hasn't been cut is drawn with drawEntity(), which uses push()/translate() to position the body shapes and calls drawGlowingEye() twice per monster so the pupils track the mouse using atan2().
  5. If the left mouse button is held, draw() renders a glowing sword line from the previous mouse position to the current one and calls the custom lineCircle() function to test whether that line segment intersects any uncut entity's bounding circle.
  6. A successful hit increments the score and calls resetEntity(), which relocates the monster, randomizes its colors and sizes, and marks it uncut again so the game keeps giving you new targets until time expires and mousePressed() lets you restart.

🎓 Concepts You'll Learn

Game state management (start/playing/game over)Custom line-circle collision detectiondrawingContext shadowBlur for glow effectsPerlin noise for animated organic shapesatan2() for pupil/eye trackingpush()/pop() and translate() for local coordinate systemsObject literals as lightweight entitiesTimers with millis()

📝 Code Breakdown

drawGlowingEye()

This helper is called four times per frame (twice per monster) and shows how to combine drawingContext.shadowBlur with simple trigonometry (atan2, cos, sin) to build an expressive, reusable eye component instead of hardcoding each eye separately.

🔬 The flicker range is -10% to +10% of the iris size. What happens visually if you widen it to -irisSize * 0.5 to irisSize * 0.5?

  let baseGlowAmount = irisSize * 0.7; // Base blur amount for the glow
  let flickerAmount = random(-irisSize * 0.1, irisSize * 0.1); // Small random variation for flicker
function drawGlowingEye(irisX, irisY, irisSize, glowColor, targetX, targetY) {
  let pupilSize = irisSize * 0.4;
  let baseGlowAmount = irisSize * 0.7; // Base blur amount for the glow
  let flickerAmount = random(-irisSize * 0.1, irisSize * 0.1); // Small random variation for flicker
  let currentGlowAmount = baseGlowAmount + flickerAmount; // Apply flicker

  // Set shadow properties for the glowing effect
  drawingContext.shadowColor = glowColor;
  drawingContext.shadowBlur = currentGlowAmount;

  // Draw the iris (the glowing part of the eye)
  fill(glowColor);
  circle(irisX, irisY, irisSize);

  // Reset shadow blur and color for the pupil
  // This ensures only the iris glows, not the pupil or other shapes
  drawingContext.shadowBlur = 0;
  drawingContext.shadowColor = 'black'; // Reset to a default or black

  // Calculate pupil position based on targetX, targetY
  // The pupil moves towards the target within a limited range
  let dx = targetX - irisX;
  let dy = targetY - irisY;
  let angle = atan2(dy, dx);
  let pupilOffset = irisSize * 0.2; // How far the pupil can move from the center
  let pupilX = irisX + cos(angle) * pupilOffset;
  let pupilY = irisY + sin(angle) * pupilOffset;

  // Draw the pupil
  fill(0); // Black pupil
  circle(pupilX, pupilY, pupilSize);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Flicker Glow Calculation let currentGlowAmount = baseGlowAmount + flickerAmount; // Apply flicker

Adds a small random offset each frame so the glow subtly flickers instead of staying static

calculation Pupil Direction Calculation let angle = atan2(dy, dx);

Finds the angle from the eye center to the target (mouse) so the pupil can point toward it

let pupilSize = irisSize * 0.4;
The pupil is always 40% the size of the iris, keeping proportions consistent no matter how big the eye is.
let flickerAmount = random(-irisSize * 0.1, irisSize * 0.1);
Picks a small random number every frame so the glow blur size jitters slightly, simulating a flickering light.
drawingContext.shadowBlur = currentGlowAmount;
Uses the raw HTML5 canvas API (via drawingContext) to add a soft blurred glow behind the next shape drawn.
drawingContext.shadowBlur = 0;
Turns the glow off again immediately after drawing the iris so the pupil and later shapes don't also glow.
let angle = atan2(dy, dx);
atan2 converts the difference in x/y between the eye and the mouse into an angle in radians, pointing at the target.
let pupilX = irisX + cos(angle) * pupilOffset;
Moves the pupil a fixed small distance in the direction of that angle, so it looks like it's gazing at the mouse without leaving the iris.

setup()

setup() runs once and is the right place to build your initial game data - here it seeds the starfield array and creates the two entity objects that draw() will read from every frame.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke(); // No outline for shapes

  // Initialize stars once in setup()
  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height),
      size: random(1, 4),
      alpha: random(100, 255) // Random transparency for twinkling
    });
  }

  // Define nebula colors
  nebulaColors.push(color(10, 0, 50, 100)); // Dark blue
  nebulaColors.push(color(50, 0, 100, 100)); // Purple
  nebulaColors.push(color(100, 0, 50, 100)); // Magenta
  nebulaColors.push(color(100, 50, 0, 100)); // Orange
  nebulaColors.push(color(0, 50, 100, 100)); // Cyan

  // Initialize entities
  entities.push({
    id: 1,
    x: width * 0.3,
    y: height * 0.5,
    bodyColor: color(50, 50, 100), // Dark blue-purple body color
    eyeColor: color(0, 255, 255), // Bright cyan eye color
    eyeSize: 60, // Size of the eyes
    eyeOffset: 40, // Distance between the two eyes
    eyeY: -30, // Vertical position of eyes relative to entity center
    boundingCircleRadius: 125, // Bounding radius for collision detection (half of ellipse width)
    isCut: false
  });

  entities.push({
    id: 2,
    x: width * 0.7,
    y: height * 0.5,
    bodyColor: color(100, 50, 50), // Dark red-brown body color
    eyeColor: color(255, 255, 0), // Bright yellow eye color
    eyeSize: 70, // Size of the eyes
    eyeOffset: 50, // Distance between the two eyes
    eyeY: -20, // Vertical position of eyes relative to entity center
    boundingCircleRadius: 110, // Bounding radius for collision detection (half of rect width)
    isCut: false
  });
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Star Generation Loop for (let i = 0; i < numStars; i++) {

Creates numStars random star objects with position, size, and transparency, storing them in the stars array

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
noStroke();
Turns off shape outlines globally so circles and rectangles are drawn as solid, clean fills.
stars.push({ x: random(width), y: random(height), size: random(1, 4), alpha: random(100, 255) });
Adds one star object with a random position, a small random size, and a random transparency value used later for twinkling.
nebulaColors.push(color(10, 0, 50, 100)); // Dark blue
Builds a palette of five semi-transparent colors that the nebula background will blend between.
entities.push({ id: 1, x: width * 0.3, y: height * 0.5, ... });
Creates the first monster as a plain JavaScript object holding its position, colors, eye layout, and collision radius.

resetEntity()

resetEntity() is the function that makes the game feel endless - instead of removing a cut monster, it relocates and re-randomizes it, which is a cheap and common trick for infinite spawning in small games.

🔬 Monsters currently only respawn in the middle 60-40% of the screen. What happens if you widen this to random(width * 0.05, width * 0.95) and random(height * 0.05, height * 0.95)?

  entity.x = random(width * 0.2, width * 0.8); // New random x position
  entity.y = random(height * 0.3, height * 0.7); // New random y position
function resetEntity(entity) {
  entity.isCut = false;
  entity.x = random(width * 0.2, width * 0.8); // New random x position
  entity.y = random(height * 0.3, height * 0.7); // New random y position
  // Optionally randomize colors/sizes for more variety
  entity.bodyColor = color(random(50, 150), random(50, 150), random(50, 150));
  entity.eyeColor = color(random(150, 255), random(150, 255), random(150, 255));
  entity.eyeSize = random(50, 80);
  entity.eyeOffset = random(30, 60);
  entity.eyeY = random(-40, -10);
  entity.boundingCircleRadius = entity.id === 1 ? random(100, 150) : random(90, 130);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional ID-based Radius Choice entity.boundingCircleRadius = entity.id === 1 ? random(100, 150) : random(90, 130);

Gives the two different monster shapes slightly different collision-radius ranges that suit their body shapes

entity.isCut = false;
Marks the entity as visible again so drawEntity() and the collision check will process it.
entity.x = random(width * 0.2, width * 0.8); // New random x position
Moves the entity to a new random horizontal spot, staying away from the extreme edges of the screen.
entity.bodyColor = color(random(50, 150), random(50, 150), random(50, 150));
Picks a new random muted body color each time the entity respawns, adding visual variety.
entity.boundingCircleRadius = entity.id === 1 ? random(100, 150) : random(90, 130);
Uses a ternary operator to give entity 1 (the horned ellipse) and entity 2 (the rectangle) different appropriate hit-radius ranges.

drawSpaceyBackground()

This function shows how noise(), lerpColor(), and blendMode() combine to fake a complex, animated nebula using only a handful of loops - a great pattern to reuse for any organic, cloud-like generative background.

🔬 There are currently 5 nebula layers drawn on top of each other. What happens if you change 5 to 15 - does the background get richer or too muddy?

  for (let i = 0; i < 5; i++) {
    // Lerp between nebula colors for variety
    let nebulaColor = lerpColor(nebulaColors[i % nebulaColors.length], nebulaColors[(i + 1) % nebulaColors.length], noise(timeOffset + i * 10));
    fill(nebulaColor);
function drawSpaceyBackground() {
  background(0); // Dark background for space

  // Draw stars
  for (let star of stars) {
    fill(255, star.alpha);
    circle(star.x, star.y, star.size);
  }

  // Draw nebulae using noise and blendMode
  push(); // Isolate blend mode and transformations for nebulae
  blendMode(SCREEN); // Blend mode for luminous nebulae (adds colors together)

  let nebulaDensity = 0.005; // How spread out the noise is for the nebulae
  let nebulaScale = 0.5; // Overall vertical size of nebulae
  let timeOffset = frameCount * 0.001; // Animate nebulae slowly over time

  // Draw multiple layers of nebulae
  for (let i = 0; i < 5; i++) {
    // Lerp between nebula colors for variety
    let nebulaColor = lerpColor(nebulaColors[i % nebulaColors.length], nebulaColors[(i + 1) % nebulaColors.length], noise(timeOffset + i * 10));
    fill(nebulaColor);

    beginShape();
    for (let x = 0; x <= width; x += 20) { // Draw points across the width
      // Use noise to create organic, cloud-like shapes
      let y = height / 2 +
              noise(x * nebulaDensity, i * 10, timeOffset) * height * nebulaScale -
              height * nebulaScale / 2;
      vertex(x, y);
    }
    vertex(width, height); // Close the shape at the bottom right
    vertex(0, height);     // Close the shape at the bottom left
    endShape(CLOSE);
  }

  pop(); // Reset blendMode to NORMAL and restore previous transformations
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Star Rendering Loop for (let star of stars) {

Draws every star at its stored position with its own random transparency, creating a twinkling field

for-loop Nebula Layer Loop for (let i = 0; i < 5; i++) {

Draws 5 overlapping noise-based cloud shapes, each with a blended color, to build up a nebula effect

for-loop Nebula Shape Vertex Loop for (let x = 0; x <= width; x += 20) { // Draw points across the width

Samples Perlin noise across the width of the screen to create an organic, wavy top edge for each nebula cloud

background(0); // Dark background for space
Repaints the whole canvas black every frame, which also erases the previous frame's drawing (no motion trails).
blendMode(SCREEN); // Blend mode for luminous nebulae (adds colors together)
Changes how new colors combine with what's already drawn - SCREEN brightens overlapping colors instead of covering them, giving a glowing look.
let timeOffset = frameCount * 0.001; // Animate nebulae slowly over time
Uses the ever-increasing frameCount to slowly shift the noise pattern each frame, making the nebula drift instead of staying frozen.
let nebulaColor = lerpColor(nebulaColors[i % nebulaColors.length], nebulaColors[(i + 1) % nebulaColors.length], noise(timeOffset + i * 10));
Blends between two palette colors using a noise value as the mix amount, so the color shifts smoothly and unpredictably over time.
let y = height / 2 + noise(x * nebulaDensity, i * 10, timeOffset) * height * nebulaScale - height * nebulaScale / 2;
Uses 3D Perlin noise (x position, layer index, time) to calculate a wavy vertical position, producing an organic cloud silhouette instead of straight lines.
pop(); // Reset blendMode to NORMAL and restore previous transformations
Restores the default blend mode so later drawing (entities, text) isn't affected by the SCREEN blend used for nebulae.

drawEntity()

drawEntity() is a great example of using push()/translate()/pop() to draw a compound shape (body + horns/teeth + eyes) in simple local coordinates instead of recalculating absolute positions for every part.

🔬 Each triangle uses three (x,y) points. What happens if you make the tip y-values more negative, like -220 instead of -175, to make the horns much taller?

    triangle(-50, -125, -75, -175, -25, -175); // Left horn
    triangle(50, -125, 75, -175, 25, -175);   // Right horn
function drawEntity(entity) {
  push(); // Isolate transformations for this entity
  translate(entity.x, entity.y); // Move to entity's position

  if (entity.id === 1) {
    // Draw Entity 1's body/head (an ellipse with horns)
    fill(entity.bodyColor);
    ellipse(0, 0, 200, 250); // Main body/head
    triangle(-50, -125, -75, -175, -25, -175); // Left horn
    triangle(50, -125, 75, -175, 25, -175);   // Right horn

    // Draw glowing eyes for Entity 1, tracking the mouse
    drawGlowingEye(-entity.eyeOffset, entity.eyeY, entity.eyeSize, entity.eyeColor, mouseX, mouseY);
    drawGlowingEye(entity.eyeOffset, entity.eyeY, entity.eyeSize, entity.eyeColor, mouseX, mouseY);
  } else if (entity.id === 2) {
    // Draw Entity 2's body/head (a rounded rectangle with teeth)
    fill(entity.bodyColor);
    rectMode(CENTER);
    rect(0, 0, 220, 200, 20); // Main body/head (rounded rectangle)

    // Draw teeth
    fill(200); // Light gray for teeth
    triangle(-70, 70, -30, 70, -50, 90);
    triangle(-20, 70, 20, 70, 0, 90);
    triangle(30, 70, 70, 70, 50, 90);

    // Draw glowing eyes for Entity 2, tracking the mouse
    drawGlowingEye(-entity.eyeOffset, entity.eyeY, entity.eyeSize, entity.eyeColor, mouseX, mouseY);
    drawGlowingEye(entity.eyeOffset, entity.eyeY, entity.eyeSize, entity.eyeColor, mouseX, mouseY);
  }
  pop(); // Restore previous transformations
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Entity Type Branch if (entity.id === 1) {

Chooses which body shape (ellipse-with-horns or rectangle-with-teeth) to draw based on the entity's id

translate(entity.x, entity.y); // Move to entity's position
Shifts the coordinate system so (0,0) is now the entity's position, letting all body parts be drawn with simple local coordinates.
ellipse(0, 0, 200, 250); // Main body/head
Draws the main head shape centered on the translated origin, since translate() already moved us to the entity's real position.
triangle(-50, -125, -75, -175, -25, -175); // Left horn
Draws a triangle horn using three (x,y) points defined relative to the entity's center.
drawGlowingEye(-entity.eyeOffset, entity.eyeY, entity.eyeSize, entity.eyeColor, mouseX, mouseY);
Calls the reusable eye-drawing function for the left eye, passing the live mouse position so the pupil tracks the cursor.
pop(); // Restore previous transformations
Undoes the translate() so the next thing drawn (another entity, or the UI) isn't accidentally offset.

draw()

draw() is the heart of every p5.js sketch, running ~60 times per second. Here it doubles as a tiny state machine - checking gameActive and gameOver flags to decide whether to show a menu, play the game, or show results - a pattern you'll reuse in almost any interactive project.

🔬 Each hit currently gives 1 point. What happens if you change score++ to score += 5 - how fast does the score climb in 20 seconds?

        if (lineCircle(pmouseX, pmouseY, mouseX, mouseY, entity.x, entity.y, entity.boundingCircleRadius)) {
          score++; // Increment score
          resetEntity(entity); // Reset the entity (make it reappear)
        }
function draw() {
  drawSpaceyBackground(); // Draw the spacey background first

  if (!gameActive) {
    // Start screen
    fill(255);
    textSize(40);
    textAlign(CENTER, CENTER);
    text("Click to Start\nCut the Entities!", width / 2, height / 2 - 50);
    textSize(20);
    text("Left-click and drag to swing your sword.", width / 2, height / 2 + 50);
    cursor(ARROW); // Show cursor on start screen
    return; // Stop draw loop if game not active
  }

  // Hide default mouse cursor during gameplay
  noCursor();

  // --- Timer Logic ---
  let elapsedTime = millis() - gameStartTime;
  let remainingTime = gameDuration - elapsedTime;

  if (remainingTime <= 0) {
    gameOver = true;
    remainingTime = 0;
  }

  // Display timer
  fill(255);
  textSize(32);
  textAlign(LEFT, TOP);
  text(`Time: ${nf(remainingTime / 1000, 1, 1)}s`, 20, 20);

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

  if (gameOver) {
    // Game Over screen
    fill(255, 0, 0); // Red color for game over text
    textSize(64);
    textAlign(CENTER, CENTER);
    text("GAME OVER!", width / 2, height / 2 - 50);
    fill(255);
    textSize(40);
    text(`Final Score: ${score}`, width / 2, height / 2 + 20);
    textSize(20);
    text("Click to play again!", width / 2, height / 2 + 80);
    cursor(ARROW); // Show cursor on game over screen
    return; // Stop draw loop if game is over
  }

  // Draw entities if they haven't been cut
  for (let entity of entities) {
    if (!entity.isCut) {
      drawEntity(entity);
    }
  }

  // --- Sword Logic ---
  if (mouseIsPressed && mouseButton === LEFT) {
    // Draw the glowing sword line
    stroke(255, 200, 0); // Orange-yellow color for the sword
    strokeWeight(5); // Thickness of the sword line
    drawingContext.shadowColor = color(255, 200, 0); // Glowing effect for the sword
    drawingContext.shadowBlur = 10;
    line(pmouseX, pmouseY, mouseX, mouseY); // Line from previous to current mouse position
    drawingContext.shadowBlur = 0; // Reset shadow blur
    noStroke(); // Reset stroke

    // Check for collision with entities
    for (let entity of entities) {
      // Only check collision if the entity is NOT already cut
      if (!entity.isCut) {
        // Use lineCircle for collision detection
        if (lineCircle(pmouseX, pmouseY, mouseX, mouseY, entity.x, entity.y, entity.boundingCircleRadius)) {
          score++; // Increment score
          resetEntity(entity); // Reset the entity (make it reappear)
        }
      }
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Start Screen Guard if (!gameActive) {

Shows the start screen and exits draw() early until the player clicks to begin

conditional Timeout Check if (remainingTime <= 0) {

Flips the game into the gameOver state once the countdown reaches zero

conditional Game Over Guard if (gameOver) {

Displays the final score screen and exits draw() early once time has run out

for-loop Entity Rendering Loop for (let entity of entities) {

Draws only the entities that haven't been cut yet

conditional Sword Drag Check if (mouseIsPressed && mouseButton === LEFT) {

Only draws the sword line and checks collisions while the player is holding down the left mouse button

for-loop Collision Detection Loop for (let entity of entities) {

Tests the current sword line segment against every uncut entity's bounding circle and scores a hit if it intersects

drawSpaceyBackground(); // Draw the spacey background first
Repaints the animated starfield and nebula at the start of every frame, before anything else is drawn on top.
if (!gameActive) {
If the game hasn't started, shows instructions and returns immediately, skipping the rest of draw() entirely for that frame.
let remainingTime = gameDuration - elapsedTime;
Subtracts how much time has passed since the game started from the total allowed duration to get the seconds left.
text(`Time: ${nf(remainingTime / 1000, 1, 1)}s`, 20, 20);
Converts milliseconds to seconds and formats it to one decimal place using nf(), then draws it in the top-left corner.
if (mouseIsPressed && mouseButton === LEFT) {
Only runs the sword-drawing and collision code while the player is actively holding down the left mouse button, turning drags into slashes.
line(pmouseX, pmouseY, mouseX, mouseY); // Line from previous to current mouse position
Draws a short glowing segment between where the mouse was last frame and where it is now, which visually becomes a continuous sword trail while dragging.
if (lineCircle(pmouseX, pmouseY, mouseX, mouseY, entity.x, entity.y, entity.boundingCircleRadius)) {
Calls the custom collision helper to check whether this frame's sword segment crosses the entity's invisible hit-circle.
score++; // Increment score
Adds one point every time the sword segment successfully intersects an uncut entity.

mousePressed()

mousePressed() is a built-in p5.js event function that fires once per click. Here it acts as the game's on/off switch, using the gameActive and gameOver flags to decide whether a click should start a new game or restart after a loss.

function mousePressed() {
  if (!gameActive) {
    gameActive = true;
    gameStartTime = millis();
    score = 0;
    gameOver = false;
    // Reset all entities at game start
    for (let entity of entities) {
      resetEntity(entity);
    }
  } else if (gameOver) {
    // Restart the game
    gameStartTime = millis();
    score = 0;
    gameOver = false;
    // Reset all entities at game restart
    for (let entity of entities) {
      resetEntity(entity);
    }
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Start Game Branch if (!gameActive) {

Begins the game the first time the player clicks, recording the start time and resetting entities

conditional Restart Game Branch } else if (gameOver) {

Lets the player click again after a game over to start a fresh round

gameActive = true;
Flips the flag that unlocks the main gameplay code in draw() instead of showing the start screen.
gameStartTime = millis();
Records the current time in milliseconds so draw() can later calculate how much time has elapsed since the round began.
for (let entity of entities) { resetEntity(entity); }
Repositions and re-randomizes both monsters so every new round starts with fresh entity placement.

pointOnLineSegment()

This is a small geometry utility used by lineCircle() to confirm that the closest point on an infinite line actually falls within the visible segment (and not off one end of it).

function pointOnLineSegment(px, py, x1, y1, x2, y2) {
  // Check if the point is within the bounding box of the line segment
  if (px < min(x1, x2) || px > max(x1, x2) || py < min(y1, y2) || py > max(y1, y2)) {
    return false;
  }
  // Check if the point lies on the line (cross product close to zero)
  const crossProduct = (py - y1) * (x2 - x1) - (px - x1) * (y2 - y1);
  const epsilon = 0.1; // Tolerance for floating point comparisons
  return abs(crossProduct) < epsilon;
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Bounding Box Rejection if (px < min(x1, x2) || px > max(x1, x2) || py < min(y1, y2) || py > max(y1, y2)) {

Quickly rejects points that fall outside the rectangular area spanned by the line segment's two endpoints

if (px < min(x1, x2) || px > max(x1, x2) || py < min(y1, y2) || py > max(y1, y2)) {
Cheaply checks whether the point is even inside the rectangle formed by the segment's endpoints before doing more expensive math.
const crossProduct = (py - y1) * (x2 - x1) - (px - x1) * (y2 - y1);
Computes a 2D cross product; it's zero only when the point lies exactly on the infinite line through the two endpoints.
return abs(crossProduct) < epsilon;
Because of floating-point rounding, we accept the point as 'on the line' if the cross product is very close to zero rather than exactly zero.

lineCircle()

This is the mathematical core of the game's hit detection - a classic line-segment-vs-circle test using vector projection. Understanding this function is a great introduction to 2D collision geometry beyond simple rectangle overlap checks.

function lineCircle(x1, y1, x2, y2, cx, cy, r) {
  // is either end inside the circle?
  const d1 = dist(cx, cy, x1, y1);
  const d2 = dist(cx, cy, x2, y2);
  if (d1 < r || d2 < r) return true;

  // get length of the line
  const len = dist(x1, y1, x2, y2);
  if (len === 0) return false; // Avoid division by zero for zero-length line

  // get dot product of the line and circle
  const dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / (len * len);

  // find the closest point on the line to the circle
  const closestX = x1 + (dot * (x2 - x1));
  const closestY = y1 + (dot * (y2 - y1));

  // is the closest point on the line segment?
  // if not, the point is outside the line segment, so we're done
  if (!pointOnLineSegment(closestX, closestY, x1, y1, x2, y2)) return false;

  // get distance to closest point
  const distance = dist(closestX, closestY, cx, cy);

  if (distance <= r) {
    return true;
  }
  return false;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Endpoint-Inside-Circle Check if (d1 < r || d2 < r) return true;

Fast-path check: if either end of the sword line is already inside the circle, it's definitely a hit

conditional Closest Point On Segment Check if (!pointOnLineSegment(closestX, closestY, x1, y1, x2, y2)) return false;

Rejects the collision if the mathematically closest point on the infinite line falls outside the actual drawn segment

const d1 = dist(cx, cy, x1, y1);
Measures the distance from the circle's center to the line segment's starting point.
if (d1 < r || d2 < r) return true;
If either endpoint of the sword swing is already inside the circle radius, we can immediately confirm a hit without more math.
const dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / (len * len);
Uses vector projection to find how far along the line segment (as a fraction from 0 to 1) the closest point to the circle's center lies.
const closestX = x1 + (dot * (x2 - x1));
Uses that projection fraction to calculate the actual (x, y) coordinate on the line that's nearest to the circle.
if (!pointOnLineSegment(closestX, closestY, x1, y1, x2, y2)) return false;
Makes sure that closest point is actually within the segment's endpoints, not off to one side on the infinite extended line.
if (distance <= r) { return true; }
If the closest point on the segment is within the circle's radius, the sword line intersects the circle - it's a hit.

windowResized()

windowResized() is a built-in p5.js callback that fires automatically whenever the browser window changes size, letting you keep responsive sketches looking correct instead of stretched or cropped.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-initialize stars for the new canvas size
  stars = [];
  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height),
      size: random(1, 4),
      alpha: random(100, 255)
    });
  }

  // Update entity positions for new canvas size if game not active or game over
  if (!gameActive || gameOver) {
    entities[0].x = width * 0.3;
    entities[0].y = height * 0.5;
    entities[1].x = width * 0.7;
    entities[1].y = height * 0.5;
  } else {
    // If game is active, just reset entities randomly
    for (let entity of entities) {
      resetEntity(entity);
    }
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Layout Reset Branch if (!gameActive || gameOver) {

Decides whether to snap entities back to their fixed start-screen positions or scatter them randomly, depending on whether a game is in progress

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window whenever it changes size.
stars = [];
Clears out the old star array completely so stars can be regenerated to fit the new canvas dimensions.
if (!gameActive || gameOver) {
If we're on the start screen or game-over screen, snaps the two entities back to their fixed showcase positions instead of leaving them randomly scattered.

📦 Key Variables

stars array

Holds objects describing each background star's position, size, and twinkle transparency

let stars = [];
numStars number

Determines how many stars are generated for the background

let numStars = 500;
nebulaColors array

Stores the palette of colors used to draw and blend the animated nebula clouds

let nebulaColors = [];
entities array

Stores the two monster objects, each with position, colors, eye settings, collision radius, and cut state

let entities = [];
score number

Tracks how many entities the player has successfully cut during the current round

let score = 0;
gameStartTime number

Stores the millis() timestamp when the current round began, used to calculate remaining time

let gameStartTime;
gameDuration number

Total length of a round in milliseconds, used with gameStartTime to compute the countdown

let gameDuration = 20000;
gameOver boolean

Flags whether the round has ended so draw() shows the game-over screen instead of gameplay

let gameOver = false;
gameActive boolean

Flags whether the game has been started, controlling whether draw() shows the start screen or gameplay

let gameActive = false;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG windowResized()

The function hardcodes entities[0] and entities[1] to reposition them, which breaks if the entities array ever contains a different number of monsters.

💡 Loop over the entities array generically (e.g. using entity.id or index-based fractional positions) instead of hardcoding array indices.

BUG draw() sword collision check

Collision is only tested between the previous and current mouse position each frame (pmouseX/pmouseY to mouseX/mouseY). If the mouse moves very fast, the sword can 'tunnel' past a monster without registering a hit.

💡 Interpolate several intermediate points between pmouseX/pmouseY and mouseX/mouseY (or check multiple sub-segments) when the distance moved is large, to avoid missed collisions during fast swipes.

STYLE setup() entity creation

The two entity objects are built with large, nearly duplicated object literals, and drawEntity() uses an if/else on entity.id to pick which shape to draw, which makes adding a third entity type tedious and error-prone.

💡 Refactor entities into a small factory function or ES6 class (e.g. class Monster) that takes a 'type' parameter, so new monster designs can be added without duplicating property lists or extending long if/else chains.

FEATURE draw() collision handling

Successfully cutting a monster gives no feedback beyond the score number changing - there's no particle burst, flash, or sound to make hits feel satisfying.

💡 Add a brief particle explosion or screen flash effect (and optionally a sound cue) when lineCircle() returns true, to make successful slashes feel more rewarding.

🔄 Code Flow

Code flow showing drawglowingeye, setup, resetentity, drawspaceybackground, drawentity, draw, mousepressed, pointonlinesegment, linecircle, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> start-screen-guard[start-screen-guard] start-screen-guard -->|if not clicked| draw start-screen-guard -->|if clicked| start-game-branch[start-game-branch] start-game-branch --> resetentity[resetentity] resetentity --> draw draw --> timeout-check[timeout-check] timeout-check -->|if countdown not zero| draw timeout-check -->|if countdown zero| gameover-guard[gameover-guard] gameover-guard -->|if game over| draw gameover-guard -->|if not game over| sword-input-conditional[sword-input-conditional] sword-input-conditional -->|if mouse down| collision-loop[collision-loop] collision-loop -->|check collisions| bbox-check[bbox-check] bbox-check -->|if outside| collision-loop bbox-check -->|if inside| endpoint-inside-check[endpoint-inside-check] endpoint-inside-check -->|if inside| collision-loop endpoint-inside-check -->|if outside| segment-bound-check[segment-bound-check] segment-bound-check -->|if outside| collision-loop segment-bound-check -->|if inside| drawentity[drawentity] drawentity --> entity-type-conditional[entity-type-conditional] entity-type-conditional -->|draw based on id| draw draw --> entity-draw-loop[entity-draw-loop] entity-draw-loop -->|draw entities| draw draw --> drawspaceybackground[drawspaceybackground] drawspaceybackground --> nebula-layer-loop[nebula-layer-loop] nebula-layer-loop -->|draw nebula layers| nebula-vertex-loop[nebula-vertex-loop] nebula-vertex-loop -->|create nebula shape| draw draw --> drawglowingeye[drawglowingeye] drawglowingeye --> eye-glow-shadow[eye-glow-shadow] eye-glow-shadow -->|flicker glow calculation| eye-pupil-tracking[eye-pupil-tracking] eye-pupil-tracking -->|pupil direction calculation| draw draw --> windowresized[windowresized] windowresized --> resize-state-conditional[resize-state-conditional] resize-state-conditional -->|if resizing| draw resize-state-conditional -->|if not resizing| draw click setup href "#fn-setup" click draw href "#fn-draw" click resetentity href "#fn-resetentity" click drawspaceybackground href "#fn-drawspaceybackground" click drawentity href "#fn-drawentity" click mousepressed href "#fn-mousepressed" click pointonlinesegment href "#fn-pointonlinesegment" click linecircle href "#fn-linecircle" click windowresized href "#fn-windowresized" click eye-glow-shadow href "#sub-eye-glow-shadow" click eye-pupil-tracking href "#sub-eye-pupil-tracking" click setup-star-loop href "#sub-setup-star-loop" click reset-radius-ternary href "#sub-reset-radius-ternary" click star-draw-loop href "#sub-star-draw-loop" click nebula-layer-loop href "#sub-nebula-layer-loop" click nebula-vertex-loop href "#sub-nebula-vertex-loop" click entity-type-conditional href "#sub-entity-type-conditional" click start-screen-guard href "#sub-start-screen-guard" click timeout-check href "#sub-timeout-check" click gameover-guard href "#sub-gameover-guard" click entity-draw-loop href "#sub-entity-draw-loop" click sword-input-conditional href "#sub-sword-input-conditional" click collision-loop href "#sub-collision-loop" click start-game-branch href "#sub-start-game-branch" click restart-game-branch href "#sub-restart-game-branch" click bbox-check href "#sub-bbox-check" click endpoint-inside-check href "#sub-endpoint-inside-check" click segment-bound-check href "#sub-segment-bound-check" click resize-state-conditional href "#sub-resize-state-conditional"

❓ Frequently Asked Questions

What visual elements are featured in the Try to get the highest score! p5.js sketch?

The sketch features a starry background with glowing eyes that track user input, creating an engaging and dynamic visual experience.

How can users interact with the Try to get the highest score! sketch?

Users can interact by moving their mouse to cut enemies within a 20-second timer, aiming to achieve the highest score possible.

What creative coding techniques are showcased in this p5.js sketch?

The sketch demonstrates techniques such as dynamic drawing with mouse tracking, shadow effects for glowing elements, and time-based game mechanics.

Preview

Try to get the highest score! - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Try to get the highest score! - Code flow showing drawglowingeye, setup, resetentity, drawspaceybackground, drawentity, draw, mousepressed, pointonlinesegment, linecircle, windowresized
Code Flow Diagram