Midterm Project –

Concept

Retro-style 2D shooters have always been a favorite among gamers. Their simplicity, fast-paced action, and pixel-art aesthetics create an engaging experience. I set out to create my own Pixel Shooter Game using p5.js, adding dynamic enemy interactions, ammo management, health pickups, and immersive sound effects.

The idea was simple:

  • The player moves around the screen and shoots at approaching enemies.
  • The enemies chase the player and deal damage upon collision.
  • The player must manage ammo and collect pickups to survive.
  • The game ends when the player’s health reaches zero.

With these mechanics in mind, I designed a game that combines action, strategy, and survival elements.

I used a lot of sound effects from popular shooter games (Half Life, Counter Strike, some soundtracks from Red Faction II) and sandbox games like Minecraft.

I also used free, open-source sprites from different forums, God bless open-source!

How It Works

The game follows a state-based system, transitioning between:

  1. Loading Screen – Displays a progress bar while assets are loading.
  2. Start Menu – Shows the title, “Start Game” and “Instructions” buttons.
  3. Instructions Page – Displays movement, shooting, and gameplay tips.
  4. Play Mode – The core gameplay where enemies chase the player.
  5. Game Over Screen – Displays the final score and an option to restart.

Core Mechanics

  • Player Movement: Uses WASD or arrow keys to move.
  • Shooting: Press Space to fire bullets.
  • Enemy AI: Enemies spawn and move toward the player.
  • Health System: The player starts with 3 HP and dies after three enemy hits.
  • Ammo System: The player has 20 bullets max and must pick up ammo to reload.
  • Pickups: Random enemies drop ammo or health packs.

Code Highlights:

1) State-Based Game Flow

