Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Finish Off a Flickr-Based Pairs Game With JavaScript

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

In this tutorial, we'll take a basic browser game (which we built in a Tuts+ Premium tutorial), and add progress bars, a preloader, a splash screen, and a lot more polish.


Introduction

In this Tuts+ Premium tutorial, we built a basic card matching game with JavaScript, whose images came from Flickr. Check out the demo:



Click to try the game as it is now.

In this tutorial, we'll add a lot of polish to the game, by implementing a preloader and progress bar, a splash screen, and a keyword search. Take a look at how the game will turn out:



Click to try the game with the improvements we'll be adding.

In this tutorial, you'll learn the JavaScript and HTML necessary to code all these improvements. Download the source files and extract the folder called StartHere; this contains all the code from the end of the Premium tutorial.

In flickrgame.js there is a function called preloadImage(), which contains this line:

tempImage.src = flickrGame.tempImages[i].imageUrl;

For the purposes of testing, change it to:

tempImage.src = "cardFront.jpg";

This will show the images on the cards all the time, which makes testing a lot easier. You can change this back at any time.

Now, read on!


Step 1: addKeyPress()

Right now we have the tag "dog" hard coded, but the game will get boring quickly if we force the user to use dog photos all the time!

The search input has been sitting there looking pretty, but being totally non-functional all this time. Let's fix that. We will listen for the user to hit the Enter key and then call the doSearch() method using whatever they typed in to call the Flickr API.

Add the following beneath the resetImages() function, in flickrgame.js.

function addKeyPress() {
	$(document).on("keypress", function (e) {
		if (e.keyCode == 13) {
		  doSearch();
		}
	});
}

Here we listen for a keypress and if the keyCode is equal to 13, we know they pressed the Enter key so we call the doSearch() function.

We need to modify the doSearch function to use the text in the input, so make the following changes:

function doSearch() {
	if ($("#searchterm").val() != "") {
		$(document).off("keypress");
		var searchURL = "http://api.flickr.com/services/rest/?method=flickr.photos.search";
		searchURL += "&api_key=" + flickrGame.APIKEY;
		searchURL += "&tags=" + $("#searchterm").val();
		searchURL += "&per_page=36"
		searchURL += "&license=5,7";
		searchURL += "&format=json";
		searchURL += "&jsoncallback=?";
		$.getJSON(searchURL, setImages);
	}
}

