Week 8 – Creative switch

Ideation

For this project, I created an unusual switch that does not require the use of hands. The switch is activated when two animal-shaped toys from Kinder Surprise make a “kiss.” I used copper foil to conduct electricity, enabling the switch to complete or break a circuit when the toys touch or separate.

Concept and Design

The switch mechanism is based on the principle of completing a circuit when conductive surfaces meet. The copper foil serves as a conductive material, allowing current to flow when the toys touch, thereby activating a response in the circuit.

I initially created a simple circuit where an LED lights up when the toys make contact. Later, I expanded the project by incorporating a second LED to indicate different states:

  • When the toys “kiss,” a green LED turns on.
  • When they are apart, a red LED shines instead.

Circuit 1: Basic Contact Switch

-> When the toys make contact, the circuit closes, and an LED turns on.

Circuit structure

Video demonstration

Code:

const int toyInputPin = 2;    // Pin connected to copper tape
const int ledPin = 13;        // LED output pin

void setup() {
  pinMode(toyInputPin, INPUT_PULLUP);  // Enable internal pull-up
  pinMode(ledPin, OUTPUT);
}

void loop() {
  int contactState = digitalRead(toyInputPin);

  if (contactState == LOW) {
    // Toys are touching — circuit is closed
    digitalWrite(ledPin, HIGH);
  } else {
    // Toys are apart — open circuit
    digitalWrite(ledPin, LOW);
  }

  delay(10); // Small debounce
}

Circuit 2: Dual LED Indicator

    • The red LED is on by default.
    • When the toys touch, the red LED turns off, and a green LED lights up.

Circuit structure

Video demonstration

Code:

const int toyInputPin = 2;    // Copper tape contact pin
const int ledB = 13;          // LED that turns ON when toys touch
const int ledA = 12;          // LED that is ON by default, turns OFF when toys touch

void setup() {
  pinMode(toyInputPin, INPUT_PULLUP);
  pinMode(ledB, OUTPUT);
  pinMode(ledA, OUTPUT);
}

void loop() {
  int contactState = digitalRead(toyInputPin);

  if (contactState == LOW) {
    // Toys are touching
    digitalWrite(ledB, HIGH);  // Turn ON contact LED
    digitalWrite(ledA, LOW);   // Turn OFF default LED
  } else {
    // Toys not touching
    digitalWrite(ledB, LOW);   // Turn OFF contact LED
    digitalWrite(ledA, HIGH);  // Turn ON default LED
  }

  delay(10); // Debounce
}

Challenges and Learnings

  1. Initially, the copper foil contacts were not always reliable. Adjusting the positioning of the conductive material improved accuracy.
  2. The switch was sensitive to small movements, causing flickering. A small delay (10ms) in the code helped stabilize readings.

Future Improvements

Would be interesting to integrate a buzzer that plays a sound when the toys kiss.

Week 8 Reading

Reading Norman’s Emotion & Design: Attractive Things Work Better made me think about how much our emotions influence not only our experience with products but also our perception of their effectiveness. I have always been drawn to minimalist and aesthetically pleasing designs, but I never consciously considered that the way something looks could actually change how well it functions in my mind. Norman’s argument that attractive things put us in a positive emotional state, which then makes us more creative and tolerant of problems, really resonated with me. It reminds me of how I feel when I use a well-designed app: if the interface is clean and straightforward, I naturally assume it’s easier to use, even before I’ve tested its features. When choosing basic necessity goods at the store, I tend to lean towards those with an aesthetically appealing package.  It also made me reflect on branding strategies I’ve worked on in the past, where visual identity is not just about aesthetics but about making the user feel like they resonate with the brand’s values enough to engage more deeply.

Similarly, Her Code Got Humans on the Moon was a fascinating look at Margaret Hamilton’s contributions to the Apollo program, and it left me thinking about the intersection of creativity and logic in problem-solving. Her story challenged the way I usually imagine coding (often as monotonous and technical) by showing how she had to anticipate every possible mistake and failure scenario. I was especially struck by how she fought for software engineering to be recognized as a legitimate discipline. It made me wonder how many fields today are still in that stage, where groundbreaking work is being done but isn’t yet fully acknowledged. In a way, it reminds me of digital community-building and brand storytelling, which are the areas I work in that are often undervalued despite their fundamental importance.

Both readings reinforced something I’ve been thinking about a lot: creativity isn’t just for traditional “creative” roles. Whether in design, engineering, or strategy, thinking outside the box is what pushes boundaries and leads us to creating great things.

Midterm project – Whack a Mole

For my midterm project, I decided to create my own version of the classic Whack-A-Mole game. The goal was to design an interactive experience where players could test their reflexes by clicking on moles as they randomly appeared on the screen. It was inspired by both traditional arcade versions and mobile games, where I wanted to reach a balance between simple navigation and entertainment.

One of the aspects I’m particularly proud of is the way I structured the game logic. I tried to keep my code modular by organizing key components into functions. This not only made the development process more efficient but also allowed me to tweak game parameters easily. Another feature I really enjoyed implementing was the randomization of mole appearances that ensures that no two games feel exactly the same. The timing and positioning algorithms took some hard work, but I’m happy with how smooth the flow of the game turned out.

How it works: Moles pop up from different holes at random intervals, and the player must click on them to score points. Each mole stays visible for a short duration before disappearing, which requires quick reactions from the player. As the game progresses, the frequency of mole appearances increases, adding a greater challenge over time. To add an element of risk, I implemented a bomb mechanic, meaning that if a player accidentally clicks on a bomb instead of a mole, the game ends immediately. This twist keeps players alert and encourages more precise clicking. The game runs in a continuous loop with constant updates on the screen that track the player’s score. Each successful mole hit adds points, while avoiding bombs becomes crucial for “survival”.

Challenges: One of the biggest issues was making sure the game felt responsive without being overwhelming. Initially, the moles appeared and disappeared too quickly which made it nearly impossible to hit them in time. I had to experiment with different timing settings to find the right balance. Another tricky aspect was collision detection, where I had to make sure that clicks were registered correctly when a mole is hit and not on empty spots or bombs. Debugging this issue took some time, but through testing and refining my hitbox logic, I was able to get it working.