To keep the game organized, I used a state-based approach to control transitions:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function draw() {
background(20);
if (gameState === "loading") {
showLoadingScreen();
if (assetsLoaded >= totalAssets && loadingCompleteTime === 0) {
loadingCompleteTime = millis();
}
if (loadingCompleteTime > 0 && millis() - loadingCompleteTime > loadingDelay) {
gameState = "start";
}
}
else if (gameState === "start") {
showStartScreen();
startButton.show();
instructionsButton.show();
backButton.hide(); // Ensure Back button is hidden
if (!startMusic.isPlaying()) {
startMusic.loop();
startMusic.setVolume(0.5);
}
}
else if (gameState === "instructions") {
showInstructionsScreen();
startButton.hide();
instructionsButton.hide();
backButton.show();
}
else if (gameState === "play") {
runGame();
showScore();
startButton.hide();
instructionsButton.hide();
backButton.hide();
if (startMusic.isPlaying()) {
startMusic.stop();
}
if (!gameMusic.isPlaying()) {
gameMusic.loop();
gameMusic.setVolume(0.5);
}
}
else if (gameState === "dead") {
showDeadScreen();
startButton.hide();
instructionsButton.hide();
backButton.hide();
}
}
function draw() { background(20); if (gameState === "loading") { showLoadingScreen(); if (assetsLoaded >= totalAssets && loadingCompleteTime === 0) { loadingCompleteTime = millis(); } if (loadingCompleteTime > 0 && millis() - loadingCompleteTime > loadingDelay) { gameState = "start"; } } else if (gameState === "start") { showStartScreen(); startButton.show(); instructionsButton.show(); backButton.hide(); // Ensure Back button is hidden if (!startMusic.isPlaying()) { startMusic.loop(); startMusic.setVolume(0.5); } } else if (gameState === "instructions") { showInstructionsScreen(); startButton.hide(); instructionsButton.hide(); backButton.show(); } else if (gameState === "play") { runGame(); showScore(); startButton.hide(); instructionsButton.hide(); backButton.hide(); if (startMusic.isPlaying()) { startMusic.stop(); } if (!gameMusic.isPlaying()) { gameMusic.loop(); gameMusic.setVolume(0.5); } } else if (gameState === "dead") { showDeadScreen(); startButton.hide(); instructionsButton.hide(); backButton.hide(); } }
function draw() {
  background(20);

  if (gameState === "loading") {
    showLoadingScreen();
    if (assetsLoaded >= totalAssets && loadingCompleteTime === 0) {
      loadingCompleteTime = millis();
    }
    if (loadingCompleteTime > 0 && millis() - loadingCompleteTime > loadingDelay) {
      gameState = "start";
    }
  } 
  else if (gameState === "start") {
    showStartScreen();
    startButton.show();
    instructionsButton.show();
    backButton.hide(); // Ensure Back button is hidden
    
    if (!startMusic.isPlaying()) {
      startMusic.loop();
      startMusic.setVolume(0.5);
    }
  } 
  else if (gameState === "instructions") {
    showInstructionsScreen();
    startButton.hide();
    instructionsButton.hide();
    backButton.show();
  } 
  else if (gameState === "play") {
    runGame();
    showScore();
    startButton.hide();
    instructionsButton.hide();
    backButton.hide();
    
    if (startMusic.isPlaying()) {
      startMusic.stop();
    }
    
    if (!gameMusic.isPlaying()) {
      gameMusic.loop();
      gameMusic.setVolume(0.5);
    }
  } 
  else if (gameState === "dead") {
    showDeadScreen();
    startButton.hide();
    instructionsButton.hide();
    backButton.hide();
  }
}

Each function (showLoadingScreen(), runGame(), etc.) controls what is displayed based on the current state.


2) Player Shooting Mechanic

The player can shoot bullets, but with an ammo limit:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Player {
constructor() {
this.pos = createVector(width / 2, height / 2);
this.speed = 4;
this.movementDist = 0; // we update this in update()
this.ammo = 20;
this.health = 3;
this.lastDamageTime = 0; // Track last time the player took damage
}
update() {
...
}
show() {
push();
translate(this.pos.x, this.pos.y);
// Face the mouse (common in top-down shooters).
let angle = atan2(mouseY - this.pos.y, mouseX - this.pos.x);
rotate(angle);
scale(0.25);
// Decide idle or move frames
let moving = (this.movementDist > 0.1);
// Pick frame index
let frameIndex;
imageMode(CENTER);
if (moving) {
frameIndex = floor(frameCount / 6) % playerMoveSprites.length;
image(playerMoveSprites[frameIndex], 0, 0);
} else {
frameIndex = floor(frameCount / 12) % playerIdleSprites.length;
image(playerIdleSprites[frameIndex], 0, 0);
}
pop();
this.showHealthBar();
}
showHealthBar() {
...
}
shoot() {
// Fire only if enough time has passed since last shot AND we have ammo left
if (this.ammo > 0 && millis() - lastShotTime > 200) {
bullets.push(new Bullet(this.pos.x, this.pos.y));
lastShotTime = millis();
this.ammo--; // reduce ammo by 1
}
if (gunSound)
gunSound.play();
}
}
class Player { constructor() { this.pos = createVector(width / 2, height / 2); this.speed = 4; this.movementDist = 0; // we update this in update() this.ammo = 20; this.health = 3; this.lastDamageTime = 0; // Track last time the player took damage } update() { ... } show() { push(); translate(this.pos.x, this.pos.y); // Face the mouse (common in top-down shooters). let angle = atan2(mouseY - this.pos.y, mouseX - this.pos.x); rotate(angle); scale(0.25); // Decide idle or move frames let moving = (this.movementDist > 0.1); // Pick frame index let frameIndex; imageMode(CENTER); if (moving) { frameIndex = floor(frameCount / 6) % playerMoveSprites.length; image(playerMoveSprites[frameIndex], 0, 0); } else { frameIndex = floor(frameCount / 12) % playerIdleSprites.length; image(playerIdleSprites[frameIndex], 0, 0); } pop(); this.showHealthBar(); } showHealthBar() { ... } shoot() { // Fire only if enough time has passed since last shot AND we have ammo left if (this.ammo > 0 && millis() - lastShotTime > 200) { bullets.push(new Bullet(this.pos.x, this.pos.y)); lastShotTime = millis(); this.ammo--; // reduce ammo by 1 } if (gunSound) gunSound.play(); } }
class Player {
  constructor() {
    this.pos = createVector(width / 2, height / 2);
    this.speed = 4;
    this.movementDist = 0; // we update this in update()
    this.ammo = 20;  
    this.health = 3;
    this.lastDamageTime = 0;  // Track last time the player took damage


  }

  update() {
   ...
  }

  show() {
    push();
    translate(this.pos.x, this.pos.y);

    // Face the mouse (common in top-down shooters).
    let angle = atan2(mouseY - this.pos.y, mouseX - this.pos.x);
    rotate(angle);
    scale(0.25);


    // Decide idle or move frames
    let moving = (this.movementDist > 0.1);

    // Pick frame index
    let frameIndex;
      imageMode(CENTER);

    if (moving) {
      frameIndex = floor(frameCount / 6) % playerMoveSprites.length;
      image(playerMoveSprites[frameIndex], 0, 0);
    } else {
      frameIndex = floor(frameCount / 12) % playerIdleSprites.length; 
      image(playerIdleSprites[frameIndex], 0, 0);
    }

    pop();
    
    this.showHealthBar();

  }
  
    showHealthBar() {
    ...
  }

  shoot() {
    // Fire only if enough time has passed since last shot AND we have ammo left
    if (this.ammo > 0 && millis() - lastShotTime > 200) {
      bullets.push(new Bullet(this.pos.x, this.pos.y));
      lastShotTime = millis();
      this.ammo--;  // reduce ammo by 1
    }
    
      if (gunSound) 
        gunSound.play();
  }
}

This ensures players can’t spam bullets, adding a strategic element to gameplay.

3) Enemy AI and Collision Handling

Enemies move toward the player, and if they collide, the player takes damage:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Enemy-player collisions (Fixed)
for (let enemy of enemies) {
let dPlayer = dist(player.pos.x, player.pos.y, enemy.pos.x, enemy.pos.y);
// Only take damage if enough time has passed (e.g., 1 second)
if (dPlayer < 30 && millis() - player.lastDamageTime > 1000) {
player.health--;
player.lastDamageTime = millis(); // Update last hit time
if (hitSound) {
hitSound.play();
}
if (player.health <= 0) {
if(playerDeathSound) {
playerDeathSound.play();
}
gameState = 'dead';
}
}
}
// Enemy-player collisions (Fixed) for (let enemy of enemies) { let dPlayer = dist(player.pos.x, player.pos.y, enemy.pos.x, enemy.pos.y); // Only take damage if enough time has passed (e.g., 1 second) if (dPlayer < 30 && millis() - player.lastDamageTime > 1000) { player.health--; player.lastDamageTime = millis(); // Update last hit time if (hitSound) { hitSound.play(); } if (player.health <= 0) { if(playerDeathSound) { playerDeathSound.play(); } gameState = 'dead'; } } }
// Enemy-player collisions (Fixed)
for (let enemy of enemies) {
  let dPlayer = dist(player.pos.x, player.pos.y, enemy.pos.x, enemy.pos.y);
  
  // Only take damage if enough time has passed (e.g., 1 second)
  if (dPlayer < 30 && millis() - player.lastDamageTime > 1000) {
    player.health--;  
    player.lastDamageTime = millis(); // Update last hit time
    
    if (hitSound) {
      hitSound.play();
    }
    
    if (player.health <= 0) {
      if(playerDeathSound) {
        playerDeathSound.play();
      }
      gameState = 'dead';
    }
  }
}

This prevents instant death by implementing a damage cooldown.

4) Dynamic Background Music

Different background tracks play in menu and play modes:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
else if (gameState === "play") {
runGame();
showScore();
startButton.hide();
instructionsButton.hide();
backButton.hide();
if (startMusic.isPlaying()) {
startMusic.stop();
}
if (!gameMusic.isPlaying()) {
gameMusic.loop();
gameMusic.setVolume(0.5);
}
}
else if (gameState === "play") { runGame(); showScore(); startButton.hide(); instructionsButton.hide(); backButton.hide(); if (startMusic.isPlaying()) { startMusic.stop(); } if (!gameMusic.isPlaying()) { gameMusic.loop(); gameMusic.setVolume(0.5); } }
else if (gameState === "play") {
  runGame();
  showScore();
  startButton.hide();
  instructionsButton.hide();
  backButton.hide();
  
  if (startMusic.isPlaying()) {
    startMusic.stop();
  }
  
  if (!gameMusic.isPlaying()) {
    gameMusic.loop();
    gameMusic.setVolume(0.5);
  }
}

This immerses the player by dynamically switching music.

Challenges and Improvements

1) Handling Enemy Collisions Fairly