Here, we first check that the input is not empty (we don't want to search for nothing), then we remove the keypress listener. Finally, we alter the tags to use the input's value.

The last thing we need to do is remove the call to doSearch() in the JS file. Find where we are manually calling doSearch() and remove it. (It's right after the addKeyPress() function.)

Dont forget to actually call the addKeyPress() function. I called it right beneath where I declared it.

function addKeyPress() {
	$(document).on("keypress", function (e) {
		if (e.keyCode == 13) {
		  doSearch();
		}
	});
}
addKeyPress();

Now if you test the game you wont see any images until you do a search.


Step 2: Contacting Server

When we make our first call to Flickr's API there is a slight delay. We will show an animated GIF (a "throbber") while we contact the server, and remove it once the call comes back.

Add the following to the doSearch() function.

function doSearch() {
    if ($("#searchterm").val() != "") {
        $(document).off("keypress");
        $("#infoprogress").css({
            'visibility': 'visible'
        });
        var searchURL = "http://api.flickr.com/services/rest/?method=flickr.photos.search";
        searchURL += "&api_key=" + flickrGame.APIKEY;
        searchURL += "&tags=" + $("#searchterm").val();
        searchURL += "&per_page=36"
        searchURL += "&license=5,7";
        searchURL += "&format=json";
        searchURL += "&jsoncallback=?";
        $.getJSON(searchURL, setImages);
    }
}

This sets the #infoprogress div to be visible. Once the information comes back from Flickr, we will hide it. To do so, add the following code to the setImages() function:

function setImages(data) {
	$("#infoprogress").css({
		'visibility': 'hidden'
	});
	$.each(data.photos.photo, function (i, item) {
		var imageURL = 'http://farm' + item.farm + '.static.flickr.com/' + item.server + '/' + item.id + '_' + item.secret + '_' + 'q.jpg';
		flickrGame.imageArray.push({
				imageUrl: imageURL,
				photoid: item.id
		});
	});
	
	infoLoaded();
}

If you test the game now, you should see the loader image show while contacting the Flickr API.


Step 3: Get Photo Info

We need to get the information for each photo we use. We will call the method=flickr.photos.getInfo on each photo, and then call the infoLoaded() function every time the information is loaded. Once the information for every photo has loaded, the game continues as before.

There is a lot of new information to take in here, so we will break it down step by step. First, add the following to the setImages() function:

function setImages(data) {
    $("#infoprogress").css({
        'visibility': 'hidden'
    });
    if (data.photos.photo.length >= 12) {
        $("#searchdiv").css({
            'visibility': 'hidden'
        });
        $.each(data.photos.photo, function (i, item) {
            var imageURL = 'http://farm' + item.farm + '.static.flickr.com/' + item.server + '/' + item.id + '_' + item.secret + '_' + 'q.jpg';
            flickrGame.imageArray.push({
                imageUrl: imageURL,
                photoid: item.id
            });
            var getPhotoInfoURL = "http://api.flickr.com/services/rest/?method=flickr.photos.getInfo";
            getPhotoInfoURL += "&api_key=" + flickrGame.APIKEY;
            getPhotoInfoURL += "&photo_id=" + item.id;
            getPhotoInfoURL += "&format=json";
            getPhotoInfoURL += "&jsoncallback=?";
            $.getJSON(getPhotoInfoURL, infoLoaded);
        });
    } else {
        alert("NOT ENOUGH IMAGES WERE RETURNED");
        addKeyPress();
    }
    flickrGame.numberPhotosReturned = flickrGame.imageArray.length;
}

Now that we are getting the tags from the user we should make sure enough images were returned to make up a single game (12). If so, we hide the input so the player can't do another search mid-game. We set a variable getPhotoInfoURL and use the method=flickr.photos.getInfo - notice that we are using the item.id for the photo_id. We then use the jQuery's getJSON method, and call infoLoaded.

If not enough images were returned, we throw up an alert and call addKeyPress() so the user can do another search.

So we need to know how many images were returned from the call to the Flickr API, and we store this in the variable numberPhotosReturned, which we add to our flickrGame object:

var flickrGame = {
	APIKEY: "76656089429ab3a6b97d7c899ece839d",
	imageArray: [],
	tempImages:[],
	theImages: [],
	chosenCards: [],
	numberPhotosReturned: 0
}

(Make sure you add a comma after chosenCards: [].)

We cannot test just yet; if we did we would be calling preloadImages() 36 times in a row since that is all our infoLoaded() function does at the moment. Definitely not what we want. In the next step we will flesh out the infoLoaded() function.


Step 4: infoLoaded()

The infoLoaded() function receives information about a single photo. It adds the information to the imageArray for the proper photo, and keeps track of how many photos' info have been loaded; if this number is equal to numberPhotosReturned, it calls preloadImages().

Delete the call to preloadImages() and put the following inside the infoLoaded() function:

	flickrGame.imageNum += 1;
	var index = 0;
	for (var i = 0; i < flickrGame.imageArray.length; i++) {
		if (flickrGame.imageArray[i].photoid == data.photo.id) {
			index = i;
			flickrGame.imageArray[index].username = data.photo.owner.username;
			flickrGame.imageArray[index].photoURL = data.photo.urls.url[0]._content;
		}
	}
	if (flickrGame.imageNum == flickrGame.numberPhotosReturned) {
		preloadImages();
	}
}

Here we increment the imageNum variable and set a variable index equal to 0. Inside the for loop we check to see if the photoid in the imageArray is equal to the data.photo.id (remember the data is a JSON representation of the current image being processed). If they do match we set index equal to i and update the appropriate index in the imageArray with a username and photoURL variable. We'll need this information when we show the image attributions later.

This might seem a bit confusing, but all we are doing is matching up the photos. Since we don't know the order in which they will be returned from the server we make sure their id's match, and then we can add the username and photoURL variables to the photo.

Lastly, we check if imageNum is equal to the numberPhotosReturned, and if it is then all images have been processed so we call preloadImages().

Don't forget to add the imageNum to the flickrGame object.

var flickrGame = {
	APIKEY: "76656089429ab3a6b97d7c899ece839d",
	imageArray: [],
	tempImages:[],
	theImages: [],
	chosenCards: [],
	numberPhotosReturned: 0,
	imageNum: 0
}

(Make sure you add a comma after the numberPhotosReturned: 0.)

If you test now it will take a little longer for you to see the photos. On top of calling the Flickr API to retrieve the photos, we are now getting information about each one of them.


Step 5: Progress Bar for Photo Info

In this step we will get the progress bar showing when we load the photo information.

Add the following code to the setImages() function:

function setImages(data) {
    $("#infoprogress").css({
        'visibility': 'hidden'
    });
    $("#progressdiv").css({
        'visibility': 'visible'
    });
    $("#progressdiv p").text("Loading Photo Information");
    if (data.photos.photo.length >= 12) {
        $("#searchdiv").css({
            'visibility': 'hidden'
        });
        $.each(data.photos.photo, function (i, item) {
            var imageURL = 'http://farm' + item.farm + '.static.flickr.com/' + item.server + '/' + item.id + '_' + item.secret + '_' + 'q.jpg';
            flickrGame.imageArray.push({
                imageUrl: imageURL,
                photoid: item.id
            });
            var getPhotoInfoURL = "http://api.flickr.com/services/rest/?method=flickr.photos.getInfo";
            getPhotoInfoURL += "&api_key=" + flickrGame.APIKEY;
            getPhotoInfoURL += "&photo_id=" + item.id;
            getPhotoInfoURL += "&format=json";
            getPhotoInfoURL += "&jsoncallback=?";
            $.getJSON(getPhotoInfoURL, infoLoaded);
        });
    } else {
        $("#progressdiv").css({
            'visibility': 'hidden'
        });
        alert("NOT ENOUGH IMAGES WERE RETURNED");
        addKeyPress();
    }
    flickrGame.numberPhotosReturned = flickrGame.imageArray.length;
}

This shows the #progressdiv and changes the paragraph's text within the #progressdiv to read "Loading Photo Information". If not enough images were returned we hide the #progressdiv.

Next add the following to the infoLoaded() function:

 function infoLoaded(data) {
	flickrGame.imageNum += 1;
	var percentage = Math.floor(flickrGame.imageNum / flickrGame.numberPhotosReturned * 100);
	$("#progressbar").progressbar({
		value: percentage
	});
	var index = 0;
	for (var i = 0; i < flickrGame.imageArray.length; i++) {
		if (flickrGame.imageArray[i].photoid == data.photo.id) {
			index = i
			flickrGame.imageArray[index].username = data.photo.owner.username;
			flickrGame.imageArray[index].photoURL = data.photo.urls.url[0]._content;
		}
	}
	if (flickrGame.imageNum == flickrGame.numberPhotosReturned) {
		preloadImages();
	}
}

Here we set a variable percentage equal to Math.floor(flickrGame.imageNum / flickrGame.numberPhotosReturned * 100); this makes sure we get a number between 0 and 100. Then we call $("#progressbar").progressbar() and set the value property equal to percentage.

Now if you test the game it should work just as before, but with a progress bar. Well, there is one problem: the progress bar sticks around after the images get drawn. In the game we first load the photo information, then we preload the images and both use the progress bar. We will fix this in the next step.


Step 6: Preloading the Images

In this step we will utlilize the jQuery.imgpreload plugin (it's already in the source download). As soon as all the file information from the above steps has been loaded, the progress bar resets itself and monitors the loading of the images.

Add the following to the preloadImages() function:

function preloadImages() {
    flickrGame.tempImages = flickrGame.imageArray.splice(0, 12);
    for (var i = 0; i < flickrGame.tempImages.length; i++) {
        for (var j = 0; j < 2; j++) {
            var tempImage = new Image();
            tempImage.src = "cardFront.png";
            tempImage.imageSource = flickrGame.tempImages[i].imageUrl;
            flickrGame.theImages.push(tempImage);

        }
    }
    $("#progressdiv").css({
        'visibility': 'visible'
    });
    $("#progressdiv p").text("Loading Images");
    var tempImageArray = [];
    for (var i = 0; i < flickrGame.tempImages.length; i++) {
        tempImageArray.push(flickrGame.tempImages[i].imageUrl);
    }

    $.imgpreload(tempImageArray, {
        each: function () {
            if ($(this).data('loaded')) {
                flickrGame.numImagesLoaded++;
                var percentage = Math.floor(flickrGame.numImagesLoaded / flickrGame.totalImages * 100);
                $("#progressbar").progressbar({
                    value: percentage
                });
            }
        },
        all: function () {
            $("#progressdiv").css({
                'visibility': 'hidden'
            });
            drawImages();
        }
    });
}

Here we set the #progressdiv to be visible and change the paragraph to read "Loading Images". We set up a temporary array and add the temporary images' URLs to it, then pass the entire array to $.imgpreload to kick off the preload.

The each function gets run each time a photo is preloaded, and the all function gets run when all the images have been preloaded. Inside each() we check to make sure the image actually was loaded, increment the numImagesLoaded variable, and use the same method for the percentage and progress bar as before. (The totalImages is 12 since that how many we are using per game.)

Once all the images have been preloaded (that is, when all() is run) we set the #progessdiv to hidden and call the drawImages() function.

We need to add the numImagesLoaded and totalImages variables to our flickrGame object:

var flickrGame = {
	APIKEY: "76656089429ab3a6b97d7c899ece839d",
	imageArray: [],
	tempImages:[],
	theImages: [],
	chosenCards: [],
	numberPhotosReturned: 0,
	imageNum: 0,
	numImagesLoaded: 0,
	totalImages: 12
}

(Make sure you add the comma after imageNum.)

If you test the game now, you should see the progress bar for both the photo information and for the preloading of the images.


Step 7: Showing the Attributions

To conform to the Flickr API terms of service, we have to show attributions for the images we use. (It's also polite to do so.)

Add the following code within the hideCards() function:

function hideCards() {
	$(flickrGame.chosenCards[0]).animate({
		'opacity': '0'
	});
	$(flickrGame.chosenCards[1]).animate({
		'opacity': '0'
	});
	flickrGame.theImages.splice(flickrGame.theImages.indexOf(flickrGame.chosenCards[0]), 1);
	flickrGame.theImages.splice(flickrGame.theImages.indexOf(flickrGame.chosenCards[1]), 1);
	$("#image1").css('background-image', 'none');
	$("#image2").css('background-image', 'none');
	
	if (flickrGame.theImages.length == 0) {
		$("#gamediv img.card").remove();
		$("#gamediv").css({
			'visibility': 'hidden'
		});
		showAttributions();
	}
	addListeners();
	flickrGame.chosenCards = new Array();
}

Here, we check if the number of images left is zero, and if so we know the user has matched all of the cards. We therefore remove all the cards from the DOM and set the #gamediv to be hidden. Then, we call the showAttributions() function which we will code next.


Step 8: Show Attributions

In this step we will code the showAttributions() function.

Add the following beneath the checkForMatch() function you coded in the steps above:

function showAttributions() {
	$("#attributionsdiv").css({
		'visibility': 'visible'
	});
	$("#attributionsdiv div").each(function (index) {
		$(this).find('img').attr('src', flickrGame.tempImages[index].imageUrl).
		next().html('<span>Username: </span> ' + flickrGame.tempImages[index].username + '<br/>' + '<a href="' + flickrGame.tempImages[index].photoURL + '"target="_blank">View Photo</a>');
	});
}

Here we set the #attributionsdiv to be visible, and then loop through each div within it. There are 12 divs, each with an image and a paragraph; we use jQuery's find() method to find the img within the div, set the src of the image to the correct imageUrl, and use jQuery's next() method to set the username and photoURL to the info from Flickr.

Here are links to jQuery's find() and next() methods so you can learn more about them.

If you test the game now and play through a level, you'll see the attributions with a link to the image on Flickr. You will also see two buttons: one for the next level and one for a new game. We will get these buttons working in the next steps.


Step 9: Next Level

In our call to the Flickr API we set per_page to 36, to request that many images at once. Since we are using 12 images per game, this means that there can be up to three levels. In this step we will get the Next Level button working.

Add the following code within the setImages() function:

function setImages(data) {
	
	// ... snip ...
	
	flickrGame.numberPhotosReturned = flickrGame.imageArray.length;
	flickrGame.numberLevels = Math.floor(flickrGame.numberPhotosReturned / 12);
}

We need to know how many levels the game will have. This depends on how many images were returned from our search. It will not always be 36. For example, I searched for "hmmmm" and it returned about 21 photos at the time. We'll use Math.floor() to round the number down - we don't want 2.456787 levels, after all, and it would throw the game logic way off.

Make sure you add the numberLevels variable to the flickrGame object:

var flickrGame = {
	APIKEY: "76656089429ab3a6b97d7c899ece839d",
	imageArray: [],
	tempImages:[],
	theImages: [],
	chosenCards: [],
	numberPhotosReturned: 0,
	imageNum: 0,
	numImagesLoaded: 0,
	totalImages: 12,
	numberLevels: 0
}

(Don't forget to add the comma after totalImages: 12.)

Now modify the drawImages() function as follows:

function drawImages() {
	flickrGame.currentLevel += 1;
	$("#leveldiv").css({
		'visibility': 'visible'
	}).html("Level " + flickrGame.currentLevel + " of " + flickrGame.numberLevels);
	flickrGame.theImages.sort(randOrd);
	for (var i = 0; i < flickrGame.theImages.length; i++) {
		$(flickrGame.theImages[i]).attr("class", "card").appendTo("#gamediv");
	}
	addListeners();
}

Here we increment the currentLevel variable, set the #leveldiv to be visible and set the HTML of the div to read what level we are on and how many levels there are.

Once again, we need to add the currentLevel variable to our flickrGame object.

var flickrGame = {
	APIKEY: "76656089429ab3a6b97d7c899ece839d",
	imageArray: [],
	tempImages:[],
	theImages: [],
	chosenCards: [],
	numberPhotosReturned: 0,
	imageNum: 0,
	numImagesLoaded: 0,
	totalImages: 12,
	numberLevels: 0,
	currentLevel: 0
}

(I'm sure you don't need reminding by now, but make sure you add the comma after numberLevels: 0.)

Now modify the showAttributions() function to the following:

function showAttributions() {
	$("#leveldiv").css({
		'visibility': 'hidden'
	});
	$("#attributionsdiv").css({
		'visibility': 'visible'
	});
	if (flickrGame.currentLevel == flickrGame.numberLevels) {
		$("#nextlevel_btn").css({
			'visibility': 'hidden'
		});
	} else {
		$("#nextlevel_btn").css({
			'visibility': 'visible'
		});
	}

	$("#attributionsdiv div").each(function (index) {
		$(this).find('img').attr('src', flickrGame.tempImages[index].imageUrl);
		$(this).find('p').html('<span>Username: </span> ' + flickrGame.tempImages[index].username + '<br/>' + '<a href="' + flickrGame.tempImages[index].photoURL + '"target="_blank">View Photo</a>');
	});
}

We hide the #leveldiv by setting its visibility to hidden.

Next we check whether the currentLevel is equal to the numberLevels. If they are equal, there are no more levels available so we hide the #nextlevel_btn; otherwise, we show it.

Finally we need to wire up the #nextlevel_btn. Add the following code beneath the addKeyPress() function you created in the step above:

$("#nextlevel_btn").on("click", function (e) {
	$(this).css({
		'visibility': 'hidden'
	});
	$("#gamediv").css({
		'visibility': 'visible'
	});
	$("#attributionsdiv").css({
		'visibility': 'hidden'
	});

	flickrGame.numImagesLoaded = 0;
	preloadImages();

});

Here we hide the button, reveal the #gamediv, hide the #attributionsdiv, reset the numImagesLoaded variable, and call preloadImages() which grabs the next 12 images.

You can test the game now and should be able to play through all the levels. We will wire up the #newgame_btn in the coming steps.


Step 10: Starting a New Game

You can begin a new game at any time, but after all the levels have been played that is the only option. In this step we will wire up the #newgame_btn.

Add the following beneath the code for the #nextlevel_btn you added in the step above:

$("#newgame_btn").on("click", function (e) {
	$("#gamediv").css({
		'visibility': 'visible'
	});
	$("#leveldiv").css({
		'visibility': 'hidden'
	});
	$("#attributionsdiv").css({
		'visibility': 'hidden'
	});
	$("#searchdiv").css({
		'visibility': 'visible'
	});
	$("#nextlevel_btn").css({
		'visibility': 'hidden'
	});
	flickrGame.imageNum = 0;
	flickrGame.numImagesLoaded = 0;
	flickrGame.imageArray = new Array();
	flickrGame.currentLevel = 0;
	flickrGame.numberLevels = 0;
	addKeyPress();
});

Here we reveal the #gamediv, hide the #leveldiv and #attributionsdiv, reveal the #searchdiv, and hide the #nextlevel_btn. We then reset some variables, and call addKeyPress() so the user can search again.

If you test now you should be able to start a new game at any time, as well as when all levels have been played.

The game is complete as far as gameplay is concerned, but we need to show a splash screen. We'll do this in the next step.


Step 11: Splash Screen

We need to make some changes to our CSS file. Specifically, we need to set the #gamediv visibility to hidden, and set the #introscreen to visible. Open styles/game.css and make those changes now:

#gamediv {
position:absolute;
left:150px;
width:600px;
height:375px;
border: 1px solid black;
padding:10px;
color:#FF0080;
visibility:hidden;
background: #FFFFFF url('../pattern.png'); 
}

#introscreen{
position:absolute;
left:150px;
width:600px;
height:375px;
border: 1px solid black;
padding-top:10px;
color:#FF0080;
visibility:visible;
background: #FFFFFF url('../pattern.png'); 
padding-left:80px;
}

Next we need to change the addKeyPress() function. Remove everything from addKeyPress() and replace it with the following:

function addKeyPress() {
    $(document).on("keypress", function (e) {
        if (e.keyCode == 13) {
            if (!flickrGame.gameStarted) {
                hideIntroScreen();
            } else {
                doSearch();
            }
            flickrGame.gameStarted = true;
        }
    });
}

Here we check if the user has pressed the Enter key, then we check whether the game has started. If it hasn't we call hideIntroScreen(); otherwise, we call doSearch(); either way, we mark the game as having started. This means that the first time the user presses Enter it will call hideIntroScreen(), and the next time the user presses the Enter key it will call doSearch().

Now we need to code the hideIntroScreen() function. Add the following beneath the addKeyPress() function:

function hideIntroScreen() {
	$("#gamediv").css({
		'visibility': 'visible'
	});
	$("#introscreen").css({
		'visibility': 'hidden'
	});
}

If you test the game now you should see the splash screen; press Enter and you can play the game as before.


Step 12: A Better Alert

Right now if enough images are not returned for a game, we pop up an alert. Although this works, we can make it look a little nicer by using jQuery UI's dialog.

We need to edit index.html, so open it and add the following right inside the #gamediv:

<div id="gamediv">
<div id="dialog" title="Sorry">
<p>Not enough images were returned, please try a different keyword.</p>


</div>

Now we need tie it in. Add the following beneath the hideIntroScreen() function in the JS file:

$("#dialog").dialog({
     autoOpen: false
});

This code converts the #dialog div into a dialog; we disable the auto-open feature.

We want to trigger this dialog to open instead of the alert we had before, so remove the alert from the setImages() function and replace it with the following:

} else {
        $("#progressdiv").css({
            'visibility': 'hidden'
        });
        $("#dialog").dialog('open');
        addKeyPress();
    }
    flickrGame.numberPhotosReturned = flickrGame.imageArray.length;
    flickrGame.numberLevels = Math.floor(flickrGame.numberPhotosReturned / 12);
}

Now, if not enough images are returned we get a nice looking dialog, instead of using an alert reminiscent of webpages from the '90s.

Don't forget to change this line, from preloadImages():

tempImage.src = "cardFront.jpg";

...back to this:

tempImage.src = flickrGame.tempImages[i].imageUrl;

...otherwise, the game will be a bit too easy!

Now test the final game. If anything's not quite right, you can always compare your source to mine, or ask a question in the comments.

Conclusion

We have coded a fun little game using images from the Flickr API, and given it a decent layer or two of polish. I hope you enjoyed this tutorial and learned something worthwhile. Thanks for reading and have fun!

Advertisement