Source: sketch.js

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