Initially, enemies instantly killed the player if they were touching them. To fix this, I added a damage cooldown, so players have 1 second of immunity between hits.

2) Centering Buttons Properly

Buttons were misaligned when resizing the canvas. Instead of manually placing them, I used dynamic centering.

3) Preventing Accidental Game Starts

Initially, pressing any key started the game. To fix this, I made keyPressed() work only in play mode.

Final Thoughts

This Pixel Shooter Game was a fun challenge that combined:

  • Game physics (enemy movement, shooting mechanics)
  • User experience improvements (better UI, centered buttons)
  • Audio immersion (different music for each state)
  • Optimization tricks (cropping backgrounds, limiting bullet spam)

Possible Future Improvements

  • Add power-ups (e.g., speed boost, rapid fire)
  • Implement different enemy types
  • Introduce a high score system
  • Introduce multiplayer using socket.io (websocket server connection, so two different clients could play on separate machines)

This project demonstrates how p5.js can create interactive, engaging 2D games while keeping code structured and scalable.

Sketch (Click Here to Open Full Screen)

 

Week 5 – Midterm Progress

Inspiration

The idea is creating a top-down shooter game. I was inspired by  Nuclear Throne, where players control a character in a confined space and must shoot incoming enemies. The thrill of dodging enemies while strategically shooting at them creates an engaging and fast-paced gameplay experience. The goal was to recreate this immersive feeling while keeping the implementation simple and beginner-friendly using p5.js.