Implementation: The game is built with JavaScript and p5.js, which handle the graphics and interactions. I used free images and sound effects from online stocks to make the game more engaging.  The Game class manages the core mechanics which includes the following:

  • Hole and Object Management – moles and bombs are placed randomly to keep the game unpredictable
  • Scoring System – players earn points for hitting moles, while bombs immediately end the game
  • Difficulty Scaling – as the game progresses, moles appear and disappear faster, making it more challenging
  • Sound Effects and Graphics – Background music, whacking sounds, and animations add to an immersive experienceCode snippets:>>Managing moles and bombs
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    if (mouseIsPressed) {
    if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) {
    mouseIsPressed = false;
    punched = true;
    sounds.whak.play();
    setTimeout(() => {
    punched = false;
    }, 200);
    if (hole.type == "mole") {
    hole.type = "hole";
    game.score += 1;
    game.timer += 30;
    } else {
    sounds.bomb.play();
    gameOver();
    }
    }
    }
    if (mouseIsPressed) { if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) { mouseIsPressed = false; punched = true; sounds.whak.play(); setTimeout(() => { punched = false; }, 200); if (hole.type == "mole") { hole.type = "hole"; game.score += 1; game.timer += 30; } else { sounds.bomb.play(); gameOver(); } } }
    if (mouseIsPressed) {
      if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) {
        mouseIsPressed = false;
        punched = true;
        sounds.whak.play();
        setTimeout(() => {
          punched = false;
        }, 200);
        if (hole.type == "mole") {
          hole.type = "hole";
          game.score += 1;
          game.timer += 30;
        } else {
          sounds.bomb.play();
          gameOver();
        }
      }
    }
    

    >>Registering the mouse clicks

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    if (frameCount % (game.difficulty - game.score) == 0) {
    let hole = random(game.holes);
    if (hole.type == "mole") {
    hole.type = "hole";
    } else {
    hole.type = "mole";
    }
    if (random(1) < 0.1) {
    hole.type = "bomb";
    setTimeout(() => {
    hole.type = "hole";
    }, 1000);
    }
    }
    if (frameCount % (game.difficulty - game.score) == 0) { let hole = random(game.holes); if (hole.type == "mole") { hole.type = "hole"; } else { hole.type = "mole"; } if (random(1) < 0.1) { hole.type = "bomb"; setTimeout(() => { hole.type = "hole"; }, 1000); } }
    if (frameCount % (game.difficulty - game.score) == 0) {
      let hole = random(game.holes);
      if (hole.type == "mole") {
        hole.type = "hole";
      } else {
        hole.type = "mole";
      }
      if (random(1) < 0.1) {
        hole.type = "bomb";
        setTimeout(() => {
          hole.type = "hole";
        }, 1000);
      }
    }
    

    >>Game Over logic

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    function gameOver() {
    sounds.gameover.play();
    setTimeout(() => {
    sounds.back.stop();
    image(imgs.blast.img, mouseX, mouseY, 250, 250);
    background(20, 100);
    textSize(64);
    text("Game Over", width / 2, height / 2);
    textSize(16);
    text("click anywhere to restart!", width / 2, height - 50);
    textSize(46);
    text(game.score, width / 2, 35);
    state = "gameOver";
    }, 100);
    noLoop();
    }
    function gameOver() { sounds.gameover.play(); setTimeout(() => { sounds.back.stop(); image(imgs.blast.img, mouseX, mouseY, 250, 250); background(20, 100); textSize(64); text("Game Over", width / 2, height / 2); textSize(16); text("click anywhere to restart!", width / 2, height - 50); textSize(46); text(game.score, width / 2, 35); state = "gameOver"; }, 100); noLoop(); }
    function gameOver() {
      sounds.gameover.play();
      setTimeout(() => {
        sounds.back.stop();
        image(imgs.blast.img, mouseX, mouseY, 250, 250);
        background(20, 100);
        textSize(64);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("click anywhere to restart!", width / 2, height - 50);
        textSize(46);
        text(game.score, width / 2, 35);
        state = "gameOver";
      }, 100);
      noLoop();
    }
    
    

    Improvements: Looking forward, there are definitely some areas that I’d like to improve. One feature I would like to add is a progressive difficulty system, where players are challenged at different levels of difficulty. Right now, the game is fun but could benefit from more depth. On top of that, I’d like to upgrade the user interface by adding a “start” and “home” screen, score tracker, and possibly a leaderboard.

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    let imgs = {};
    let sounds = {};
    let punched = false;
    let state = "start";
    function preload() {
    backImg = loadImage("assets/back.png");
    font = loadFont("assets/Sigmar-Regular.ttf");
    imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 };
    imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 };
    imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 };
    imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 };
    imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 };
    sounds.bomb = loadSound("sounds/Bomb hit.mp3");
    sounds.back = loadSound("sounds/Game main theme.mp3");
    sounds.gameover = loadSound("sounds/game-over.mp3");
    sounds.whak = loadSound("sounds/Whacking a mole.mp3");
    }
    function setup() {
    createCanvas(600, 600);
    imageMode(CENTER);
    textFont(font);
    game = new Game();
    textAlign(CENTER, CENTER);
    }
    function draw() {
    image(backImg, width / 2, height / 2, width, height);
    switch (state) {
    case "start":
    sounds.back.stop();
    textSize(68);
    fill(255);
    text("WhACK!\na MOLE!", width / 2, height / 2 - 120);
    textSize(16);
    text("press anywhere to start!", width / 2, height - 30);
    textSize(26);
    let img = [imgs.hole, imgs.mole, imgs.bomb];
    image(
    img[floor(frameCount / 60) % 3].img,
    width / 2 + img[floor(frameCount / 60) % 3].xoff,
    height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff
    );
    if (mouseIsPressed) {
    mouseIsPressed = false;
    sounds.whak.play();
    state = "game";
    }
    break;
    case "game":
    game.show();
    if (!sounds.back.isPlaying()) sounds.back.play();
    break;
    }
    if (mouseX != 0 && mouseY != 0) {
    push();
    translate(mouseX, mouseY + 10);
    if (punched) {
    rotate(-PI / 2);
    }
    scale(map(game.holes.length, 4, 20, 1, 0.25));
    image(imgs.hammer.img, 0, 0, 150, 150);
    pop();
    }
    }
    function mousePressed() {
    if (state == "gameOver") {
    state = "start";
    game = new Game();
    mouseIsPressed = false;
    loop();
    }
    }
    function gameOver() {
    sounds.gameover.play();
    setTimeout(() => {
    sounds.back.stop();
    image(imgs.blast.img, mouseX, mouseY, 250, 250);
    background(20, 100);
    textSize(64);
    text("Game Over", width / 2, height / 2);
    textSize(16);
    text("click anywhere to restart!", width / 2, height - 50);
    textSize(46);
    text(game.score, width / 2, 35);
    state = "gameOver";
    }, 100);
    noLoop();
    }
    class Game {
    constructor() {
    this.x = 10;
    this.y = height / 2 - 80;
    this.w = width - 20;
    this.h = height / 2 + 70;
    this.holesNum = 4;
    this.holes = [];
    this.difficulty = 60;
    this.score = 0;
    this.timer = 4800;
    }
    show() {
    //timer
    if (this.timer > 4800) this.timer = 4800;
    this.timer -= 1.5;
    fill(20, 100);
    rect(10, 5, width - 20, 10);
    fill(255);
    rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10);
    if (this.timer < 0) {
    mouseX = width / 2;
    mouseY = height / 2;
    gameOver();
    }
    //score
    fill(255);
    textSize(46);
    if (punched) textSize(54);
    text(this.score, width / 2, 35);
    if (this.holesNum != this.holes.length) {
    this.holes = this.findHolePositions(1);
    }
    for (let i = 0; i < this.holes.length; i++) {
    push();
    translate(this.holes[i].x, this.holes[i].y);
    scale(this.holes[i].d / 250);
    let img;
    switch (this.holes[i].type) {
    case "hole":
    img = imgs.hole;
    //nothing
    break;
    case "mole":
    img = imgs.mole;
    break;
    case "bomb":
    img = imgs.bomb;
    break;
    }
    if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") {
    //check mouse click on mole
    if (mouseIsPressed) {
    if (
    dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) <
    this.holes[i].d
    ) {
    mouseIsPressed = false;
    punched = true;
    sounds.whak.play();
    setTimeout(() => {
    punched = false;
    }, 200);
    if (this.holes[i].type == "mole") {
    this.holes[i].type = "hole";
    this.score += 1;
    this.timer += 30;
    } else {
    sounds.bomb.play();
    gameOver();
    }
    }
    }
    }
    image(img.img, img.xoff, img.yoff);
    pop();
    }
    if (this.difficulty - this.score < 20) {
    this.difficulty += 30;
    this.holesNum += 1;
    }
    if (frameCount % (this.difficulty - this.score) == 0) {
    let hole = random(this.holes);
    if (hole.type == "mole") {
    hole.type = "hole";
    } else {
    hole.type = "mole";
    }
    if (random(1) < 0.1) {
    hole.type = "bomb";
    setTimeout(() => {
    hole.type = "hole";
    }, 1000);
    }
    }
    }
    findHolePositions(n, d = 200) {
    let arr = [];
    for (let i = 0; i < this.holesNum; i++) {
    let x = random(this.x + d / 2, this.x + this.w - d / 2);
    let y = random(this.y + d / 2, this.y + this.h - d / 2);
    arr.push({ x: x, y: y, d: d, type: "hole" });
    }
    //no hole should overlap
    for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length; j++) {
    if (i != j) {
    let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y);
    if (d_ < d) {
    n += 1;
    if (n > 50) {
    n = 0;
    d *= 0.9;
    return this.findHolePositions(n, d);
    }
    return this.findHolePositions(n, d);
    }
    }
    }
    }
    return arr;
    }
    }
    let imgs = {}; let sounds = {}; let punched = false; let state = "start"; function preload() { backImg = loadImage("assets/back.png"); font = loadFont("assets/Sigmar-Regular.ttf"); imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 }; imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 }; imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 }; imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 }; imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 }; sounds.bomb = loadSound("sounds/Bomb hit.mp3"); sounds.back = loadSound("sounds/Game main theme.mp3"); sounds.gameover = loadSound("sounds/game-over.mp3"); sounds.whak = loadSound("sounds/Whacking a mole.mp3"); } function setup() { createCanvas(600, 600); imageMode(CENTER); textFont(font); game = new Game(); textAlign(CENTER, CENTER); } function draw() { image(backImg, width / 2, height / 2, width, height); switch (state) { case "start": sounds.back.stop(); textSize(68); fill(255); text("WhACK!\na MOLE!", width / 2, height / 2 - 120); textSize(16); text("press anywhere to start!", width / 2, height - 30); textSize(26); let img = [imgs.hole, imgs.mole, imgs.bomb]; image( img[floor(frameCount / 60) % 3].img, width / 2 + img[floor(frameCount / 60) % 3].xoff, height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff ); if (mouseIsPressed) { mouseIsPressed = false; sounds.whak.play(); state = "game"; } break; case "game": game.show(); if (!sounds.back.isPlaying()) sounds.back.play(); break; } if (mouseX != 0 && mouseY != 0) { push(); translate(mouseX, mouseY + 10); if (punched) { rotate(-PI / 2); } scale(map(game.holes.length, 4, 20, 1, 0.25)); image(imgs.hammer.img, 0, 0, 150, 150); pop(); } } function mousePressed() { if (state == "gameOver") { state = "start"; game = new Game(); mouseIsPressed = false; loop(); } } function gameOver() { sounds.gameover.play(); setTimeout(() => { sounds.back.stop(); image(imgs.blast.img, mouseX, mouseY, 250, 250); background(20, 100); textSize(64); text("Game Over", width / 2, height / 2); textSize(16); text("click anywhere to restart!", width / 2, height - 50); textSize(46); text(game.score, width / 2, 35); state = "gameOver"; }, 100); noLoop(); } class Game { constructor() { this.x = 10; this.y = height / 2 - 80; this.w = width - 20; this.h = height / 2 + 70; this.holesNum = 4; this.holes = []; this.difficulty = 60; this.score = 0; this.timer = 4800; } show() { //timer if (this.timer > 4800) this.timer = 4800; this.timer -= 1.5; fill(20, 100); rect(10, 5, width - 20, 10); fill(255); rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10); if (this.timer < 0) { mouseX = width / 2; mouseY = height / 2; gameOver(); } //score fill(255); textSize(46); if (punched) textSize(54); text(this.score, width / 2, 35); if (this.holesNum != this.holes.length) { this.holes = this.findHolePositions(1); } for (let i = 0; i < this.holes.length; i++) { push(); translate(this.holes[i].x, this.holes[i].y); scale(this.holes[i].d / 250); let img; switch (this.holes[i].type) { case "hole": img = imgs.hole; //nothing break; case "mole": img = imgs.mole; break; case "bomb": img = imgs.bomb; break; } if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") { //check mouse click on mole if (mouseIsPressed) { if ( dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) < this.holes[i].d ) { mouseIsPressed = false; punched = true; sounds.whak.play(); setTimeout(() => { punched = false; }, 200); if (this.holes[i].type == "mole") { this.holes[i].type = "hole"; this.score += 1; this.timer += 30; } else { sounds.bomb.play(); gameOver(); } } } } image(img.img, img.xoff, img.yoff); pop(); } if (this.difficulty - this.score < 20) { this.difficulty += 30; this.holesNum += 1; } if (frameCount % (this.difficulty - this.score) == 0) { let hole = random(this.holes); if (hole.type == "mole") { hole.type = "hole"; } else { hole.type = "mole"; } if (random(1) < 0.1) { hole.type = "bomb"; setTimeout(() => { hole.type = "hole"; }, 1000); } } } findHolePositions(n, d = 200) { let arr = []; for (let i = 0; i < this.holesNum; i++) { let x = random(this.x + d / 2, this.x + this.w - d / 2); let y = random(this.y + d / 2, this.y + this.h - d / 2); arr.push({ x: x, y: y, d: d, type: "hole" }); } //no hole should overlap for (let i = 0; i < arr.length; i++) { for (let j = 0; j < arr.length; j++) { if (i != j) { let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y); if (d_ < d) { n += 1; if (n > 50) { n = 0; d *= 0.9; return this.findHolePositions(n, d); } return this.findHolePositions(n, d); } } } } return arr; } }
    let imgs = {};
    let sounds = {};
    let punched = false;
    let state = "start";
    
    function preload() {
      backImg = loadImage("assets/back.png");
      font = loadFont("assets/Sigmar-Regular.ttf");
      imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 };
      imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 };
      imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 };
      imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 };
      imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 };
    
      sounds.bomb = loadSound("sounds/Bomb hit.mp3");
      sounds.back = loadSound("sounds/Game main theme.mp3");
      sounds.gameover = loadSound("sounds/game-over.mp3");
      sounds.whak = loadSound("sounds/Whacking a mole.mp3");
    }
    
    function setup() {
      createCanvas(600, 600);
      imageMode(CENTER);
      textFont(font);
      game = new Game();
      textAlign(CENTER, CENTER);
    }
    
    function draw() {
      image(backImg, width / 2, height / 2, width, height);
      switch (state) {
        case "start":
          sounds.back.stop();
    
          textSize(68);
          fill(255);
          text("WhACK!\na MOLE!", width / 2, height / 2 - 120);
          textSize(16);
          text("press anywhere to start!", width / 2, height - 30);
    
          textSize(26);
          let img = [imgs.hole, imgs.mole, imgs.bomb];
          image(
            img[floor(frameCount / 60) % 3].img,
            width / 2 + img[floor(frameCount / 60) % 3].xoff,
            height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff
          );
          if (mouseIsPressed) {
            mouseIsPressed = false;
            sounds.whak.play();
            state = "game";
          }
          break;
        case "game":
          game.show();
          if (!sounds.back.isPlaying()) sounds.back.play();
          break;
      }
      if (mouseX != 0 && mouseY != 0) {
        push();
        translate(mouseX, mouseY + 10);
        if (punched) {
          rotate(-PI / 2);
        }
        scale(map(game.holes.length, 4, 20, 1, 0.25));
        image(imgs.hammer.img, 0, 0, 150, 150);
        pop();
      }
    }
    
    function mousePressed() {
      if (state == "gameOver") {
        state = "start";
        game = new Game();
        mouseIsPressed = false;
        loop();
      }
    }
    function gameOver() {
      sounds.gameover.play();
      setTimeout(() => {
        sounds.back.stop();
        image(imgs.blast.img, mouseX, mouseY, 250, 250);
        background(20, 100);
        textSize(64);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("click anywhere to restart!", width / 2, height - 50);
        textSize(46);
        text(game.score, width / 2, 35);
        state = "gameOver";
      }, 100);
    
      noLoop();
    }
    
    class Game {
      constructor() {
        this.x = 10;
        this.y = height / 2 - 80;
        this.w = width - 20;
        this.h = height / 2 + 70;
    
        this.holesNum = 4;
        this.holes = [];
        this.difficulty = 60;
        this.score = 0;
        this.timer = 4800;
      }
      show() {
        //timer
        if (this.timer > 4800) this.timer = 4800;
        this.timer -= 1.5;
        fill(20, 100);
        rect(10, 5, width - 20, 10);
        fill(255);
        rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10);
        if (this.timer < 0) {
          mouseX = width / 2;
          mouseY = height / 2;
          gameOver();
        }
    
        //score
        fill(255);
        textSize(46);
        if (punched) textSize(54);
        text(this.score, width / 2, 35);
    
        if (this.holesNum != this.holes.length) {
          this.holes = this.findHolePositions(1);
        }
        for (let i = 0; i < this.holes.length; i++) {
          push();
          translate(this.holes[i].x, this.holes[i].y);
          scale(this.holes[i].d / 250);
          let img;
          switch (this.holes[i].type) {
            case "hole":
              img = imgs.hole;
              //nothing
              break;
            case "mole":
              img = imgs.mole;
              break;
            case "bomb":
              img = imgs.bomb;
              break;
          }
    
          if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") {
            //check mouse click on mole
            if (mouseIsPressed) {
              if (
                dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) <
                this.holes[i].d
              ) {
                mouseIsPressed = false;
                punched = true;
                sounds.whak.play();
                setTimeout(() => {
                  punched = false;
                }, 200);
                if (this.holes[i].type == "mole") {
                  this.holes[i].type = "hole";
                  this.score += 1;
                  this.timer += 30;
                } else {
                  sounds.bomb.play();
                  gameOver();
                }
              }
            }
          }
          image(img.img, img.xoff, img.yoff);
          pop();
        }
        if (this.difficulty - this.score < 20) {
          this.difficulty += 30;
          this.holesNum += 1;
        }
    
        if (frameCount % (this.difficulty - this.score) == 0) {
          let hole = random(this.holes);
          if (hole.type == "mole") {
            hole.type = "hole";
          } else {
            hole.type = "mole";
          }
          if (random(1) < 0.1) {
            hole.type = "bomb";
            setTimeout(() => {
              hole.type = "hole";
            }, 1000);
          }
        }
      }
    
      findHolePositions(n, d = 200) {
        let arr = [];
    
        for (let i = 0; i < this.holesNum; i++) {
          let x = random(this.x + d / 2, this.x + this.w - d / 2);
          let y = random(this.y + d / 2, this.y + this.h - d / 2);
          arr.push({ x: x, y: y, d: d, type: "hole" });
        }
        //no hole should overlap
        for (let i = 0; i < arr.length; i++) {
          for (let j = 0; j < arr.length; j++) {
            if (i != j) {
              let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y);
              if (d_ < d) {
                n += 1;
                if (n > 50) {
                  n = 0;
                  d *= 0.9;
                  return this.findHolePositions(n, d);
                }
                return this.findHolePositions(n, d);
              }
            }
          }
        }
        return arr;
      }
    }
    

    The Visuals:

Midterm Project Plan

Scope of Work
For my midterm project, I am designing a digital version of the classic Whack-a-Mole game, inspired by the attached references. The goal is to create an engaging and interactive game where players use their mouse to “whack” moles as they pop out of holes. The game should challenge players’ reflexes and introduce risk elements like bombs that add complexity.

The game will start with a Start Screen featuring the game title and a simple “Start” button. I also plan to add access settings, like toggling sound on or off. Once the game begins, moles will randomly pop up from a grid of holes, and the player must click or tap on them to score points. Not every hole will be safe. Occasionally, a bomb will pop up, and hitting it will result in losing points or lives. Players will have a limited time (for example, 60 seconds) to score as many points as possible before the timer runs out. As the game progresses, the difficulty will increase, with moles appearing and disappearing faster, making the game more challenging.

Each successful hit of a mole adds 10 points, while mistakenly hitting a bomb will deduct 20 points or reduce a life. The game will display a score counter and a countdown timer to keep players aware of their progress and remaining time. At the end of the game, an End Screen will appear, showing the final score and offering options to “Play Again” or “Quit.”

I want the grid to contain between 9 and 16 holes, depending on the level of complexity I decide to implement. Moles and bombs will randomly pop up in these holes at varying intervals. The randomness is crucial to establish unpredictability. To add to the challenge, the moles will pop up faster as time progresses, requiring quicker reflexes from the player.

