have fun

This sketch creates an interactive physics simulation where clicking destroys falling blocks, shattering them into flying pieces while drawing cracks across the screen. As you accumulate clicks, the cracks intensify until the screen itself "breaks," revealing a grotesque face beneath with a dramatic jumpscare effect.

🧪 Try This!

Experiment with the code by making these changes:

  1. Create larger explosion pieces — Increase the size of fragments when blocks shatter by removing the /2 divisor in the piece size calculation.
  2. Trigger jumpscare with fewer clicks — Lower the threshold for the screen-breaking jumpscare effect so it triggers much sooner.
  3. Make blocks fall faster — Increase gravity so blocks plummet more quickly, making the game feel more intense and urgent.
  4. Slow the explosion force — Reduce how far pieces fly when clicked by lowering the explosion force magnitude.
  5. Increase the initial stack of blocks — Create a taller starting pile so you have more to smash before triggering the jumpscare.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch combines p5.js graphics with Matter.js physics to create a satisfying destruction game. You click on falling blocks to shatter them into smaller pieces that fly outward from the impact point. Each click adds a visible crack to the screen itself, and when you reach enough cracks, the entire screen breaks dramatically to reveal a distorted face—a full jumpscare sequence using glitch effects, red flashes, and animated screen transitions.

The code is organized into three main systems: physics simulation (using Matter.js bodies and forces), visual crack generation and rendering (storing line segments and drawing them with increasing thickness), and a multi-stage jumpscare animation (automated cracking, falling screen piece, and face reveal). By studying it, you'll learn how to integrate an external physics library with p5.js, build cumulative visual effects, and orchestrate a timed animation sequence with multiple stages.

⚙️ How It Works

  1. When the sketch loads, setup() initializes a Matter.js physics engine with gravity, creates a ground and two side walls, and stacks 50 random blocks (circles and rectangles) at the top of the screen. The draw loop is paused initially, waiting for user interaction.
  2. When you click the mouse, mousePressed() finds the nearest block to your click and calls shatterBody(), which removes the original block and creates 15 smaller pieces that fly outward with physics forces applied.
  3. Each click also increments crackStrength and generates new crack segments (white lines radiating from the click point) that are drawn on top of the canvas every frame, growing thicker as crackStrength increases.
  4. When crackStrength reaches the threshold (20 clicks), activateJumpscare() triggers a multi-stage animation sequence: first, 50 large cracks are automatically generated across the screen with a red background flash; second, a dark rectangle falls away from the top to reveal content beneath; third, a grotesque distorted face is drawn with glitch effects (random shearing, scaling, and color shifts) while distorted text appears.
  5. After the jumpscare holds for 1 second, the screen resets, crack strength returns to zero, and the game restarts with fresh blocks and the instruction text visible again.

🎓 Concepts You'll Learn

Physics engine integration (Matter.js)Dynamic object spawning and removalCollision detection and impact forcesCumulative visual effects (cracks)Multi-stage animation sequencesGlitch effects and visual distortion

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the physics engine, creates static boundaries, and populates the world with objects. The noLoop() at the end keeps the screen frozen until the user interacts—a nice way to show instructions without animation running.