Concept and User Interaction

The game concept revolves around a player-controlled character that moves around the screen and shoots at enemy units that spawn randomly and chase the player. The user can interact with the game in the following ways:

  • Movement: The player uses the arrow keys or WASD to move in different directions.
  • Shooting: The player shoots bullets towards the mouse cursor by pressing the spacebar.
  • Enemies: Randomly spawned enemies move towards the player and can be destroyed by bullets.
  • Survival Challenge: The player must continuously avoid enemies while shooting them down.

This simple yet engaging mechanic ensures a dynamic game experience where quick reflexes and strategic positioning are key to survival.

Designing the Code Structure

Before diving into the code, I designed a modular approach to keep the project manageable and scalable. The core elements of the game were broken down into:

  1. Player Class: Handles movement, shooting, and rendering.
  2. Bullet Class: Manages bullet behavior, movement, and collision detection.
  3. Enemy Class: Controls enemy spawning, movement, and interaction with bullets.
  4. Game Loop: Updates and renders all game elements in each frame.
  5. Collision Handling: Detects when bullets hit enemies and removes them from the game.
  6. Enemy Spawning System: Ensures a steady challenge for the player.

By structuring the game this way, each component is easy to manage and modify.

Example – Player Class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Player {
constructor() {
this.pos = createVector(width / 2, height / 2);
this.speed = 4;
}
update() {
if (keyIsDown(UP_ARROW) || keyIsDown(87)) this.pos.y -= this.speed;
if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) this.pos.y += this.speed;
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) this.pos.x -= this.speed;
if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) this.pos.x += this.speed;
this.pos.x = constrain(this.pos.x, 0, width);
this.pos.y = constrain(this.pos.y, 0, height);
}
show() {
fill(0, 255, 0);
ellipse(this.pos.x, this.pos.y, 30, 30);
}
shoot() {
if (millis() - lastShotTime > 200) {
bullets.push(new Bullet(this.pos.x, this.pos.y));
lastShotTime = millis();
}
}
}
class Player { constructor() { this.pos = createVector(width / 2, height / 2); this.speed = 4; } update() { if (keyIsDown(UP_ARROW) || keyIsDown(87)) this.pos.y -= this.speed; if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) this.pos.y += this.speed; if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) this.pos.x -= this.speed; if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) this.pos.x += this.speed; this.pos.x = constrain(this.pos.x, 0, width); this.pos.y = constrain(this.pos.y, 0, height); } show() { fill(0, 255, 0); ellipse(this.pos.x, this.pos.y, 30, 30); } shoot() { if (millis() - lastShotTime > 200) { bullets.push(new Bullet(this.pos.x, this.pos.y)); lastShotTime = millis(); } } }
class Player {
  constructor() {
    this.pos = createVector(width / 2, height / 2);
    this.speed = 4;
  }

  update() {
    if (keyIsDown(UP_ARROW) || keyIsDown(87)) this.pos.y -= this.speed;
    if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) this.pos.y += this.speed;
    if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) this.pos.x -= this.speed;
    if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) this.pos.x += this.speed;
    
    this.pos.x = constrain(this.pos.x, 0, width);
    this.pos.y = constrain(this.pos.y, 0, height);
  }

  show() {
    fill(0, 255, 0);
    ellipse(this.pos.x, this.pos.y, 30, 30);
  }

  shoot() {
    if (millis() - lastShotTime > 200) {
      bullets.push(new Bullet(this.pos.x, this.pos.y));
      lastShotTime = millis();
    }
  }
}

Identifying and Addressing Key Challenges

One of the most challenging parts of the project is collision detection between bullets and enemies. Ensuring that fast-moving bullets accurately register hits on enemies can be tricky, especially in a game with rapid movement and frequent object interactions. Also, I wanted to add a multiplayer gameplay experience, so 2 players could play in the same session. However, I do not think it is possible without the use of socket.io.

Next Steps

Moving forward, possible improvements could include:

  • Adding different enemy types with unique behaviors.
  • Implementing a score system to track progress.
  • Introducing power-ups to enhance gameplay variety.
  • Multiplayer Mode: Implementing real-time multiplayer gameplay using Socket.IO so that two players can play together from different machines. This would involve syncing player movement, bullets, and enemies across connected clients through a Node.js server.