Code Structure

For the game’s development, I plan to use an object-oriented approach. The game will be structured around a few core classes:

  • Game Class: Manages the overall game loop, score tracking, and time countdown
  • Mole Class: Controls the mole’s behavior (when it pops up, how long it stays, and how it reacts to player interaction)
  • Bomb Class: Functions similarly to the mole but triggers penalties when a bomb is clicked
  • Hole Class: Represents each position on the grid, randomly spawning moles or bombs
  • UI Class: Manages elements like the start screen, score display, timer, and end screen

The core gameplay loop will rely on these functions:

  • startGame(): Initializes the game and resets scores and timers
  • spawnMole(): Randomly selects holes for moles to appear
  • spawnBomb(): Introduces bombs with a set probability
  • whackMole(): Detects player clicks and updates the score
  • hitBomb(): Triggers penalties for clicking bombs
  • updateTimer(): Counts down and ends the game when time runs out
  • increaseDifficulty(): Speeds up mole appearances as the game progresses

Challenges and Risks

I expect that one of the most complex parts of this project would be ensuring accurate collision detection. In other words, making sure the program properly registers when a player clicks on a mole or bomb.

Timing is also a big concern. Moles need to appear and disappear at unpredictable but balanced intervals to make the pace of the game flawless and not frustrating.