function setup() {
  createCanvas(windowWidth, windowHeight);

  // Initialize Matter.js engine and world
  engine = Engine.create();
  world = engine.world;
  world.gravity.scale = 0.001; // Reduced gravity for a more deliberate shatter effect

  // Create the ground
  ground = Bodies.rectangle(width / 2, height, width, 50, { isStatic: true });
  Composite.add(world, ground);

  // Create side walls
  let wallThickness = 20;
  walls.push(Bodies.rectangle(0, height / 2, wallThickness, height, { isStatic: true }));
  walls.push(Bodies.rectangle(width, height / 2, wallThickness, height, { isStatic: true }));
  Composite.add(world, walls);

  // Add an initial pile of objects that can shatter
  for (let i = 0; i < 50; i++) {
    addShatterableBody(width / 2, height - 100 - i * 20); // Stack them up
  }

  // Draw initial instructions
  textAlign(CENTER, CENTER);
  textSize(32);
  fill(255);
  noStroke();
  text('Click or drag your mouse to smash objects!', width / 2, height / 2);

  // Stop the draw loop until the first mouse interaction
  noLoop();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Physics Engine Initialization engine = Engine.create(); world = engine.world; world.gravity.scale = 0.001;

Creates a Matter.js physics engine and world with very low gravity to keep blocks suspended longer

calculation Static Boundary Setup ground = Bodies.rectangle(width / 2, height, width, 50, { isStatic: true }); Composite.add(world, ground); // Create side walls let wallThickness = 20; walls.push(Bodies.rectangle(0, height / 2, wallThickness, height, { isStatic: true })); walls.push(Bodies.rectangle(width, height / 2, wallThickness, height, { isStatic: true })); Composite.add(world, walls);

Creates immobile ground and wall bodies that blocks collide with but cannot move

for-loop Initial Block Stack for (let i = 0; i < 50; i++) { addShatterableBody(width / 2, height - 100 - i * 20); // Stack them up }

Creates 50 blocks stacked vertically, each 20 pixels higher than the last

createCanvas(windowWidth, windowHeight);
Creates a full-window canvas that responds to the browser's dimensions
engine = Engine.create();
Instantiates a new Matter.js physics engine—this object will calculate all physics updates
world = engine.world;
Retrieves the physics world from the engine—bodies added here will experience gravity and collisions
world.gravity.scale = 0.001;
Sets gravity to a very low value (normally 0.001, which is 1/1000th of default) so blocks fall slowly and deliberately
ground = Bodies.rectangle(width / 2, height, width, 50, { isStatic: true });
Creates a static (non-moving) rectangular body at the bottom of the canvas to act as the ground
Composite.add(world, ground);
Adds the ground body to the physics world so it participates in collisions
walls.push(Bodies.rectangle(0, height / 2, wallThickness, height, { isStatic: true }));
Creates a static wall on the left edge (x=0) and adds it to the walls array
for (let i = 0; i < 50; i++) {
Loops 50 times to create a stack of blocks
addShatterableBody(width / 2, height - 100 - i * 20);
Calls addShatterableBody() to create a random block; each iteration i stacks blocks 20 pixels higher (i * 20)
noLoop();
Stops the draw loop from running automatically—it will only run when the user clicks

draw()

draw() runs 60 times per second and is the heartbeat of the sketch. It updates physics, draws all visible objects, and performs cleanup. The jumpscare check at the top creates a fork in logic—either animate the jumpscare or simulate normal physics.

🔬 This loop removes bodies that fall off the bottom. What happens if you change > height + 50 to > height + 500? To < 0 (removing bodies that go above the screen)?

    // Remove bodies that fall off-screen
    for (let i = bodies.length - 1; i >= 0; i--) {
      let body = bodies[i];
      if (body.position.y > height + 50) {
function draw() {
  background(20, 20, 40); // Dark background

  // If jumpscare is active, handle its animation steps
  if (jumpscareActive) {
    handleJumpscareAnimation();
  } else {
    // Update the physics engine
    Engine.update(engine);

    // Draw the ground and walls
    fill(100);
    noStroke();
    drawBody(ground);
    for (let wall of walls) {
      drawBody(wall);
    }

    // Draw all other bodies
    for (let body of shatterables) {
      drawBody(body);
    }

    // Remove bodies that fall off-screen
    for (let i = bodies.length - 1; i >= 0; i--) {
      let body = bodies[i];
      if (body.position.y > height + 50) {
        Composite.remove(world, body);
        bodies.splice(i, 1);
        // Also remove from shatterables if it was one
        let shatterableIndex = shatterables.indexOf(body);
        if (shatterableIndex > -1) {
          shatterables.splice(shatterableIndex, 1);
        }
      }
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Jumpscare Check if (jumpscareActive) { handleJumpscareAnimation(); } else {

Routes execution to either the jumpscare animation or normal physics simulation

calculation Physics Engine Update Engine.update(engine);

Advances the physics simulation by one frame, calculating all forces, collisions, and position updates

for-loop Draw All Shatterable Bodies for (let body of shatterables) { drawBody(body); }

Iterates through every shatterable block and draws it at its current physics-calculated position

for-loop Remove Off-Screen Bodies for (let i = bodies.length - 1; i >= 0; i--) { let body = bodies[i]; if (body.position.y > height + 50) { Composite.remove(world, body); bodies.splice(i, 1); // Also remove from shatterables if it was one let shatterableIndex = shatterables.indexOf(body); if (shatterableIndex > -1) { shatterables.splice(shatterableIndex, 1); } } }

Cleans up physics bodies that have fallen below the screen to avoid memory leaks

background(20, 20, 40);
Clears the canvas to a dark blue-gray color, erasing the previous frame so animations appear smooth
if (jumpscareActive) {
Checks if the jumpscare sequence is currently playing
handleJumpscareAnimation();
If jumpscare is active, runs the jumpscare animation sequence instead of normal physics
Engine.update(engine);
Tells Matter.js to calculate one frame of physics—blocks fall, collide, and bounce
fill(100); noStroke();
Sets fill color to gray and disables outlines for drawing static bodies
drawBody(ground); for (let wall of walls) { drawBody(wall); }
Draws the ground and both side walls using the drawBody() helper function
for (let body of shatterables) { drawBody(body); }
Loops through every block in the shatterables array and draws it at its current position
for (let i = bodies.length - 1; i >= 0; i--) {
Loops backward through the bodies array (backward prevents index errors when splicing)
if (body.position.y > height + 50) {
Checks if a body has fallen 50 pixels below the bottom of the screen
Composite.remove(world, body); bodies.splice(i, 1);
Removes the body from the physics world and from the bodies array to free memory

mousePressed()

mousePressed() fires once each time the user clicks. It is the main interaction point: it finds the nearest block, shatters it, accumulates cracks, and checks for the jumpscare trigger. The early return for jumpscareActive ensures clicking during jumpscare exits cleanly.

🔬 This loop finds the closest block to your click by comparing distances. What happens if you change the if condition to distance > minDistance instead of <? Which block would be selected then?

  for (let body of shatterables) {
    let bodyPos = createVector(body.position.x, body.position.y);
    let distance = mousePos.dist(bodyPos);
    if (distance < minDistance) {
      minDistance = distance;
      closestBody = body;
    }
  }
function mousePressed() {
  if (jumpscareActive) {
    // If jumpscare is active, simply reset and allow the drawJumpscare to handle duration
    jumpscareActive = false;
    crackStrength = 0;
    crackSegments = [];
    background(20, 20, 40);
    textAlign(CENTER, CENTER);
    textSize(32);
    fill(255);
    noStroke();
    text('Click or drag your mouse to smash objects!', width / 2, height / 2);
    loop(); // Resume loop if it was stopped
    jumpscareStep = 0; // Reset step for next jumpscare
    automatedCrackCounter = 0; // Reset automated crack counter
    return;
  }

  // Restart the draw loop
  loop();

  // Find the closest shatterable body to the mouse click
  let mousePos = createVector(mouseX, mouseY);
  let closestBody = null;
  let minDistance = Infinity;

  for (let body of shatterables) {
    let bodyPos = createVector(body.position.x, body.position.y);
    let distance = mousePos.dist(bodyPos);
    if (distance < minDistance) {
      minDistance = distance;
      closestBody = body;
    }
  }

  // If a shatterable body is found close enough, remove it and add pieces
  if (closestBody && minDistance < SHATTER_MAX_RADIUS * 1.5) { // Increase radius for easier smashing
    shatterBody(closestBody, mousePos);
  } else {
    // If no body is hit, just add a new one at the mouse position
    addShatterableBody(mouseX, mouseY);
  }

  // Increment crack strength and check for jumpscare trigger
  crackStrength++;
  if (crackStrength >= MAX_CRACK_STRENGTH && !jumpscareActive) {
    activateJumpscare();
  } else if (!jumpscareActive) {
    // Only generate cracks if not jumpscaring and not reached threshold yet
    generateNewCracks(3, mouseX, mouseY);
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional Jumpscare Reset Branch if (jumpscareActive) { jumpscareActive = false; crackStrength = 0; crackSegments = []; background(20, 20, 40); textAlign(CENTER, CENTER); textSize(32); fill(255); noStroke(); text('Click or drag your mouse to smash objects!', width / 2, height / 2); loop(); jumpscareStep = 0; automatedCrackCounter = 0; return;

If a jumpscare is playing, clicking exits it immediately and resets all state

conditional Impact and Hit Detection if (closestBody && minDistance < SHATTER_MAX_RADIUS * 1.5) { shatterBody(closestBody, mousePos); } else { addShatterableBody(mouseX, mouseY); }

If a block was clicked close enough, shatter it; otherwise create a new block at the click point

conditional Crack Accumulation and Jumpscare Trigger crackStrength++; if (crackStrength >= MAX_CRACK_STRENGTH && !jumpscareActive) { activateJumpscare(); } else if (!jumpscareActive) { generateNewCracks(3, mouseX, mouseY); }

Increments crack strength; if threshold is reached, triggers jumpscare; otherwise adds visible cracks

if (jumpscareActive) {
Checks if the jumpscare is currently playing
jumpscareActive = false;
Stops the jumpscare animation
crackStrength = 0; crackSegments = [];
Resets cracks to zero and clears all crack line segments
text('Click or drag your mouse to smash objects!', width / 2, height / 2);
Redisplays the instruction text at the center of the screen
loop();
Restarts the draw() loop so animation resumes
let mousePos = createVector(mouseX, mouseY);
Creates a vector from the mouse's x and y coordinates for distance calculations
for (let body of shatterables) {
Loops through every shatterable block to find distances
let distance = mousePos.dist(bodyPos);
Calculates the distance from the mouse click to this block using p5.Vector.dist()
if (distance < minDistance) {
If this block is closer than previous ones, update closest body and minimum distance
if (closestBody && minDistance < SHATTER_MAX_RADIUS * 1.5) {
Checks if a block was found AND if it's within 1.5× the max radius (the hit zone)
shatterBody(closestBody, mousePos);
Destroys the hit block and creates 15 smaller pieces flying outward
crackStrength++;
Increments the crack accumulator—each click adds 1 to this counter
if (crackStrength >= MAX_CRACK_STRENGTH && !jumpscareActive) {
If crack strength reaches 20 (the max) and jumpscare is not already playing, trigger jumpscare
generateNewCracks(3, mouseX, mouseY);
Otherwise, generate 3 new crack line segments radiating from the click point

shatterBody(body, impactPos)

shatterBody() is the destruction function—it removes the hit block and replaces it with 15 smaller pieces, each scattered within the original block's bounds and pushed outward with physics forces. The pieces are also added to the shatterables array, so you can click on them again for chain reactions.

🔬 These three lines calculate the outward explosion. What happens if you multiply by a negative number, like forceDirection.mult(-SHATTER_FORCE_MAGNITUDE)? Which direction do the pieces go then?

    let forceDirection = p5.Vector.sub(createVector(piece.position.x, piece.position.y), impactPos);
    forceDirection.normalize();
    forceDirection.mult(SHATTER_FORCE_MAGNITUDE);
function shatterBody(body, impactPos) {
  // Remove the original body from the world and shatterables array
  Composite.remove(world, body);
  let bodyIndex = bodies.indexOf(body);
  if (bodyIndex > -1) bodies.splice(bodyIndex, 1);
  let shatterableIndex = shatterables.indexOf(body);
  if (shatterableIndex > -1) shatterables.splice(shatterableIndex, 1);

  // Create multiple smaller pieces
  for (let i = 0; i < SHATTER_PIECES_COUNT; i++) {
    let piece;
    let pieceSize = random(SHATTER_MIN_RADIUS / 2, SHATTER_MAX_RADIUS / 2);
    let pieceX = body.position.x + random(-body.bounds.max.x + body.position.x, body.bounds.max.x - body.position.x);
    let pieceY = body.position.y + random(-body.bounds.max.y + body.position.y, body.bounds.max.y - body.position.y);

    if (random(1) < 0.5) {
      piece = Bodies.circle(pieceX, pieceY, pieceSize, { restitution: 0.7 });
    } else {
      piece = Bodies.rectangle(pieceX, pieceY, pieceSize * 2, pieceSize * 2, { restitution: 0.7 });
    }

    Composite.add(world, piece);
    bodies.push(piece);
    shatterables.push(piece); // Add new pieces back to shatterables array
    // This allows the newly created pieces to be smashed again!

    // Apply an outward force to each piece from the impact point
    let forceDirection = p5.Vector.sub(createVector(piece.position.x, piece.position.y), impactPos);
    forceDirection.normalize();
    forceDirection.mult(SHATTER_FORCE_MAGNITUDE);
    Body.applyForce(piece, piece.position, {
      x: forceDirection.x,
      y: forceDirection.y
    });
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Remove Original Body Composite.remove(world, body); let bodyIndex = bodies.indexOf(body); if (bodyIndex > -1) bodies.splice(bodyIndex, 1); let shatterableIndex = shatterables.indexOf(body); if (shatterableIndex > -1) shatterables.splice(shatterableIndex, 1);

Removes the original block from the physics world and both tracking arrays

for-loop Piece Creation Loop for (let i = 0; i < SHATTER_PIECES_COUNT; i++) {

Repeats 15 times (SHATTER_PIECES_COUNT) to create 15 smaller pieces

conditional Random Shape Selection if (random(1) < 0.5) { piece = Bodies.circle(pieceX, pieceY, pieceSize, { restitution: 0.7 }); } else { piece = Bodies.rectangle(pieceX, pieceY, pieceSize * 2, pieceSize * 2, { restitution: 0.7 }); }

Each piece is randomly either a circle or rectangle, 50/50 chance

calculation Outward Force Vector Calculation let forceDirection = p5.Vector.sub(createVector(piece.position.x, piece.position.y), impactPos); forceDirection.normalize(); forceDirection.mult(SHATTER_FORCE_MAGNITUDE); Body.applyForce(piece, piece.position, { x: forceDirection.x, y: forceDirection.y });

Calculates a direction away from the impact point and applies a physics force, making pieces fly outward

Composite.remove(world, body);
Removes the original body from the Matter.js physics world so it no longer participates in collisions
let bodyIndex = bodies.indexOf(body);
Searches the bodies array for the index of the original body
if (bodyIndex > -1) bodies.splice(bodyIndex, 1);
If found (index is not -1), removes it from the bodies array
let shatterableIndex = shatterables.indexOf(body);
Searches the shatterables array for the original body's index
if (shatterableIndex > -1) shatterables.splice(shatterableIndex, 1);
If found, removes it from the shatterables array
for (let i = 0; i < SHATTER_PIECES_COUNT; i++) {
Loops 15 times (SHATTER_PIECES_COUNT) to create 15 pieces
let pieceSize = random(SHATTER_MIN_RADIUS / 2, SHATTER_MAX_RADIUS / 2);
Picks a random size for this piece—between half the min and half the max radius
let pieceX = body.position.x + random(-body.bounds.max.x + body.position.x, body.bounds.max.x - body.position.x);
Calculates a random x position within the original body's bounding box so pieces spawn near the impact
if (random(1) < 0.5) {
Generates a random number 0–1; if less than 0.5, create a circle, otherwise a rectangle
piece = Bodies.circle(pieceX, pieceY, pieceSize, { restitution: 0.7 });
Creates a circular piece with restitution 0.7 (bounciness between 0 and 1, so 0.7 means fairly bouncy)
Composite.add(world, piece); bodies.push(piece); shatterables.push(piece);
Adds the new piece to the physics world and both tracking arrays so it can be drawn and shattered again
let forceDirection = p5.Vector.sub(createVector(piece.position.x, piece.position.y), impactPos);
Calculates a vector from the impact point to the piece's position (the direction away from impact)
forceDirection.normalize();
Scales the direction vector to length 1 so all pieces receive the same force magnitude regardless of distance
forceDirection.mult(SHATTER_FORCE_MAGNITUDE);
Multiplies the direction by 0.05 (SHATTER_FORCE_MAGNITUDE) to create the final force vector
Body.applyForce(piece, piece.position, { x: forceDirection.x, y: forceDirection.y });
Applies the force to the piece, pushing it outward from the impact point

generateNewCracks(numSegments, x, y)

generateNewCracks() builds the visible crack effect by pushing line segments into the crackSegments array. Each segment radiates outward from the click point in a random direction (using cos/sin trigonometry). The cracks grow longer as crackStrength increases, building visual intensity.

function generateNewCracks(numSegments, x, y) {
  for (let i = 0; i < numSegments; i++) {
    let angle = random(TWO_PI);
    let length = random(5, 50 + crackStrength * 2); // Cracks get longer
    let x2 = x + cos(angle) * length;
    let y2 = y + sin(angle) * length;
    crackSegments.push(x, y, x2, y2);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Crack Segment Generation Loop for (let i = 0; i < numSegments; i++) {

Repeats numSegments times (usually 3) to create multiple crack lines per click

calculation Radial Line Calculation let angle = random(TWO_PI); let length = random(5, 50 + crackStrength * 2); let x2 = x + cos(angle) * length; let y2 = y + sin(angle) * length;

Creates a random line emanating from the click point using trigonometry (angle and distance)

for (let i = 0; i < numSegments; i++) {
Loops numSegments times (3 when called from mousePressed) to create multiple cracks per click
let angle = random(TWO_PI);
Generates a random angle from 0 to 2π radians (0 to 360 degrees), ensuring cracks point in all directions
let length = random(5, 50 + crackStrength * 2);
Random crack length between 5 and 50 pixels, plus up to 40 more pixels if crackStrength is high (grows with clicks)
let x2 = x + cos(angle) * length;
Calculates the end x coordinate of the crack line using cosine and the angle
let y2 = y + sin(angle) * length;
Calculates the end y coordinate using sine (trigonometry converts angle to x/y direction)
crackSegments.push(x, y, x2, y2);
Adds four values to the crackSegments array: the start point (x, y) and end point (x2, y2) as a line segment

drawCracks()

drawCracks() is called every frame and renders all accumulated crack segments stored in the crackSegments array. The stroke weight grows with crackStrength, making cracks visually thicken as the screen gets more damaged. Each line segment is stored as four values (x1, y1, x2, y2) and retrieved by stepping through the array in groups of 4.

function drawCracks() {
  if (crackStrength === 0) return;

  stroke(255); // White cracks
  strokeWeight(crackStrength / (MAX_CRACK_STRENGTH / 5)); // Cracks get thicker

  for (let i = 0; i < crackSegments.length; i += 4) {
    line(crackSegments[i], crackSegments[i+1], crackSegments[i+2], crackSegments[i+3]);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Early Exit If No Cracks if (crackStrength === 0) return;

Skips drawing if there are no cracks yet (optimization)

for-loop Crack Line Drawing Loop for (let i = 0; i < crackSegments.length; i += 4) { line(crackSegments[i], crackSegments[i+1], crackSegments[i+2], crackSegments[i+3]); }

Iterates through crackSegments in groups of 4 (each line has 2 points with 2 coordinates) and draws each segment

if (crackStrength === 0) return;
If no cracks have been generated yet, exit early without drawing anything (fast optimization)
stroke(255);
Sets the stroke color to white (RGB: 255, 255, 255)
strokeWeight(crackStrength / (MAX_CRACK_STRENGTH / 5));
Sets line thickness proportional to crackStrength—as you click more, cracks get thicker (dividing by 4 because MAX_CRACK_STRENGTH is 20, so max weight is 5)
for (let i = 0; i < crackSegments.length; i += 4) {
Loops through crackSegments, incrementing by 4 each time (because each line is stored as [x1, y1, x2, y2])
line(crackSegments[i], crackSegments[i+1], crackSegments[i+2], crackSegments[i+3]);
Draws a line from (x1, y1) to (x2, y2) where x1=index i, y1=i+1, x2=i+2, y2=i+3

addShatterableBody(x, y)

addShatterableBody() is a factory function that creates a new block at a given position. It randomly chooses between a circle and rectangle shape, then adds the body to the physics world and tracking arrays. This function is called during setup() to create the initial stack and also from mousePressed() when you miss a block.

function addShatterableBody(x, y) {
  let body;
  if (random(1) < 0.5) {
    let radius = random(SHATTER_MIN_RADIUS, SHATTER_MAX_RADIUS);
    body = Bodies.circle(x, y, radius, { restitution: 0.8 }); // Make them bouncy
  } else {
    let w = random(SHATTER_MIN_RADIUS * 2, SHATTER_MAX_RADIUS * 2);
    let h = random(SHATTER_MIN_RADIUS * 2, SHATTER_MAX_RADIUS * 2);
    body = Bodies.rectangle(x, y, w, h, { restitution: 0.8 }); // Make them bouncy
  }
  Composite.add(world, body);
  bodies.push(body);
  shatterables.push(body); // Add to shatterables array
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Random Shape Selection if (random(1) < 0.5) { let radius = random(SHATTER_MIN_RADIUS, SHATTER_MAX_RADIUS); body = Bodies.circle(x, y, radius, { restitution: 0.8 }); } else { let w = random(SHATTER_MIN_RADIUS * 2, SHATTER_MAX_RADIUS * 2); let h = random(SHATTER_MIN_RADIUS * 2, SHATTER_MAX_RADIUS * 2); body = Bodies.rectangle(x, y, w, h, { restitution: 0.8 }); }

Randomly creates either a circle or rectangle with bounciness (restitution) set to 0.8

if (random(1) < 0.5) {
Generates a random number 0–1; if less than 0.5 (50% chance), create a circle
let radius = random(SHATTER_MIN_RADIUS, SHATTER_MAX_RADIUS);
Picks a random radius between 10 and 30 pixels for the circle
body = Bodies.circle(x, y, radius, { restitution: 0.8 });
Creates a circular Matter.js body at position (x, y) with bounciness 0.8 (fairly springy)
let w = random(SHATTER_MIN_RADIUS * 2, SHATTER_MAX_RADIUS * 2);
Picks a random width between 20 and 60 pixels for the rectangle
let h = random(SHATTER_MIN_RADIUS * 2, SHATTER_MAX_RADIUS * 2);
Picks a random height between 20 and 60 pixels for the rectangle
body = Bodies.rectangle(x, y, w, h, { restitution: 0.8 });
Creates a rectangular Matter.js body with the random width and height
Composite.add(world, body);
Adds the new body to the physics world so it participates in gravity and collisions
bodies.push(body); shatterables.push(body);
Adds the body to both the bodies and shatterables arrays for tracking and cleanup

drawBody(body)

drawBody() takes a Matter.js body and renders it using p5.js. It uses push()/pop() to isolate transformations, translate() to move the origin to the body's position, and rotate() to match the body's angle. The vertex loop traces the body's polygon shape and closes it with endShape(CLOSE). This is the bridge between the physics engine and the visual renderer.

function drawBody(body) {
  push();
  translate(body.position.x, body.position.y);
  rotate(body.angle);
  beginShape();
  for (let v of body.vertices) {
    vertex(v.x - body.position.x, v.y - body.position.y);
  }
  endShape(CLOSE);
  pop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Coordinate Transformation push(); translate(body.position.x, body.position.y); rotate(body.angle);

Moves the drawing origin to the body's position and rotates the coordinate system to match the body's rotation

for-loop Vertex Drawing Loop for (let v of body.vertices) { vertex(v.x - body.position.x, v.y - body.position.y); }

Iterates through all vertices of the body's polygon and draws them relative to the translated origin

push();
Saves the current transformation state (position, rotation) so changes don't affect later drawings
translate(body.position.x, body.position.y);
Moves the drawing origin (0, 0) to the body's physics position
rotate(body.angle);
Rotates the coordinate system by the body's angle so the shape is drawn rotated correctly
beginShape();
Starts recording vertices to form a polygon
for (let v of body.vertices) {
Loops through every vertex in the body's polygon (Matter.js stores shapes as arrays of vertices)
vertex(v.x - body.position.x, v.y - body.position.y);
Records a vertex relative to the translated origin (subtracting position because we already translated)
endShape(CLOSE);
Closes the polygon by drawing a line from the last vertex back to the first
pop();
Restores the previous transformation state so the origin and rotation return to normal

handleJumpscareAnimation()

handleJumpscareAnimation() orchestrates a three-stage visual sequence using a switch statement on jumpscareStep. Stage 0 rapidly generates large cracks; stage 1 animates a black rectangle falling away while the face appears underneath; stage 2 holds the face visible until the duration expires. The red background and glitch effects run throughout all stages for maximum horror impact.

function handleJumpscareAnimation() {
  // Red flash for the duration of the jumpscare
  background(255, 0, 0);

  // Distorted text
  textAlign(CENTER, CENTER);
  textSize(64);
  fill(255);
  noStroke();

  push();
  // Apply random distortions for glitch effect
  shearX(random(-0.1, 0.1));
  shearY(random(-0.1, 0.1));
  translate(random(-10, 10), random(-10, 10));
  scale(random(0.95, 1.05));
  text('!!! SMASHED !!!', width / 2, height / 2 - 50);
  pop();

  switch (jumpscareStep) {
    case 0: // Automated Cracking
      if (automatedCrackCounter < AUTOMATED_CRACK_COUNT) {
        generateAutomatedCrack();
        automatedCrackCounter++;
      } else {
        crackSegments = []; // Clear automated cracks after this step
        screenFallingOffRect = { x: width / 4, y: -200, w: width / 2, h: height / 2 };
        jumpscareStep = 1; // Move to screen falling off
        jumpscareStartTime = millis(); // Reset time for next step
      }
      break;
    case 1: // Screen Falling Off
      // Draw the cracks generated in step 0
      drawCracks();

      // Simulate a part of the screen falling off
      let fallingProgress = (millis() - jumpscareStartTime) / (JUMPSCARE_DURATION / 2);
      screenFallingOffRect.y = lerp(-screenFallingOffRect.h, height, fallingProgress);

      push();
      translate(screenFallingOffRect.x, screenFallingOffRect.y);
      noStroke();
      fill(20, 20, 40); // Match background color for the falling piece
      rect(0, 0, screenFallingOffRect.w, screenFallingOffRect.h);
      pop();

      // Draw the grotesque face revealed underneath
      push();
      translate(width / 2, height / 2);
      scale(1.2); // Make the face slightly larger
      drawGrotesqueFace();
      pop();

      if (fallingProgress >= 1) {
        jumpscareStep = 2; // Move to face reveal
        jumpscareStartTime = millis(); // Reset time for final step
      }
      break;
    case 2: // Face Reveal & Hold
      // Draw the cracks generated in step 0
      drawCracks();

      // Draw the grotesque face revealed underneath
      push();
      translate(width / 2, height / 2);
      scale(1.2); // Make the face slightly larger
      drawGrotesqueFace();
      pop();

      // Check if jumpscare duration is over
      if (millis() - jumpscareStartTime > JUMPSCARE_DURATION / 2) { // Hold for remaining half duration
        jumpscareActive = false;
        crackStrength = 0; // Reset cracks
        crackSegments = []; // Clear crack segments
        background(20, 20, 40); // Clear screen
        // Redraw initial instructions
        textAlign(CENTER, CENTER);
        textSize(32);
        fill(255);
        noStroke();
        text('Click or drag your mouse to smash objects!', width / 2, height / 2);
        // Restart loop for normal physics
        loop();
        jumpscareStep = 0; // Reset step for next jumpscare
        automatedCrackCounter = 0; // Reset automated crack counter
      }
      break;
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Red Flash Background background(255, 0, 0);

Fills the entire screen with bright red

calculation Glitched Text Rendering push(); // Apply random distortions for glitch effect shearX(random(-0.1, 0.1)); shearY(random(-0.1, 0.1)); translate(random(-10, 10), random(-10, 10)); scale(random(0.95, 1.05)); text('!!! SMASHED !!!', width / 2, height / 2 - 50); pop();

Applies random skewing, shifting, and scaling to text each frame for a glitch effect

switch-case Three-Stage Animation State Machine switch (jumpscareStep) { case 0: // ... case 1: // ... case 2: // ... }

Routes execution through three stages: automated cracking, screen falling, and face reveal

background(255, 0, 0);
Clears the entire canvas with bright red (RGB: 255 red, 0 green, 0 blue)
shearX(random(-0.1, 0.1)); shearY(random(-0.1, 0.1));
Applies random horizontal and vertical skewing distortions each frame for a glitch effect
translate(random(-10, 10), random(-10, 10));
Randomly shifts the text position by up to ±10 pixels each frame
scale(random(0.95, 1.05));
Randomly scales the text between 95% and 105% of normal size each frame
if (automatedCrackCounter < AUTOMATED_CRACK_COUNT) {
In case 0 (Automated Cracking), checks if we've generated fewer than 50 cracks yet
generateAutomatedCrack();
Generates one large random crack segment and adds it to crackSegments
automatedCrackCounter++;
Increments the counter so next frame we know how many cracks have been generated
crackSegments = [];
Clears all crack segments once 50 have been generated (preparing for next stage)
jumpscareStep = 1;
Transitions to case 1 (Screen Falling Off) on the next call to handleJumpscareAnimation()
let fallingProgress = (millis() - jumpscareStartTime) / (JUMPSCARE_DURATION / 2);
Calculates progress from 0 to 1 over half the jumpscare duration (0.5 seconds for a 1-second total)
screenFallingOffRect.y = lerp(-screenFallingOffRect.h, height, fallingProgress);
Animates the screen piece falling from off-screen (negative y) down to the bottom using lerp()
fill(20, 20, 40);
Sets the fill color to match the background so the falling rectangle blends
rect(0, 0, screenFallingOffRect.w, screenFallingOffRect.h);
Draws a rectangle covering part of the screen, simulating the screen falling away
push(); translate(width / 2, height / 2); scale(1.2); drawGrotesqueFace(); pop();
Draws the grotesque face centered on screen and 20% larger (scale 1.2)
if (millis() - jumpscareStartTime > JUMPSCARE_DURATION / 2) {
In case 2 (Face Reveal & Hold), checks if half the jumpscare duration has elapsed
jumpscareActive = false;
Exits the jumpscare sequence and returns to normal physics
loop();
Restarts the draw loop so physics simulation resumes

generateAutomatedCrack()

generateAutomatedCrack() is called 50 times during jumpscare stage 0 to create massive cracks across the entire screen. Unlike generateNewCracks() which creates small cracks near the click point, this generates huge lines anywhere on screen, contributing to the screen-breaking visual spectacle.

function generateAutomatedCrack() {
  let x1 = random(width);
  let y1 = random(height);
  let angle = random(TWO_PI);
  let length = random(width / 3, width / 2); // Large cracks
  let x2 = x1 + cos(angle) * length;
  let y2 = y1 + sin(angle) * length;
  crackSegments.push(x1, y1, x2, y2);
}
Line-by-line explanation (7 lines)
let x1 = random(width);
Picks a random x coordinate anywhere across the screen width
let y1 = random(height);
Picks a random y coordinate anywhere across the screen height
let angle = random(TWO_PI);
Picks a random angle from 0 to 2π radians (0 to 360 degrees)
let length = random(width / 3, width / 2);
Picks a very long crack length—between 1/3 and 1/2 of the screen width (much larger than player-generated cracks)
let x2 = x1 + cos(angle) * length;
Calculates the end x coordinate using cosine and the angle
let y2 = y1 + sin(angle) * length;
Calculates the end y coordinate using sine (trigonometry)
crackSegments.push(x1, y1, x2, y2);
Appends the four values (start and end point) to the crackSegments array

drawGrotesqueFace()

drawGrotesqueFace() creates the horror element of the jumpscare. It uses random distortions (shearing, scaling, translation) on every frame to create a flickering, digital-artifact feel. The face itself is grotesque: black oval with huge red eyes and a jagged white mouth. Random glitch lines overlay the face to enhance the unsettling digital aesthetic.

function drawGrotesqueFace() {
  push();
  // Apply random distortions for glitch effect
  shearX(random(-0.1, 0.1));
  shearY(random(-0.1, 0.1));
  translate(random(-10, 10), random(-10, 10));
  scale(random(0.95, 1.05));

  // Face shape
  fill(0); // Darker face
  ellipse(0, 0, 200, 250);

  // Eyes
  fill(255, 0, 0); // Red eyes
  ellipse(-50, -40, 50, 70);
  ellipse(50, -40, 50, 70);

  // Pupils (more distorted)
  fill(255);
  ellipse(-50 + random(-10, 10), -40 + random(-10, 10), 20, 30);
  ellipse(50 + random(-10, 10), -40 + random(-10, 10), 20, 30);

  // Mouth (distorted, jagged teeth)
  fill(255);
  beginShape();
  vertex(-70, 70);
  vertex(-30, 40);
  vertex(0, 80);
  vertex(30, 40);
  vertex(70, 70);
  vertex(30, 100);
  vertex(0, 60);
  vertex(-30, 100);
  endShape(CLOSE);

  // Glitch lines
  stroke(255, random(50, 150));
  strokeWeight(random(1, 3));
  line(-width / 4, random(-height / 4, height / 4), width / 4, random(-height / 4, height / 4));
  line(random(-width / 4, width / 4), -height / 4, random(-width / 4, width / 4), height / 4);

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

🔧 Subcomponents:

calculation Random Distortion Transforms shearX(random(-0.1, 0.1)); shearY(random(-0.1, 0.1)); translate(random(-10, 10), random(-10, 10)); scale(random(0.95, 1.05));

Applies random skewing, shifting, and scaling each frame to create a glitch/digital artifact effect

calculation Face Geometry (Ellipses and Polygon) fill(0); ellipse(0, 0, 200, 250); // Eyes fill(255, 0, 0); ellipse(-50, -40, 50, 70); ellipse(50, -40, 50, 70); // Pupils fill(255); ellipse(-50 + random(-10, 10), -40 + random(-10, 10), 20, 30); ellipse(50 + random(-10, 10), -40 + random(-10, 10), 20, 30); // Mouth fill(255); beginShape(); vertex(-70, 70); vertex(-30, 40); vertex(0, 80); vertex(30, 40); vertex(70, 70); vertex(30, 100); vertex(0, 60); vertex(-30, 100); endShape(CLOSE);

Draws the distorted face using ellipses for the face and eyes, and a custom polygon for the jagged mouth

calculation Glitch Line Artifacts stroke(255, random(50, 150)); strokeWeight(random(1, 3)); line(-width / 4, random(-height / 4, height / 4), width / 4, random(-height / 4, height / 4)); line(random(-width / 4, width / 4), -height / 4, random(-width / 4, width / 4), height / 4);

Draws random white scan lines with varying opacity and thickness to enhance the digital glitch aesthetic

push();
Saves the current transformation state
shearX(random(-0.1, 0.1)); shearY(random(-0.1, 0.1));
Applies random horizontal and vertical skewing—this changes every frame for a flickering effect
translate(random(-10, 10), random(-10, 10));
Shifts the entire face randomly by ±10 pixels each frame
scale(random(0.95, 1.05));
Scales the face randomly between 95% and 105% size each frame
fill(0);
Sets fill to black (0 in grayscale)
ellipse(0, 0, 200, 250);
Draws a black elliptical face shape at origin, 200 pixels wide and 250 pixels tall
fill(255, 0, 0);
Sets fill to pure red (RGB: 255, 0, 0) for the eyes
ellipse(-50, -40, 50, 70); ellipse(50, -40, 50, 70);
Draws two red ellipses (50×70 pixels) positioned as eyes at (-50, -40) and (50, -40)
fill(255); ellipse(-50 + random(-10, 10), -40 + random(-10, 10), 20, 30); ellipse(50 + random(-10, 10), -40 + random(-10, 10), 20, 30);
Draws white pupils inside the eyes, with random jitter (±10 pixels) each frame for an unsettling twitching effect
beginShape(); vertex(-70, 70); vertex(-30, 40); ... endShape(CLOSE);
Draws a white polygon with 8 vertices forming a grotesque, jagged mouth shape
stroke(255, random(50, 150));
Sets stroke color to white with random alpha (opacity) between 50 and 150 (0–255 scale)
strokeWeight(random(1, 3));
Sets stroke thickness to a random value between 1 and 3 pixels
line(-width / 4, random(-height / 4, height / 4), width / 4, random(-height / 4, height / 4));
Draws a horizontal glitch line across the middle of the screen with random vertical jitter
line(random(-width / 4, width / 4), -height / 4, random(-width / 4, width / 4), height / 4);
Draws a vertical glitch line with random horizontal jitter
pop();
Restores the previous transformation state

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. It resizes the canvas and redraws the instruction text so the UI stays centered. If the user hasn't interacted yet (no blocks have been clicked), it stops the loop to show instructions again.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  background(20, 20, 40); // Clear background on resize
  // Redraw the instruction text on resize
  textAlign(CENTER, CENTER);
  textSize(32); // Made text larger for better visibility
  fill(255); // Made text fully opaque white for better visibility
  noStroke();
  text('Click or drag your mouse to smash objects!', width / 2, height / 2);
  // Keep the draw loop stopped after resize if no interaction has occurred yet
  if (shatterables.length === 0) { // Only stop if no smashable bodies exist
    noLoop();
  }
}
Line-by-line explanation (5 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions
background(20, 20, 40);
Clears the canvas to the dark background color
text('Click or drag your mouse to smash objects!', width / 2, height / 2);
Redraws the instruction text centered on the newly resized canvas
if (shatterables.length === 0) {
Checks if there are any blocks remaining in the game
noLoop();
If no blocks exist, stops the draw loop (returns to showing instructions)

📦 Key Variables

engine object

The Matter.js physics engine instance that calculates all physics updates, forces, and collisions each frame

engine = Engine.create();
world object

The Matter.js physics world where all bodies exist and gravity is applied; contains all physics objects

world = engine.world;
bodies array

Array storing all physics bodies (ground, walls, blocks, and pieces) for cleanup and management

let bodies = [];
shatterables array

Array storing only the blocks and pieces that can be clicked and shattered by the player

let shatterables = [];
crackStrength number

Accumulator tracking how many times the player has clicked; increments by 1 per click and triggers jumpscare at 20

let crackStrength = 0;
crackSegments array

Array storing all crack line segments as [x1, y1, x2, y2] values; drawn every frame to show screen damage

let crackSegments = [];
jumpscareActive boolean

Flag indicating whether the jumpscare sequence is currently playing; when true, draw() calls handleJumpscareAnimation()

let jumpscareActive = false;
jumpscareStep number

State variable (0, 1, or 2) controlling which stage of the jumpscare animation is playing: 0=cracking, 1=screen falling, 2=face reveal

let jumpscareStep = 0;
automatedCrackCounter number

Counter tracking how many of the 50 automated cracks have been generated during jumpscare step 0

let automatedCrackCounter = 0;
jumpscareStartTime number

Timestamp (from millis()) recorded when jumpscare starts or advances to a new stage; used to time animations

jumpscareStartTime = millis();

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG handleJumpscareAnimation() case 2

After jumpscare completes, the draw loop restarts but the physics bodies remain at their positions from when jumpscare triggered, creating a jarring visual discontinuity

💡 Consider resetting the physics engine or clearing/recreating the initial block stack after jumpscare ends to provide a clean visual restart

PERFORMANCE mousePressed() nearest body search

The loop that finds the closest block runs in O(n) time on every click—with hundreds of pieces, this could become slow

💡 Implement spatial partitioning (quadtree) or a spatial hash to quickly narrow down nearby bodies before distance checking

STYLE drawGrotesqueFace() and handleJumpscareAnimation()

Random glitch values are generated every frame, making distortions and opacity flicker unpredictably—some frames may not render distortion at all due to small random values

💡 Use a Perlin noise-based or eased animation approach for distortions so they vary smoothly and predictably over time rather than completely randomly

FEATURE shatterBody()

Shattered pieces can only be hit if they're in the shatterables array, but falling pieces off-screen are removed—allowing clean garbage collection but limiting gameplay depth

💡 Add a brief window (e.g., 2 seconds) where off-screen pieces can still be smashed if the player clicks near their last known position, extending the challenge

PERFORMANCE draw() offscreen cleanup loop

Looping backward through bodies to clean up off-screen objects is correct but does not check if the body is in both arrays before removing—could cause inconsistent state if a body isn't in one array

💡 Add an explicit check or factory function to ensure bodies are added/removed from both arrays atomically to prevent state mismatches

🔄 Code Flow

Code flow showing setup, draw, mousepressed, shatterbody, generatenewcracks, drawcracks, addshaterbody, drawbody, handlejumpscareanimation, generatautomatedcrack, drawgrotesqueface, windowresized

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

graph TD start[Start] --> setup[setup] setup --> engine-init[Physics Engine Initialization] setup --> static-bodies[Static Boundary Setup] setup --> initial-blocks[Initial Block Stack] setup --> draw[draw loop] click setup href "#fn-setup" click engine-init href "#sub-engine-init" click static-bodies href "#sub-static-bodies" click initial-blocks href "#sub-initial-blocks" draw --> jumpscare-branch[Jumpscare Check] jumpscare-branch -->|Normal| physics-update[Physics Engine Update] jumpscare-branch -->|Jumpscare| handlejumpscareanimation[handleJumpscareAnimation] click jumpscare-branch href "#sub-jumpscare-branch" click physics-update href "#sub-physics-update" click handlejumpscareanimation href "#fn-handlejumpscareanimation" physics-update --> draw-bodies[Draw All Shatterable Bodies] physics-update --> offscreen-cleanup[Remove Off-Screen Bodies] draw-bodies --> draw offscreen-cleanup --> draw click draw-bodies href "#sub-draw-bodies" click offscreen-cleanup href "#sub-offscreen-cleanup" draw --> mousepressed[mousePressed] click mousepressed href "#fn-mousepressed" mousepressed --> nearest-body-search[Nearest Body Search] nearest-body-search --> hit-detection[Impact and Hit Detection] hit-detection -->|Hit| shatterbody[shatterBody] hit-detection -->|Miss| addshaterbody[addShatterableBody] click nearest-body-search href "#sub-nearest-body-search" click hit-detection href "#sub-hit-detection" click shatterbody href "#fn-shatterbody" click addshaterbody href "#fn-addshaterbody" shatterbody --> remove-original[Remove Original Body] remove-original --> piece-creation-loop[Piece Creation Loop] piece-creation-loop --> piece-shape-choice[Random Shape Selection] piece-shape-choice --> force-application[Outward Force Vector Calculation] click remove-original href "#sub-remove-original" click piece-creation-loop href "#sub-piece-creation-loop" click piece-shape-choice href "#sub-piece-shape-choice" click force-application href "#sub-force-application" draw --> crack-accumulation[Crack Accumulation and Jumpscare Trigger] crack-accumulation -->|Threshold Reached| jumpscare-reset[Jumpscare Reset Branch] crack-accumulation -->|Not Reached| generatenewcracks[generateNewCracks] click crack-accumulation href "#sub-crack-accumulation" click jumpscare-reset href "#sub-jumpscare-reset" click generatenewcracks href "#fn-generatenewcracks" generatenewcracks --> crack-segment-loop[Crack Segment Generation Loop] crack-segment-loop --> radial-calculation[Radial Line Calculation] click crack-segment-loop href "#sub-crack-segment-loop" click radial-calculation href "#sub-radial-calculation" draw --> drawcracks[drawCracks] drawcracks --> early-return[Early Exit If No Cracks] early-return --> line-drawing-loop[Crack Line Drawing Loop] line-drawing-loop --> drawcracks click drawcracks href "#fn-drawcracks" click early-return href "#sub-early-return" click line-drawing-loop href "#sub-line-drawing-loop" handlejumpscareanimation --> animation-state-machine[Three-Stage Animation State Machine] animation-state-machine -->|Stage 0| generatautomatedcrack[generateAutomatedCrack] animation-state-machine -->|Stage 1| red-flash[Red Flash Background] animation-state-machine -->|Stage 2| drawgrotesqueface[drawGrotesqueFace] click handlejumpscareanimation href "#fn-handlejumpscareanimation" click generatautomatedcrack href "#fn-generatautomatedcrack" click red-flash href "#sub-red-flash" click drawgrotesqueface href "#fn-drawgrotesqueface" drawgrotesqueface --> distortion-transforms[Random Distortion Transforms] distortion-transforms --> face-geometry[Face Geometry] face-geometry --> glitch-lines[Glitch Line Artifacts] click distortion-transforms href "#sub-distortion-transforms" click face-geometry href "#sub-face-geometry" click glitch-lines href "#sub-glitch-lines" draw --> windowresized[windowResized] click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effects can I expect from the 'have fun' p5.js sketch?

The sketch visually simulates a pile of blocks falling and shattering into pieces upon mouse clicks, creating dynamic cracks across the screen that culminate in a dramatic jumpscare effect.

How can I interact with the 'have fun' creative coding sketch?

Users can interact by clicking on the falling blocks, which increases the crack strength and triggers visual explosions and cracks on the screen.

What creative coding techniques does the 'have fun' sketch showcase?

This sketch demonstrates interactive physics simulation using Matter.js, along with visual effects for shattering and crack animations based on user interaction.

Preview

have fun - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of have fun - Code flow showing setup, draw, mousepressed, shatterbody, generatenewcracks, drawcracks, addshaterbody, drawbody, handlejumpscareanimation, generatautomatedcrack, drawgrotesqueface, windowresized
Code Flow Diagram