By integrating multiplayer functionality, the game could become even more engaging and interactive. Using real-time communication, players could strategize together, compete for the highest score, or even introduce cooperative play against waves of enemies. Setting up server-side logic to handle multiple players efficiently is a challenge but would greatly enhance the gaming experience.

Reading Response 4 – Computer Vision for Artists and Designers (Week 5)

In his article, Levin delves into the relationship between code and creative expression, illustrating how coding and computation offer a unique medium for artists to explore new forms of interactivity and non-verbal communication. This perspective was particularly eye-opening for me, as it shed light on how computation is not just a tool for efficiency or automation but also a canvas for artistic exploration.

One of the most fascinating aspects discussed in the article was computer vision. While the term itself is somewhat new to me, I was surprised to learn that efforts to advance this field began over half a century ago. It is remarkable to realize that machines can now collect visual data and “interpret” it, mimicking human perception in ways that were once the realm of science fiction. Computer vision models allow computers to identify human features, recognize expressions, and even infer emotions—all of which have groundbreaking implications, not only for fields like surveillance and security but also for art. In interactive media, for instance, artists are using computer vision to create installations that respond dynamically to human presence, movement, or even facial expressions, transforming passive spectators into active participants in digital art.

However, despite its exciting artistic applications, computer vision carries an eerie undertone due to its origins. The fact that this field was initially a military endeavor makes its transition into the realm of creative expression feel somewhat uncanny. The same technology that was once developed for warfare—such as guiding missiles or identifying enemy targets—is now being used to make art installations more immersive. This contrast raises an unsettling question: can a technology born from conflict and control ever be fully dissociated from its original intent?

Beyond its history, the rapid advancement of computer vision presents an undeniable threat to human privacy. Today, no one is truly safe from being recognized, analyzed, and cataloged by ubiquitous surveillance cameras, facial recognition systems, and AI-powered security networks. What was once considered futuristic is now an everyday reality—public spaces are filled with CCTV cameras that can track individuals in real time, while social media platforms use facial recognition to tag people in photos automatically. While some of these applications serve practical or even artistic purposes, they also blur the boundaries between technological progress and ethical concerns. When does interactivity cross into intrusion? At what point does an artistic exploration of human expression become indistinguishable from surveillance?

Reading Response 3 – Design of Everyday Things or Why I Hate Smart Watches (Week 4)

In “Design of Everyday Things”, Don Norman brings his perspective on the challenges of design of the daily tech. stuff. While I was reading the chapter, the example of a watch with four buttons really resonated with me. 

Don discusses how the excessive design features of the mentioned watch makes the overall user experience less enjoyable as a watch that has 4 different buttons brings extra functionality to something that was supposed to show time and time only. Even though the idea of a device with multiple buttons and functionality is quite progressive and optimistic, such a redundant feature bomb confuses the users of such a watch. It distracts the user from the initial purpose of a watch: time display.

I resonate with Don’s frustration with this product as I have experienced this firsthand when I got my hands on my first Apple watch. At the beginning, it was really fun to use: switching music tracks on the go, replying to messages and even using Siri to essentially Google anything was amazing, as if I had a mini companion on my hand. However, as time went on, I started to realize that I do not really use my Apple watch for checking time. Now it was another distraction that kept my feeble brain from work and kept me from focusing on the task in front of me. The constant notifications, messages and other features became hateful to me. At times, I didn’t even charge my own watch to work in silence.

Ever since I got my Apple Watch, I feel even more attached to my phone. The small screen, limited performance, and awkward interaction with tiny buttons make it frustrating to use. Plus, it’s packed with features I’ll likely never need, echoing the usability challenges Norman described. Having to charge it daily only adds another layer of inconvenience.

So, I believe watches shouldn’t try to be multifunctional gadgets but rather remain focused on a single purpose with a sleek, intuitive design. That’s what truly defines a watch. When overloaded with features, they lose their essence and become just another mini-smartphone—one that’s harder to use and more of a hassle to maintain. A watch should enhance convenience, not add to the digital clutter we already navigate daily.

Week 4 – Loading Data, Displaying text

Concept

I wanted to experiment with ASCII art in a dynamic way, using Perlin noise and interactive effects to create an evolving text-based visualization (I got inspired by the Coding Train videos on Perlin noise and this image manipulation + ASCII density coding challenge by him).

Each character is selected based on noise values, and their colors shift over time. As the mouse moves, it creates a ripple effect by “pushing” nearby characters away, giving a fluid and organic motion to the grid.

Demo (Click on canvas)

Code Highlight:

This part of the code uses Perlin noise to smoothly select characters from the density string and generate dynamic colors for each character.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Using Perlin noise for smoother character selection
let noiseVal = noise(i * 0.1, j * 0.1, frameCount * 0.02) * density.length;
let charIndex = floor(noiseVal);
let char = density.charAt(charIndex);
// Generate color based on Perlin noise
let r = noise(i * 0.05, j * 0.05, frameCount * 0.05) * 255;
let g = noise(i * 0.07, j * 0.07, frameCount * 0.05) * 255;
let b = noise(i * 0.09, j * 0.09, frameCount * 0.05) * 255;
fill(r, g, b);
text(char, newX, newY);
// Using Perlin noise for smoother character selection let noiseVal = noise(i * 0.1, j * 0.1, frameCount * 0.02) * density.length; let charIndex = floor(noiseVal); let char = density.charAt(charIndex); // Generate color based on Perlin noise let r = noise(i * 0.05, j * 0.05, frameCount * 0.05) * 255; let g = noise(i * 0.07, j * 0.07, frameCount * 0.05) * 255; let b = noise(i * 0.09, j * 0.09, frameCount * 0.05) * 255; fill(r, g, b); text(char, newX, newY);
// Using Perlin noise for smoother character selection
let noiseVal = noise(i * 0.1, j * 0.1, frameCount * 0.02) * density.length;
let charIndex = floor(noiseVal);
let char = density.charAt(charIndex);

// Generate color based on Perlin noise
let r = noise(i * 0.05, j * 0.05, frameCount * 0.05) * 255;
let g = noise(i * 0.07, j * 0.07, frameCount * 0.05) * 255;
let b = noise(i * 0.09, j * 0.09, frameCount * 0.05) * 255;

fill(r, g, b);
text(char, newX, newY);

How it works:

  • noiseVal determines the ASCII character by mapping noise to the density string length.
  • r, g, and b are also mapped to Perlin noise, creating a smooth, organic color transition over time.

Reflection & Improvements for Future Work

This experiment opened up some interesting possibilities for ASCII-based generative art. I’d love to refine the animation by adding more layers of motion, perhaps introducing gravity-like effects or different interaction styles. Another idea is to use real-time audio input to influence the movement, making the piece reactive to sound. Definitely a fun one to explore further!

Week 3 – Functions, Arrays, and Object-Oriented Programming

Concept

In this p5.js sketch, my main objective was to practice the use of classes and arrays. In my sketch, little objects called “walkers” wander randomly across the canvas, leaving behind the trail that slowly fades away. Each walker has its properties (see below in Code Highlight part), which are determined randomly at the moment of creation (user click).

Demo (Click on canvas)

Code Highlight:

The core of this sketch is the Walker class. It encapsulates everything each walker needs to function:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Walker class definition
class Walker {
constructor(x, y) {
this.x = x;
this.y = y;
// Give each Walker a random color
this.col = color(random(255), random(255), random(255));
// Control how quickly it moves around
this.stepSize = random(1, 3);
}
update() {
// Randomly move the walker in x & y directions
this.x += random(-this.stepSize, this.stepSize);
this.y += random(-this.stepSize, this.stepSize);
// Keep the walker within the canvas boundaries
this.x = constrain(this.x, 0, width);
this.y = constrain(this.y, 0, height);
}
show() {
noStroke();
fill(this.col);
ellipse(this.x, this.y, 8, 8);
}
}
// Walker class definition class Walker { constructor(x, y) { this.x = x; this.y = y; // Give each Walker a random color this.col = color(random(255), random(255), random(255)); // Control how quickly it moves around this.stepSize = random(1, 3); } update() { // Randomly move the walker in x & y directions this.x += random(-this.stepSize, this.stepSize); this.y += random(-this.stepSize, this.stepSize); // Keep the walker within the canvas boundaries this.x = constrain(this.x, 0, width); this.y = constrain(this.y, 0, height); } show() { noStroke(); fill(this.col); ellipse(this.x, this.y, 8, 8); } }
// Walker class definition
class Walker {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    // Give each Walker a random color
    this.col = color(random(255), random(255), random(255));
    // Control how quickly it moves around
    this.stepSize = random(1, 3);
  }

  update() {
    // Randomly move the walker in x & y directions
    this.x += random(-this.stepSize, this.stepSize);
    this.y += random(-this.stepSize, this.stepSize);

    // Keep the walker within the canvas boundaries
    this.x = constrain(this.x, 0, width);
    this.y = constrain(this.y, 0, height);
  }

  show() {
    noStroke();
    fill(this.col);
    ellipse(this.x, this.y, 8, 8);
  }
}