To tackle these challenges, I plan to build a test script focused on collision detection, using simple shapes before applying it to the actual mole and bomb sprites. This will help me adjust the hitboxes and make sure user interactions feel responsive. I might also test randomization algorithms to ensure that mole and bomb appearances are unpredictable yet adequate.

Week5 Reading: “Computer Vision for Artists and Designers”

Reading this article felt like taking a peek behind the scenes of how computers “see” the world, which is a process that’s really different from how we see it. Unlike our eyes that automatically pick up on contexts, depths, and meanings, computers have to break everything down into pixels and simple data to be able to process it. They use techniques like frame differencing, background subtraction, and brightness thresholding to detect movement and distinguish objects from their surroundings. In other words, while we instantly recognize a face or a smile, a computer needs a lot of help to do even simple things like tell the difference between a moving person and a stationary background.

What really stood out to me was how much effort goes into making the computer’s vision work well. It’s much more than just writing code. It’s also about setting up the right physical conditions like using specific lighting, reflective materials, or even special cameras to boost the system’s accuracy. This mix of coding and physical tweaking shows that computer vision is as much an art as it is a science.

I also found it interesting how computer vision’s tracking and surveillance capabilities have shaped interactive media art. On one hand, these systems let artists create installations where your movements become part of the artwork (like in the classic Videoplace) but on the other hand, there’s a darker side: the same technology that can create immersive art experiences can also be used to monitor and profile people. This duality makes me think about the ethical implications and the balance between creating engaging art and respecting personal privacy.

To sum up, the article not only breaks down the technical side of computer vision for beginners but also opens up deeper questions about technology’s role in our lives, both as a creative tool and a way of surveillance.

Week 4 – reading response

Reading Response: The Psychopathology of Everyday Things

Something That Drives Me Crazy: Confusing Shower Knobs

One of the most frustrating design flaws in everyday life is the lack of a standardized, intuitive shower knob system across different countries. If you’ve ever traveled internationally, you’ve likely encountered showers that seem to require a degree in engineering to operate.

In the United States, many showers have a single-knob system where you turn left for hot water and right for cold. However, some models require you to pull or push the knob, which isn’t always obvious. In the United Kingdom, separate hot and cold taps are still common, making us mix water manually. In Norway, for example, some showers have buttons that must be pressed in a particular sequence before water flows. The inconsistency means that travellers like me often accidentally scald themselves, turn on the water at full blast, or get an unexpected cold shock.

The biggest issue is discoverability. There’s often no clear indication of how the system works. Some showers even have extra knobs that control water pressure or temperature separately, adding to more confusion. Without obvious instructions, we are left to experiment, sometimes getting drenched unexpectedly in freezing or boiling water.

Applying Norman’s Principles to Improve Shower Design

Don Norman’s principles of discoverability, affordances, and signifiers could help improve shower designs:

  1. Clear affordances. The shape and placement of knobs should signify their function. A lever-style handle naturally implies to us that it has to be turned, while a button clearly calls for you to press it.
  2. Icons or labels could tell us about temperature directions, with simple red/blue colors universally suggesting hot and cold temp.
  3. Natural mapping. This could be a horizontal sliding control that moves left for hot and right for cold. It would be more intuitive than rotating knobs in random directions.

Another potential hi-tech solution is a digital shower interface with a display which could get rid of confusion entirely.

Week 4 – generative text output

Inspiration for This Project

I wanted to create an interactive and visually engaging experience that merges astrology with generative art. The idea was to provide users with a simple yet immersive way to receive a zodiac-based “psychic reading,” followed by animations and visuals. Astrology is often associated with mysticism and magic, so I aimed to reflect that via changing background colours and adding floating particles. For the visual part, I took my inspiration from this website: https://www.horoscope.com/us/index.aspx

Code Highlight I Am Proud Of

One part of the code I’m particularly proud of is the getZodiacColor function, which assigns a unique background color to each zodiac sign:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function getZodiacColor(sign) {
let colors = {
"Aries": color(255, 99, 71), "Taurus": color(107, 142, 35), "Gemini": color(255, 215, 0), "Cancer": color(70, 130, 180),
"Leo": color(255, 165, 0), "Virgo": color(46, 139, 87), "Libra": color(123, 104, 238), "Scorpio": color(148, 0, 211),
"Sagittarius": color(255, 140, 0), "Capricorn": color(139, 69, 19), "Aquarius": color(0, 191, 255), "Pisces": color(72, 61, 139)
};
return colors[sign] || color(240);
}
function getZodiacColor(sign) { let colors = { "Aries": color(255, 99, 71), "Taurus": color(107, 142, 35), "Gemini": color(255, 215, 0), "Cancer": color(70, 130, 180), "Leo": color(255, 165, 0), "Virgo": color(46, 139, 87), "Libra": color(123, 104, 238), "Scorpio": color(148, 0, 211), "Sagittarius": color(255, 140, 0), "Capricorn": color(139, 69, 19), "Aquarius": color(0, 191, 255), "Pisces": color(72, 61, 139) }; return colors[sign] || color(240); }
function getZodiacColor(sign) {
  let colors = {
    "Aries": color(255, 99, 71), "Taurus": color(107, 142, 35), "Gemini": color(255, 215, 0), "Cancer": color(70, 130, 180),
    "Leo": color(255, 165, 0), "Virgo": color(46, 139, 87), "Libra": color(123, 104, 238), "Scorpio": color(148, 0, 211),
    "Sagittarius": color(255, 140, 0), "Capricorn": color(139, 69, 19), "Aquarius": color(0, 191, 255), "Pisces": color(72, 61, 139)
  };
  return colors[sign] || color(240);
}

This function is simple, but it instantly transforms the visual feel of the project based on the user’s selection, creating some sense of personalization.

Reflection

For future projects, I’d love to explore more complex generative animations, such as constellations that change based on the zodiac sign. Things like integrating sound effects or subtle ambient music could enhance the mystical atmosphere. Another direction could be adding more interactive elements, like having particles respond to mouse movement, making the experience feel even more magical and immersive.

