// Mood Mirror p5 Code
// File name : sketch.js
// Authors : Brendan Lilly & Brandon Scott
// Brendan's API Credentials
// var client_id = 'ecca4367ce9547a19d8685c6f74ab73b';
// var app_key = 'e8eebbb1ed6e4205bf9ac81516c5ed7f';
// Brandon's API Credentials
var client_id = '3aa495d31d5b46538749947312678261';
var app_key = 'ec13aab4b301449abbdc957c6aeb1f31';
// These arrays hold the returned x,y face positions and dimensions
var facex = [], facey = [], facew = [], faceh = [];
// These arrays hold the determined colors & masks for each person's mood
var color = [], mask = [] , gif = [];
// These are 2D arrays that will hold the values of a person's mood history
var anger = [], happy = [], surprise = [], fear = [], sad = [], disgust = [];
// These variables hold the references to the image objects for the masks
var angerMask, happyMask, surpriseMask, fearMask, sadMask, disgustMask;
//These variables hold the references to the gif objects for the masks
var angerGif , happyGif;
// These arrays hold all of the currently drawn rings & masks
var moodrings = [], moodmasks = [] , moodgifs = [];
// Variables to map the webcam photo -> monitor resolution
var ratiow = 1, ratioh = 1;
// Flag to freeze the canvas (and the API calls)
var frozen = false;
// Counter for # of people currently in frame
var numOfPersons = 0;
/**
Handles the 'f' key to freeze the canvas.
*/
function keyTyped() {
// Toggle frozen mode
if (key == 'f') {
frozen = !frozen;
console.log("[DEBUG] frozen: " + frozen);
}
}
/**
Runs on startup, setting the framerate, creating the canvas, and initializing
the webcam capture.
*/
function setup() {
// Load images for the masks
angerMask = loadImage("assets/masks-01.png");
happyMask = loadImage("assets/masks-02.png");
surpriseMask = loadImage("assets/masks-03.png");
disgustMask = loadImage("assets/masks-04.png");
sadMask = loadImage("assets/masks-05.png");
fearMask = loadImage("assets/masks-06.png");
// Load the gifs
/*
angerGif = loadGif("assets/gif-01.gif");
happyGif = loadGif("assets/gif-02.gif");
*/
for (i = 0; i < 5; i++) {
// Create arrays within each element, making each emotion a 2D array
// The first index is associated with one person, and the second
// index is associated with the last 10 calls of the API
anger[i] = new Array(10);
anger[i].fill(0);
happy[i] = new Array(10);
happy[i].fill(0);
surprise[i] = new Array(10);
surprise[i].fill(0);
fear[i] = new Array(10);
fear[i].fill(0);
sad[i] = new Array(10);
sad[i].fill(0);
disgust[i] = new Array(10);
disgust[i].fill(0);
}
// Force the page to use https
forceHttps();
// Set the framerate to 60
var fr = 60;
frameRate(fr);
//Creates a canvas that is the width and height of the window
createCanvas(windowWidth, windowHeight);
// Set ratiow & ratioh
ratiow = width / 640;
ratioh = height / 480;
console.log("DEBUG: frameRate = " + fr);
console.log("DEBUG: windowWidth = " + windowWidth);
console.log("DEBUG: windowHeight = " + windowHeight);
console.log("DEBUG: ratiow = " + ratiow);
console.log("DEBUG: ratioh = " + ratioh);
//Initialize the Webcam capture
startCapture();
}
/**
Called whenever the page is resized, adjusts canvas to the new windowWidth and
windowHeight, and calculates the scale variables (ratiow, ratioh).
*/
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
ratiow = windowWidth / 640;
ratioh = windowHeight / 480;
console.log("DEBUG: windowWidth = " + windowWidth);
console.log("DEBUG: windowHeight = " + windowHeight);
console.log("DEBUG: ratiow = " + ratiow);
console.log("DEBUG: ratioh = " + ratioh);
}
/**
Handles the graphic generation, drawing rings or the masks to the canvas when
people are detected by the FACE API.
*/
function draw() {
if (frozen) {
return;
}
background(0);
if (numOfPersons > 0) {
// Framerate starts to drop if we don't have a bound on the number of
// rings drawn on the screen
var maxRingCount = 80;
if (numOfPersons == 1) {
maxRingCount = 60;
} else if (numOfPersons == 2) {
maxRingCount = 80;
} else {
maxRingCount = 90;
}
// Draw moodrings for each person on screen, and maybe, a mask
for (i = 0; i < numOfPersons; i++) {
if (moodrings.length < maxRingCount && maybe(maxRingCount/numOfPersons)) {
moodrings.push(new MoodRing((width - (facex[i] * ratiow)), facey[i] * ratioh, facew[i] * ratiow, faceh[i] * ratioh, color[i]));
}
if (moodmasks.length < 1 && maybe(3)) {
moodmasks.push(new MoodMask((width - (facex[i] * ratiow)), facey[i] * ratioh, facew[i] * ratiow, faceh[i] * ratioh, mask[i], random(45, 75)));
//If the mask being added is happ or angry also add a gif
if(mask[i] == happyMask || mask[i] == angerMask)
{
//Push the gif
// moodgifs.push(new MoodGif(facew[i] * ratiow, faceh[i] * ratioh, gif[i], random(45, 75)));
}
}
}
}
// Check to see if any rings need to be removed
for (i = 0; i < moodrings.length; i++) {
// Check to see if any of the rings are hitting the edge of the screen
// This is calculated by adding or subtracting the radius of the
// circle (of which the arc is based from) from the width/height of the
// canvas.
if (moodrings[i].y + (moodrings[i].h / 2) > height) {
console.log("moodrings[" + i + "] hit the bottom edge, removing it");
moodrings.splice(i, 1);
} else if (moodrings[i].x + (moodrings[i].w / 2) > width) {
console.log("moodrings[" + i + "] hit the right edge, removing it");
moodrings.splice(i, 1);
} else if (moodrings[i].y - (moodrings[i].h / 2) < 0) {
console.log("moodrings[" + i + "] hit the top edge, removing it");
moodrings.splice(i, 1);
} else if (moodrings[i].x - (moodrings[i].w / 2) < 0) {
console.log("moodrings[" + i + "] hit the left edge, removing it");
moodrings.splice(i, 1);
} else if (moodrings[i].framecount > 150) {
// Prevents random rings from burning into the screen
// (seems to be a bug of the FACE API to randomly return a face)
console.log("moodrings[" + i + "] hit 150+ framecount, removing it");
moodrings.splice(i, 1);
}
}
// Check to see if any masks need to be removed
for (i = 0; i < moodmasks.length; i++) {
if (moodmasks[i].framecount > moodmasks[i].maxFramecount) {
console.log("moodmasks[" + i + "] hit maxFramecount, removing it");
moodmasks.splice(i, 1);
}
}
// Check to see if any gifs need to be removed
for (i = 0; i < moodgifs.length; i++) {
if (moodgifs[i].framecount > moodgifs[i].maxFramecount) {
console.log("moodgifs[" + i + "] hit maxFramecount, removing it");
moodgifs.splice(i, 1);
}
}
// Draw all of the moodrings
for (i = 0; i < moodrings.length; i++) {
moodrings[i].drawArcs();
}
// Draw all of the moodmasks
for (i = 0; i < moodmasks.length; i++) {
moodmasks[i].drawMask();
}
// Draw all of the moodgifs
for (i = 0; i < moodgifs.length; i++) {
moodgifs[i].drawGif();
}
}
/**
Returns true or false randomly depending on the threshold that is passed in.
@param {int} threshold The % threshold to be used for returning true.
*/
function maybe(threshold) {
var chance = random(0, 100);
if (chance < threshold) {
return true;
} else {
return false;
}
}
/**
Creates a new MoodMask object, which is a mask image relating to the person's
mood, plotted from an initial x,y position, width, height, drawn until it hits
a maxFramecount.
@param {int} x The starting x position of the mask.
@param {int} y The starting y position of the mask.
@param {int} w The starting width of the mask.
@param {int} h The starting height of the mask.
@param {img} img The image of the mask.
@param {int} maxFramecount The number of frames the mask will be drawn for.
*/
function MoodMask(x, y, w, h, img, maxFramecount) {
this.x = x;
this.y = y;
this.initialw = w;
this.initialh = h;
this.w = w;
this.h = h;
this.img = img;
this.maxFramecount = maxFramecount;
this.framecount = 0;
this.tint = 100;
this.drawMask = function() {
// Increase opacity for the first 50% of the frames
if (this.framecount < (this.maxFramecount / 2) && this.tint < 255) {
this.tint += 10;
} else if (this.framecount > (this.maxFramecount / 2)) {
// Decrease opacity for the last 50% of the frames
this.tint -= 10;
}
// Set the new opacity by calling tint()
tint(255, this.tint);
// image() call plots from the top left corner, so we need to shift
// the mask by half of its width & half of its height
image(img, 0, 0, img.width, img.height, this.x - this.w/2, this.y - this.h/2, this.w, this.h);
// Increase width & height of the mask by 1%
if (this.framecount < (this.maxFramecount / 2)) {
this.w += Math.round(this.w * .01);
this.h += Math.round(this.h * .01);
}
// Increase the framecount
this.framecount += 1;
}
}
/**
Creates a new MoodGif object, which is a gif image relating to the person's
mood, plotted from an initial x,y position, width, height, drawn until it hits
a maxFramecount.
@param {int} w The starting width of the mask.
@param {int} h The starting height of the mask.
@param {gif} gif The gif animation.
@param {int} maxFramecount The number of frames the mask will be drawn for.
*/
function MoodGif(w, h, gif, maxFramecount) {
this.initialw = w;
this.initialh = h;
this.w = w;
this.h = h;
this.gif = gif;
this.maxFramecount = maxFramecount;
this.framecount = 0;
this.tint = 100;
this.drawGif = function() {
// Increase opacity for the first 50% of the frames
if (this.framecount < (this.maxFramecount / 2) && this.tint < 255) {
this.tint += 10;
} else if (this.framecount > (this.maxFramecount / 2)) {
// Decrease opacity for the last 50% of the frames
this.tint -= 10;
}
// Set the new opacity by calling tint()
tint(255, this.tint);
// If the emotion is anger postion the flames at the bottom center of the screen
if(gif == angerGif)
{
image(gif,(windowWidth/2)-(this.w/2),(windowHeight-this.h),this.w,this.h);
}
//If the emotion is happy position the sun at the top left of the screen
if(gif == happyGif)
{
image(gif, 0, 0, this.w , this.h);
}
// Increase the framecount
this.framecount += 1;
}
}
/**
Creates a new MoodRing object, which is a group of 4 arcs of random lengths,
plotted from an initial x,y position, width, height, and color from the FACE
API.
@param {int} x The starting x position of the ring.
@param {int} y The starting y position of the ring.
@param {int} w The starting width of the ring.
@param {int} h The starting height of the ring.
@param {string} color The starting color of the ring.
*/
function MoodRing(x, y, w, h, color) {
this.x = x;
this.y = y;
this.initialw = w;
this.initialh = h;
this.w = w;
this.h = h;
this.color = color;
this.framecount = 0;
// start/end angles for arcs of each quadrant (top right, top left, etc)
this.startTR = Math.round(random(270, 360));
this.endTR = Math.round(random(this.startTR, 360));
this.startTL = Math.round(random(180, 270));
this.endTL = Math.round(random(this.startTL, 270));
this.startBL = Math.round(random(90, 180));
this.endBL = Math.round(random(this.startBL, 180));
this.startBR = Math.round(random(0, 90));
this.endBR = Math.round(random(this.startBR, 90));
this.drawArcs = function() {
stroke(this.color);
noFill();
strokeWeight(1);
// Top right quadrant (270˚ -> 0˚) arc
arc(this.x, this.y, this.w, this.h, radians(this.startTR), radians(this.endTR));
// Top left quadrant (180˚ -> 270˚) arc
arc(this.x, this.y, this.w, this.h, radians(this.startTL), radians(this.endTL));
// Bottom left quadrant (90˚ -> 180˚) arc
arc(this.x, this.y, this.w, this.h, radians(this.startBL), radians(this.endBL));
// Bottom right quadrant (0˚ -> 90˚) arc
arc(this.x, this.y, this.w, this.h, radians(this.startBR), radians(this.endBR));
// Increase width & height of the ring by 1%
this.w += Math.round(this.w * .01);
this.h += Math.round(this.h * .01);
// Increase the framecount
this.framecount += 1;
}
}
/**
Parses the JSON response returned from the FACE API.
@param {JSON} result The response from the FACE API.
*/
function parseResponse(result) {
// Grab the number of people detected
numOfPersons = result.persons.length;
//If at least 1 person is detected by the webcam
if (result.persons.length > 0) {
for (i = 0; i < numOfPersons; i ++) {
// Remove the oldest value from the mood history arrays with shift()
anger[i].shift();
happy[i].shift();
surprise[i].shift();
fear[i].shift();
sad[i].shift();
disgust[i].shift();
// Push the newest value for the mood history arrays
anger[i].push(result.persons[i].expressions.anger.value);
happy[i].push(result.persons[i].expressions.happiness.value);
surprise[i].push(result.persons[i].expressions.surprise.value);
fear[i].push(result.persons[i].expressions.fear.value);
sad[i].push(result.persons[i].expressions.sadness.value);
disgust[i].push(result.persons[i].expressions.disgust.value);
// Find the averages of all the emotions
var angerAvg = 0, happyAvg = 0, surpriseAvg = 0;
var fearAvg = 0, sadAvg = 0, disgustAvg = 0;
// i index is the current person
// j index is 1 of 10 recorded mood values that we are summing
for (j = 0; j < anger[i].length; j++) {
angerAvg += anger[i][j];
happyAvg += happy[i][j];
surpriseAvg += surprise[i][j];
fearAvg += fear[i][j];
sadAvg += sad[i][j];
disgustAvg += disgust[i][j];
}
angerAvg /= anger[i].length;
happyAvg /= happy[i].length;
surpriseAvg /= surprise[i].length;
fearAvg /= fear[i].length;
sadAvg /= sad[i].length;
disgustAvg /= disgust[i].length;
// Find the max value of all the averages
var maxMood = Math.max(angerAvg, happyAvg, surpriseAvg, fearAvg, sadAvg, disgustAvg);
// Assign the color & mask type appropriate for the mood
if (maxMood == angerAvg) {
color[i] = "red";
mask[i] = angerMask;
gif[i] = angerGif;
} else if (maxMood == happyAvg) {
color[i] = "yellow";
mask[i] = happyMask;
gif[i] = happyGif;
} else if (maxMood == surpriseAvg) {
color[i] = "orange";
mask [i] = surpriseMask;
} else if (maxMood == fearAvg) {
color[i] = "purple";
mask [i] = fearMask;
} else if (maxMood == sadAvg) {
color[i] = "blue";
mask [i] = sadMask;
} else if (maxMood == disgustAvg) {
color[i] = "green";
mask [i] = disgustMask;
}
// Grab the x,y positions of the face, along with the width + height
facex[i] = result.persons[i].face.x;
facey[i] = result.persons[i].face.y;
facew[i] = result.persons[i].face.w;
faceh[i] = result.persons[i].face.h;
}
}
}
/**
Logs the error to the console when the FACE.sendImage() call fails.
@param error The error response from the sendImage() request.
*/
function failure( error ) {
//Pops an alert with the error to the screen
console.log(error);
}
/**
Sends the request to the FACE API, calling parseResponse() on success, or
failure() on failure.
*/
function sendDetectRequest() {
//Verifies that the image has been taken
var img = document.querySelector( "#img_snapshot" );
if( img.naturalWidth == 0 || img.naturalHeight == 0 )
return;
//Converts the image to a Blob
var imgBlob = FACE.util.dataURItoBlob( img.src );
//Send the image Blob , app_ key , client_id to the FACE API and get the expressions result
FACE.sendImage( imgBlob, parseResponse, failure, app_key, client_id, 'expressions, landmarks, face' );
}
/**
Initializes the webcam capture, taking a snapshot and sending the request every
1.1 seconds.
*/
function startCapture() {
//Start the webcam preview
FACE.webcam.startPlaying( "webcam_preview" );
//Takes a photo and calls the sendDetectRequest() function
//Then waits 2.1 seconds in between calls
setInterval( function()
{
// Don't bother sending API calls if the canvas is frozen
if (!frozen) {
FACE.webcam.takePicture( "webcam_preview", "img_snapshot" );
sendDetectRequest();
}
},
1100 );
}
/**
Redirects the page to use https protocol if it is not.
*/
function forceHttps(){
if (window.location.protocol != "https:"){
window.location.href = "https:" + window.location.href.substring(window.location.protocol.length);
}
}