yuvian-Body

flappy face

note: this is not working 100% smoothly yet (collision detection is buggy sometimes)

For this project, I worked with clmtrackr.js in order to take input from the user's face to control a game. More specifically, I took note of where the user's face was positioned within the frame and the width of their smile. The more the user smiles, the higher the red "bird" flies. Moving his/her face from side to side also changes the bird's x-position on the canvas. I developed this project by getting clmtrackr to work first, then pulling data from the array of points it had given me, and then styling and implementing the game aspect. I didn't want to work from a motion capture because I wanted it to be more interactive, which is why I chose to get the user's face from his/her webcam. But with this also comes the potential for buggy face tracking as I have found while testing the project. I chose a flappy bird-esque game to implement because I wanted the game to be simple, easily understood, and somewhat recognizable. I had made another duplicate side-project in which the "bird" goes around eating berries but it seemed less like a game and more like a task.

how to play:
- smile widely to move bird up
- move head side to side to move bird left and right
- avoid white blocks

smile

open mouth

var ctracker;
var video;
var positions;
var bird;
var blockSpeed = 5
var blocks = [];
var score = 0;
var startGame = false;

function setup() {

  // setup webcam video capture
  video = createCapture(VIDEO);
  video.size(600, 450);
  video.position(0, 0);

  //hide video feed
  video.hide();

  // setup canvas
  createCanvas(600, 450);

  // setup tracker
  ctracker = new clm.tracker();
  ctracker.init(pModel);
  ctracker.start(video.elt);

  noStroke();

  // add block to blocks array
  for (var i = 0; i < 1; i++) {
    blocks[i] = new Block(random(40, 100), random(50, 100))
  }

}

function draw() {
  
  background(0);
  
  push();
  translate(video.width, 0);
  scale(-1, 1);

  image(video, 0, 0, width, height)

  positions = ctracker.getCurrentPosition();
  for (var i = 0; i < positions.length; i++) {
    fill(255, 80);
    // for when clmtracker is being weird in p5js editor
    // ellipse(positions[i][0] - 20, positions[i][1] - 10, 4, 4);
    // for online p5js editor
    ellipse(positions[i][0], positions[i][1], 2.5, 2.5);
  }
  pop();
  
  if (!startGame) {
    textAlign(CENTER)
    fill(255);
    text("press the space bar to start", width/2, height/2);
  }

  // if face is found, commence the game
  if (positions.length > 0 && startGame) {
    playGame()
  }
}

function keyPressed() {
  if (key == " ") {
    startGame = true
  } 
  else if (key == "R") {
    print('restart')
    setup();
  }
}


function playGame() {
    // blocks
    for (var i = 0; i < blocks.length; i++) {
      blocks[i].move();
      blocks[i].display();

      // remove block when it goes off screen and add a new block
      if (blocks[i].x < 25) {
        score += 1;
        blocks = []
        blocks.push(new Block(random(40, 100), random(50, 300)))
        // gradually increase speed of the moving blocks
        if (blockSpeed < 13) {
          blockSpeed = blockSpeed * 1.05
        } else {
          blockSpeed = blockSpeed * 1.005
        }
      }
  
      push();
        // display score
        fill(0);
        rectMode(CENTER);
        rect(80, 47, 70, 25)
        fill(255);
        textAlign(CENTER);
        text('score: ' + score, 80, 50);
      pop();
    }

    // background of smile bar
    fill(255);
    rect(20, 20, 15, height - 40);

    // smile bar
    var mouthLeft = createVector(positions[44][0], positions[44][1]);
    var mouthRight = createVector(positions[50][0], positions[50][1]);
    var smileDistance = Math.round(mouthLeft.dist(mouthRight));

    smile = map(smileDistance, 60, 75, 17, 0); // mapped smile distance to chopped up parts so bird doesn't jitter as much
    
    // prevent smile bar from going beyond bottom of bar
    rectSmile = smile * 20
    rectSmile = constrain(rectSmile, 20, height - 20)
    fill(50);
    rect(20, 20, 15, rectSmile);

    // smile number next to smile bar
    fill(255);
    smileText = height - 40 - Math.round(smile)
    // text(smileText, 50, constrain(smile, 30, height - 20))

    // bird
    push();
    translate(video.width, 0);
    scale(-1, 1);
    var noseX = positions[37][0]; // get nose x position to determine center of face
    noseX = noseX - 25 // wack p5js editor positioning
    birdY = smile * 20
    birdY = constrain(smile * 20, 50, height - 50); //prevent bird from moving offscreen
    bird = new Bird(noseX, birdY); // bird's x is nose position, y depends on width of smile
    bird.display();
    pop();
    
    // if bird collides with block, freeze everything and display game over message
    // for (var i = 0; i < blocks.length; i++) {
    block = blocks[0]
    if (bird.x >= block.x  && bird.x <= block.x + block.width) {
      bottomHeight = height - block.opening - block.height
      if (bird.y <= block.height || bird.y >= bottomHeight) {
        print('collision')
        // freeze webcam video, blocks, and bird
        video.stop()
        block.speed = 0
        // bird.y = height/2
        gameOver();
      }
    }
    
    // check top collision,
    // count = 0
    // d = getDistance(block.x, block.height, bird.x, bird.y)
    // if ( d < block.width/2) {
    //   count += 1
    //   print('hit' +  count)
    // }
  
    
    // alert user to fix positioning if bird is too high or too low
    if (smileText < 0) {
      fill(226, 74, 92);
      strokeWeight(3);
      text('move closer', 80, 100);
    }
    else if (smileText > 900) {
      fill(226, 74, 92);
      strokeWeight(3);
      text('move farther away', 80, 100);
    }
}

function gameOver() {
  fill(255);
  text("GAME OVER", width/2, height/2)
}

function Bird(x, y) {
  this.x = x;
  this.y = y;

  this.display = function() {
    fill(226, 74, 92);
    ellipse(this.x, this.y, 25, 25);
  }
}

function Block(w, h) {
  this.x = width - w / 2;
  this.width = w;
  this.height = h;
  this.speed = blockSpeed;
  this.opening = random(90, height / 2 - 50);

  this.display = function() {
    fill(255);
    // top block
    rect(this.x, 0, this.width, this.height)
      // bottom block
    rect(this.x, this.height + this.opening, this.width, height - this.opening - this.height)
  }

  this.move = function() {
    this.x = this.x - this.speed
  }
}

// from https://gist.github.com/timohausmann/5003280
function getDistance(x1,y1,x2,y2) {
  var 	xs = x2 - x1,
	ys = y2 - y1;		
	xs *= xs;
	ys *= ys;
	return Math.sqrt( xs + ys );
}