Here is the full code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let signs = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"
];
// Zodiac readings for each sign
let readings = {
"Aries": ["Today is a day for bold moves.", "A new adventure awaits you.", "Your energy will attract opportunities."],
"Taurus": ["Stay grounded, but take a leap of faith.", "Patience will bring unexpected rewards.", "A financial opportunity is coming your way."],
"Gemini": ["A conversation will spark inspiration.", "Your curiosity leads to a surprising discovery.", "Adaptability is your greatest strength today."],
"Cancer": ["Your emotions will guide you well.", "A nostalgic moment will bring clarity.", "Trust your intuition—it knows the way."],
"Leo": ["Your confidence will open doors.", "A bold move will lead to admiration.", "Shine your light and others will follow."],
"Virgo": ["Your keen eye will catch an important detail.", "Organization will bring unexpected rewards.", "A small habit change will lead to a breakthrough."],
"Libra": ["Balance is key today.", "A relationship will deepen in an unexpected way.", "Harmony will find you when you least expect it."],
"Scorpio": ["Mystery surrounds you—embrace it.", "Transformation is closer than you think.", "Your passion will lead you to new heights."],
"Sagittarius": ["An exciting journey is on the horizon.", "Your optimism will inspire someone today.", "The universe is conspiring in your favor."],
"Capricorn": ["Hard work pays off—stay focused.", "A disciplined approach will yield results.", "Your perseverance will be rewarded soon."],
"Aquarius": ["Innovation is your ally today.", "A sudden insight will change your path.", "Your unique perspective is your greatest strength."],
"Pisces": ["Your dreams hold important messages.", "Creativity will flow effortlessly.", "A moment of solitude will bring deep understanding."]
};
let dropdown, button, output;
let bgColor;
let particles = [];
function setup() {
createCanvas(400, 300);
textSize(16);
textAlign(CENTER, CENTER);
// Create dropdown menu for zodiac signs
dropdown = createSelect();
dropdown.position(100, 100);
for (let sign of signs) {
dropdown.option(sign);
}
// Create button to generate reading
button = createButton("Get Your Reading");
button.position(100, 140);
button.mousePressed(generateReading);
output = "Select your sign and receive your reading";
bgColor = color(240);
// Create floating particles for magical effect
for (let i = 0; i < 50; i++) {
particles.push(new Particle());
}
}
function draw() {
background(bgColor);
fill(50);
text("Psychic Zodiac Reading", width / 2, 50);
text(output, width / 2, 80);
// Update and show floating particles
for (let p of particles) {
p.update();
p.show();
}
}
// Generate random reading based on selected zodiac sign
function generateReading() {
let selectedSign = dropdown.value();
let possibleReadings = readings[selectedSign];
output = possibleReadings[int(random(possibleReadings.length))];
bgColor = getZodiacColor(selectedSign);
}
// Assign unique background color for each zodiac sign
function getZodiacColor(sign) {
let colors = {
"Aries": color(255, 99, 71), "Taurus": color(107, 142, 35), "Gemini": color(255, 215, 0), "Cancer": color(70, 130, 180),
"Leo": color(255, 165, 0), "Virgo": color(46, 139, 87), "Libra": color(123, 104, 238), "Scorpio": color(148, 0, 211),
"Sagittarius": color(255, 140, 0), "Capricorn": color(139, 69, 19), "Aquarius": color(0, 191, 255), "Pisces": color(72, 61, 139)
};
return colors[sign] || color(240);
}
// Particle class for floating magic effect
class Particle {
constructor() {
this.x = random(width);
this.y = random(height);
this.vx = random(-1, 1);
this.vy = random(-1, 1);
this.alpha = random(100, 255);
}
// Update particle movement
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x > width || this.x < 0) this.vx *= -1;
if (this.y > height || this.y < 0) this.vy *= -1;
}
// Display particle as a glowing dot
show() {
noStroke();
fill(255, this.alpha);
ellipse(this.x, this.y, 5, 5);
}
}
let signs = [ "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces" ]; // Zodiac readings for each sign let readings = { "Aries": ["Today is a day for bold moves.", "A new adventure awaits you.", "Your energy will attract opportunities."], "Taurus": ["Stay grounded, but take a leap of faith.", "Patience will bring unexpected rewards.", "A financial opportunity is coming your way."], "Gemini": ["A conversation will spark inspiration.", "Your curiosity leads to a surprising discovery.", "Adaptability is your greatest strength today."], "Cancer": ["Your emotions will guide you well.", "A nostalgic moment will bring clarity.", "Trust your intuition—it knows the way."], "Leo": ["Your confidence will open doors.", "A bold move will lead to admiration.", "Shine your light and others will follow."], "Virgo": ["Your keen eye will catch an important detail.", "Organization will bring unexpected rewards.", "A small habit change will lead to a breakthrough."], "Libra": ["Balance is key today.", "A relationship will deepen in an unexpected way.", "Harmony will find you when you least expect it."], "Scorpio": ["Mystery surrounds you—embrace it.", "Transformation is closer than you think.", "Your passion will lead you to new heights."], "Sagittarius": ["An exciting journey is on the horizon.", "Your optimism will inspire someone today.", "The universe is conspiring in your favor."], "Capricorn": ["Hard work pays off—stay focused.", "A disciplined approach will yield results.", "Your perseverance will be rewarded soon."], "Aquarius": ["Innovation is your ally today.", "A sudden insight will change your path.", "Your unique perspective is your greatest strength."], "Pisces": ["Your dreams hold important messages.", "Creativity will flow effortlessly.", "A moment of solitude will bring deep understanding."] }; let dropdown, button, output; let bgColor; let particles = []; function setup() { createCanvas(400, 300); textSize(16); textAlign(CENTER, CENTER); // Create dropdown menu for zodiac signs dropdown = createSelect(); dropdown.position(100, 100); for (let sign of signs) { dropdown.option(sign); } // Create button to generate reading button = createButton("Get Your Reading"); button.position(100, 140); button.mousePressed(generateReading); output = "Select your sign and receive your reading"; bgColor = color(240); // Create floating particles for magical effect for (let i = 0; i < 50; i++) { particles.push(new Particle()); } } function draw() { background(bgColor); fill(50); text("Psychic Zodiac Reading", width / 2, 50); text(output, width / 2, 80); // Update and show floating particles for (let p of particles) { p.update(); p.show(); } } // Generate random reading based on selected zodiac sign function generateReading() { let selectedSign = dropdown.value(); let possibleReadings = readings[selectedSign]; output = possibleReadings[int(random(possibleReadings.length))]; bgColor = getZodiacColor(selectedSign); } // Assign unique background color for each zodiac sign function getZodiacColor(sign) { let colors = { "Aries": color(255, 99, 71), "Taurus": color(107, 142, 35), "Gemini": color(255, 215, 0), "Cancer": color(70, 130, 180), "Leo": color(255, 165, 0), "Virgo": color(46, 139, 87), "Libra": color(123, 104, 238), "Scorpio": color(148, 0, 211), "Sagittarius": color(255, 140, 0), "Capricorn": color(139, 69, 19), "Aquarius": color(0, 191, 255), "Pisces": color(72, 61, 139) }; return colors[sign] || color(240); } // Particle class for floating magic effect class Particle { constructor() { this.x = random(width); this.y = random(height); this.vx = random(-1, 1); this.vy = random(-1, 1); this.alpha = random(100, 255); } // Update particle movement update() { this.x += this.vx; this.y += this.vy; if (this.x > width || this.x < 0) this.vx *= -1; if (this.y > height || this.y < 0) this.vy *= -1; } // Display particle as a glowing dot show() { noStroke(); fill(255, this.alpha); ellipse(this.x, this.y, 5, 5); } }
let signs = [
  "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
  "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"
];