Inside this class, the update() method adjusts the walker’s position in small random increments, while show() draws a small ellipse at its current location.

The background(0, 13) call in the draw() loop is also important because it uses transparency to slowly erase the walkers’ paths, creating the glowing trails of each walker (I think it looks kinda cool).

Reflection & Improvements for Future Work

First of all, it was really entertaining to watch each walker wander around, leaving a trail. By simply changing parameters like number of walkers, their step size, or fade value in the background, one could produce unique versions of this piece.

In the future, I would love to make sure that the walkers of the same color would interact with each other (mixing together and creating a bigger walker). Another interesting idea would be to incorporate music in this piece (maybe 90s like Jungle beat) -> this way each walker would change its size/movement speed/direction based on the song.

If one would think long enough, the possibilities for making this piece more immersive and dynamic are essentially limitless.

Maybe one could make a game out of this piece, who knows?

Reading Response 2 – What Exactly Is Interactivity? (Week 3)

Chris Crawford’s idea of interactivity as a conversation between two participants came as a novelty to me. Before reading this, I assumed that any program that reacted to user input was interactive in its nature, but after reading this I was convinced that an interactive system must possess these three elements; listening, thinking and responding/speaking. 

I feel like Crawford’s example of the refrigerator light was brilliant, it made me rethink what it means for a system to be truly interactive. I began examining the apps and websites I use every day and noticed that many of them only react to user input without actually processing information in a meaningful way. They operate on predefined rules, responding to clicks and taps but not truly engaging with the user. This made me question whether these systems, despite their responsiveness, can really be considered interactive in the way Crawford describes. 

This reading also made me reflect on how I can make my own p5 sketches feel more interactive. Right now, they mostly respond to simple inputs like mouse clicks, but what if I could design interactions that feel more like a two-way conversation?

Instead of just reacting instantly, the system could analyze patterns in user input, adapt its responses based on context, or even introduce an element of unpredictability to keep the interaction dynamic. For example, it could recognize different drawing styles over time and subtly adjust its behavior to match the user’s preferences. Of course, achieving this level of interactivity would likely require more advanced tools than p5.js (Maybe use of LLMs/AI).

I would love to create something beyond basic cause-and-effect responses and explore ways to create a more engaging.

Reading Response 1 – Chance Operations (Week 2)

In his talk, Casey Reas explores the theme of order and chaos in art, emphasizing the role of randomness in creative processes. He also focuses on the notion of “controlled randomness” , where artists establish specific rules and parameters within code, allowing for unexpected outcomes while maintaining the overall coherence.

First of all, I really enjoyed listening to Reas’ talk. I appreciated the way he showcased his work and explained his philosophy on creativity and art. Reflecting on his insights, I aimed to incorporate the concept of controlled randomness in my most recent p5.js sketch, which I titled randomness. The overall structure of the artwork is straightforward—featuring a 10×10 grid of circles. However, the element of controlled randomness comes into play when the user interacts with the piece. Each time the user clicks on the canvas, the circles in the grid change in shape and size, creating a dynamic balance between order and unpredictability. While users can grasp the overall structure, every interaction (mouse click) generates a unique and evolving pattern. I wouldn’t go so far as to claim that my artwork revolutionizes the concept of generative art, but I believe it is an engaging and enjoyable piece to explore.

That being said, Reas’ perspective on art and the creative process encourages me to explore the potential of chance in my work. With every new p5.js sketch I create, I will keep the notion of “controlled randomness” in mind.

Week 2 – Animation, Conditionals, Loops

Concept

In this p5.js sketch, my main objective was to practice for loops and play around with the concept of randomness a bit. It is a simple generative art piece. The idea was to generate a grid of circles, where each circle varies in size, color and level of transparency. So by clicking on the window/canvas, the user can regenerate a completely random pattern (this adds an element of interactivity and makes the artwork dynamic).

Demo (Click on canvas)

Code Highlight:

One of the parts I’m most proud of is the loop structure that efficiently places the circles while maintaining randomness in their size and color:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
for (let x = 0; x < width; x += spacing) {
for (let y = 0; y < height; y += spacing) {
let size = random(10, spacing * 0.8);
let r = random(100, 255);
let g = random(100, 255);
let b = random(100, 255);
fill(r, g, b, 180); // Semi-transparent random color
noStroke();
ellipse(x + spacing / 2, y + spacing / 2, size, size);
}
}
for (let x = 0; x < width; x += spacing) { for (let y = 0; y < height; y += spacing) { let size = random(10, spacing * 0.8); let r = random(100, 255); let g = random(100, 255); let b = random(100, 255); fill(r, g, b, 180); // Semi-transparent random color noStroke(); ellipse(x + spacing / 2, y + spacing / 2, size, size); } }
for (let x = 0; x < width; x += spacing) {
  for (let y = 0; y < height; y += spacing) {
    let size = random(10, spacing * 0.8);
    let r = random(100, 255);
    let g = random(100, 255);
    let b = random(100, 255);

    fill(r, g, b, 180); // Semi-transparent random color
    noStroke();
    ellipse(x + spacing / 2, y + spacing / 2, size, size);
  }
}

Reflection & Improvements for Future Work

This project reinforced my understanding of looping structures in p5.js and the power of randomness in generative art. It was interesting to see how slight variations in parameters can lead to visually engaging results.

Future Work:
Experiment with different shapes (e.g., triangles, squares, or custom polygons). Add animation so circles gradually fade in and out.  Allow users to save their favorite generated patterns.

This was a fun dive into creative coding, and I look forward to exploring more complex generative techniques!

Week 1 – Self Portrait

Concept

My main objective was to create an accurate self-portrait of myself. To achieve this, I started by taking a photo of myself at the beginning of the assignment and then attempted to capture all the details I saw in the photo using p5.js. The challenge arose when it came to my wavy (almost curly) hair, as I needed to accurately depict its shape. Additionally, I spent a considerable amount of time figuring out what kind of facial expression to depict, I ended up doing “closed eyes” look since it looked better than the “open eyes” to me. I also focused on maintaining the correct proportions of my body in the portrait. In the end, I was pleased with the minimalistic aesthetic that emerged, as it seemed to align well with my personality.

Demo

Code Highlight (Hair area):

So this part is the part of the code that I am most proud of, maybe due to the fact that I spent so much time on it 🙂

It was the most challenging part of the code due to various reasons like lack of p5.js knowledge (I had to look a lot of stuff up), trying to make different shapes work in harmony and perfectionism stalling the productivity.

At the end, I tried to imitate a middle-part hairstyle using arcs and triangles (might not be the best way but I am fine with the end result).

Here’s the code snippet of the Hair class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Hair class
class Hair {
draw() {
fill(0, 0, 0); // Hair color
noStroke();
arc(300, 205, 197, 170, PI, 0); // Top hair arc
fill(228, 177, 171); // Skin color
arc(300, 210, 50, 50, PI, 0); // Forehead section (mid-part separation)
fill(1, 1, 1); // Hair color
triangle(201, 205, 275, 205, 175, 230); // Left side bangs
rect(200, 205, 15, 45); // Left side hair sideburn
rect(385, 205, 15, 45); // Right side hair sideburn
triangle(326, 205, 400, 205, 430, 230); // Right side bangs
}
}
// Hair class class Hair { draw() { fill(0, 0, 0); // Hair color noStroke(); arc(300, 205, 197, 170, PI, 0); // Top hair arc fill(228, 177, 171); // Skin color arc(300, 210, 50, 50, PI, 0); // Forehead section (mid-part separation) fill(1, 1, 1); // Hair color triangle(201, 205, 275, 205, 175, 230); // Left side bangs rect(200, 205, 15, 45); // Left side hair sideburn rect(385, 205, 15, 45); // Right side hair sideburn triangle(326, 205, 400, 205, 430, 230); // Right side bangs } }
// Hair class 
class Hair {
  draw() {
    fill(0, 0, 0); // Hair color
    noStroke();
    arc(300, 205, 197, 170, PI, 0); // Top hair arc
    fill(228, 177, 171); // Skin color
    arc(300, 210, 50, 50, PI, 0); // Forehead section (mid-part separation)
    fill(1, 1, 1); // Hair color
    triangle(201, 205, 275, 205, 175, 230); // Left side bangs
    rect(200, 205, 15, 45); // Left side hair sideburn
    rect(385, 205, 15, 45); // Right side hair sideburn
    triangle(326, 205, 400, 205, 430, 230); // Right side bangs
  }
}

Reflection & Improvements for Future Work

The experience was quite enjoyable for me because, I was exposed to p5.js before, but it never went beyond some simple sketches, just messing around with the software, nothing serious.  Additionally, it felt like I was opening a new realm of coding. After completing handful of courses in Computer Science curriculum, coding started to associate with efficiency and work, not necessarily artistic vision/creativity, so I was delighted to work on something creative using code.

I always wanted to try to do digital portraits, previously I only did pencil portraits, but I never had the time or reason to do it until now. Discovering that I could make art with code was a delightful surprise. It made me realize I had missed out on a great way to express myself creatively. This experience has sparked a new interest in combining technology and art in my work, and I’m eager to explore this intersection more in the future.