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 ); }