// Zodiac readings for each sign
let readings = {
  "Aries": ["Today is a day for bold moves.", "A new adventure awaits you.", "Your energy will attract opportunities."],
  "Taurus": ["Stay grounded, but take a leap of faith.", "Patience will bring unexpected rewards.", "A financial opportunity is coming your way."],
  "Gemini": ["A conversation will spark inspiration.", "Your curiosity leads to a surprising discovery.", "Adaptability is your greatest strength today."],
  "Cancer": ["Your emotions will guide you well.", "A nostalgic moment will bring clarity.", "Trust your intuition—it knows the way."],
  "Leo": ["Your confidence will open doors.", "A bold move will lead to admiration.", "Shine your light and others will follow."],
  "Virgo": ["Your keen eye will catch an important detail.", "Organization will bring unexpected rewards.", "A small habit change will lead to a breakthrough."],
  "Libra": ["Balance is key today.", "A relationship will deepen in an unexpected way.", "Harmony will find you when you least expect it."],
  "Scorpio": ["Mystery surrounds you—embrace it.", "Transformation is closer than you think.", "Your passion will lead you to new heights."],
  "Sagittarius": ["An exciting journey is on the horizon.", "Your optimism will inspire someone today.", "The universe is conspiring in your favor."],
  "Capricorn": ["Hard work pays off—stay focused.", "A disciplined approach will yield results.", "Your perseverance will be rewarded soon."],
  "Aquarius": ["Innovation is your ally today.", "A sudden insight will change your path.", "Your unique perspective is your greatest strength."],
  "Pisces": ["Your dreams hold important messages.", "Creativity will flow effortlessly.", "A moment of solitude will bring deep understanding."]
};

let dropdown, button, output;
let bgColor;
let particles = [];

function setup() {
  createCanvas(400, 300);
  textSize(16);
  textAlign(CENTER, CENTER);
  
  // Create dropdown menu for zodiac signs
  dropdown = createSelect();
  dropdown.position(100, 100);
  
  for (let sign of signs) {
    dropdown.option(sign);
  }
  
  // Create button to generate reading
  button = createButton("Get Your Reading");
  button.position(100, 140);
  button.mousePressed(generateReading);
  
  output = "Select your sign and receive your reading";
  bgColor = color(240);
  
  // Create floating particles for magical effect
  for (let i = 0; i < 50; i++) {
    particles.push(new Particle());
  }
}

function draw() {
  background(bgColor);
  fill(50);
  text("Psychic Zodiac Reading", width / 2, 50);
  text(output, width / 2, 80);
  
  // Update and show floating particles
  for (let p of particles) {
    p.update();
    p.show();
  }
}

// Generate random reading based on selected zodiac sign
function generateReading() {
  let selectedSign = dropdown.value();
  let possibleReadings = readings[selectedSign];
  output = possibleReadings[int(random(possibleReadings.length))];
  bgColor = getZodiacColor(selectedSign);
}

// Assign unique background color for each zodiac sign
function getZodiacColor(sign) {
  let colors = {
    "Aries": color(255, 99, 71), "Taurus": color(107, 142, 35), "Gemini": color(255, 215, 0), "Cancer": color(70, 130, 180),
    "Leo": color(255, 165, 0), "Virgo": color(46, 139, 87), "Libra": color(123, 104, 238), "Scorpio": color(148, 0, 211),
    "Sagittarius": color(255, 140, 0), "Capricorn": color(139, 69, 19), "Aquarius": color(0, 191, 255), "Pisces": color(72, 61, 139)
  };
  return colors[sign] || color(240);
}

// Particle class for floating magic effect
class Particle {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    this.vx = random(-1, 1);
    this.vy = random(-1, 1);
    this.alpha = random(100, 255);
  }

  // Update particle movement
  update() {
    this.x += this.vx;
    this.y += this.vy;
    if (this.x > width || this.x < 0) this.vx *= -1;
    if (this.y > height || this.y < 0) this.vy *= -1;
  }

  // Display particle as a glowing dot
  show() {
    noStroke();
    fill(255, this.alpha);
    ellipse(this.x, this.y, 5, 5);
  }
}

 

Object-oriented programming – Week 3

Reflection: Floating Bubbles

This artwork is inspired by the simple joy of blowing soap bubbles we used to do as kids. Just like real bubbles, the circles appear wherever you click, float around randomly, and slowly fade away or pop. I wanted each click to feel like blowing a new batch of bubbles, watching them drift and disappear. The way they change color and move in different directions makes them feel alive, conveying the playful nature of real soap bubbles.

Structure

The Circle class manages each individual bubble’s movement, opacity, and lifespan, ensuring that they appear and disappear naturally over time. Functions like setup(), draw(), and mousePressed() organize different parts of the code, keeping it easy to understand and modify.

Challenges

One issue was finding the right balance between movement and fading, so that that bubbles did not disappear too quickly while still feeling transient. Another challenge was making the interaction feel engaging, which I solved by adjusting the number of bubbles created per click and giving them different speeds and directions. Additionally, I had to optimize performance to prevent the sketch from slowing down over time, so circles are now removed once their lifespan ends.

Overall, I appreciate how this piece captures the lighthearted  beauty of soap bubbles in a digital form. To make it more realistic, I’d try to make the direction that bubbles take more random and add the effect of abrupt popping.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Array to store circles
let circles = [];
let circleSize = 50;
let numCirclesPerClick = 5; // Number of circles generated per mouse click
function setup() {
createCanvas(windowWidth, windowHeight);
noFill(); // No fill for circles, only stroke
}
function draw() {
background(20, 20, 30, 50);
// Loop through circles in reverse order
for (let i = circles.length - 1; i >= 0; i--) {
circles[i].update();
circles[i].display();
// Remove circle when its lifespan reaches zero
if (circles[i].lifespan <= 0) {
circles.splice(i, 1);
}
}
}
// Circle class (individual moving and fading circles)
class Circle {
constructor(x, y, size) {
this.x = x;
this.y = y;
this.vx = random(-2, 2); // Assign random speed in x direction
this.vy = random(-2, 2); // Assign random speed in y direction
this.baseSize = size;
this.size = size;
this.opacity = random(100, 200);
this.growthSpeed = random(0.5, 2);
this.color = color(random(255), random(255), random(255));
this.lifespan = 200; // Circle disappears after a set time
}
// Update circle properties (position, size, opacity, lifespan)
update() {
this.x += this.vx;
this.y += this.vy;
this.size = this.baseSize + sin(frameCount * 0.05) * 10; // Oscillating size effect
this.opacity -= 2;
this.lifespan -= 2;
}
// Display the objects
display() {
stroke(this.color);
strokeWeight(2);
fill(this.color.levels[0], this.color.levels[1], this.color.levels[2], this.opacity); // fading effect
ellipse(this.x, this.y, this.size, this.size);
}
}
// generates multiple circles at the mouse click location
function mousePressed() {
for (let i = 0; i < numCirclesPerClick; i++) {
circles.push(new Circle(mouseX, mouseY, circleSize));
}
}
// Array to store circles let circles = []; let circleSize = 50; let numCirclesPerClick = 5; // Number of circles generated per mouse click function setup() { createCanvas(windowWidth, windowHeight); noFill(); // No fill for circles, only stroke } function draw() { background(20, 20, 30, 50); // Loop through circles in reverse order for (let i = circles.length - 1; i >= 0; i--) { circles[i].update(); circles[i].display(); // Remove circle when its lifespan reaches zero if (circles[i].lifespan <= 0) { circles.splice(i, 1); } } } // Circle class (individual moving and fading circles) class Circle { constructor(x, y, size) { this.x = x; this.y = y; this.vx = random(-2, 2); // Assign random speed in x direction this.vy = random(-2, 2); // Assign random speed in y direction this.baseSize = size; this.size = size; this.opacity = random(100, 200); this.growthSpeed = random(0.5, 2); this.color = color(random(255), random(255), random(255)); this.lifespan = 200; // Circle disappears after a set time } // Update circle properties (position, size, opacity, lifespan) update() { this.x += this.vx; this.y += this.vy; this.size = this.baseSize + sin(frameCount * 0.05) * 10; // Oscillating size effect this.opacity -= 2; this.lifespan -= 2; } // Display the objects display() { stroke(this.color); strokeWeight(2); fill(this.color.levels[0], this.color.levels[1], this.color.levels[2], this.opacity); // fading effect ellipse(this.x, this.y, this.size, this.size); } } // generates multiple circles at the mouse click location function mousePressed() { for (let i = 0; i < numCirclesPerClick; i++) { circles.push(new Circle(mouseX, mouseY, circleSize)); } }
// Array to store circles
let circles = [];
let circleSize = 50;
let numCirclesPerClick = 5; // Number of circles generated per mouse click

function setup() {
  createCanvas(windowWidth, windowHeight);
  noFill(); // No fill for circles, only stroke
}

function draw() {
  background(20, 20, 30, 50); 
  
  // Loop through circles in reverse order 
  for (let i = circles.length - 1; i >= 0; i--) {
    circles[i].update(); 
    circles[i].display(); 
    
    // Remove circle when its lifespan reaches zero
    if (circles[i].lifespan <= 0) {
      circles.splice(i, 1);
    }
  }
}

// Circle class (individual moving and fading circles)
class Circle {
  constructor(x, y, size) {
    this.x = x;
    this.y = y;
    this.vx = random(-2, 2); // Assign random speed in x direction
    this.vy = random(-2, 2); // Assign random speed in y direction
    this.baseSize = size;
    this.size = size;
    this.opacity = random(100, 200); 
    this.growthSpeed = random(0.5, 2); 
    this.color = color(random(255), random(255), random(255)); 
    this.lifespan = 200; // Circle disappears after a set time
  }

  // Update circle properties (position, size, opacity, lifespan)
  update() {
    this.x += this.vx; 
    this.y += this.vy; 
    this.size = this.baseSize + sin(frameCount * 0.05) * 10; // Oscillating size effect
    this.opacity -= 2; 
    this.lifespan -= 2; 
  }

  // Display the objects
  display() {
    stroke(this.color);
    strokeWeight(2);
    fill(this.color.levels[0], this.color.levels[1], this.color.levels[2], this.opacity); // fading effect
    ellipse(this.x, this.y, this.size, this.size);
  }
}

// generates multiple circles at the mouse click location
function mousePressed() {
  for (let i = 0; i < numCirclesPerClick; i++) {
    circles.push(new Circle(mouseX, mouseY, circleSize));
  }
}

 

Reading Response – Week 3

I believe that interactivity is like a conversation. It’s alive, reciprocal, and unpredictable. It thrives on an ongoing exchange where both parties listen, process, exchange ideas. Yet sometimes, we tend to mistake reaction for interaction. Imagine a fridge light turning on when you open the door – that’s a reaction. In this context, a true interaction requires intention, curiosity, and a sense of the unexpected.

When designing for interactivity, I want to create projects that thrive on engagement, not just response. In my p5.js sketches, I want to move beyond the input-output relationships and create something that listens and adapts to what the audience feeds to the “artwork”. This may mean making the visuals morph in response to prolonged interaction or rewarding users for exploration rather than just reacting to a single click. In any case, I want each person who interacts with my projects to have a unique experience.

To sum up, I think that a truly interactive system should feel like a dynamic exchange, where the user isn’t just playing with the system but within it. That’s the kind of interactivity I want to create—something more like a meaningful conversation and less like a fridge light turning on and off.

Reading Reflection – Week 2

The first thing about Casey’s presentation that made me challenge my beliefs is that digital art is not real art. I used to think that things that are ‘generated’ are not authentic enough to be considered a unique piece of art. Yet, seeing how much thought and structure it takes to create an abstract pattern with the help of algorithms made me reevaluate my views. It’s fascinating how a series of flat geometric shapes drawn over and over again can create such a lively picture.  This idea is reflected in the quote by Michael Noll, “The computer is a unique device for the arts, since it can function solely as an obedient tool with vast capabilities for controlling complicated and involved processes. But then again, full exploitation of those unique talents for controlled randomness and detailed algorithms can result in an entirely new medium—a creative, artistic medium.” In my future works, I’d like to work more on thinking creatively within a limited range of functions I know.

I believe that uncontrolled randomness leads to complete chaos, while good digital artwork takes a little bit of randomness and a lot of decision-making. As humans, we are set to think in patters, thus it can be quite challenging to create something inherently random, something that has no beginning or end, or doesn’t follow standard rules of composition. I like how Casey uses randomness as a jumping-off point for his artworks such as rolling a dice to trigger the final composition, which is built based on a series of carefully designed algorithms.  

Looking at Casey’s journey, I admire how meticulous he is about building an algorithm and exploring it to the fullest before moving onto a new concept. It seems like he won’t leave his idea until he has tried visualizing it in all ways possible. This reminds me of how famous artists such as Picasso, who went though different ‘eras’ in his art, ended up creating a series of artworks known as The Blue Period. 

My final takeaway from this talk is that true randomness can not be generated by a computer – it has to be “borrowed” from nature, like the patters that bacteria create while reacting to sunlight, where computer serves as a tool to make randomness make sense.