Shkrubbel
I gave this presentation at Open Web Camp at Stanford on 16 July 2011. Organized by John Foliot (@johnfoliot), Ed Palumbo (@doted), and Thomas Ford (though I interacted with only John), Open Web Camp was an *amazing* event: the speakers were incredible, the talks great, the events well organized, the location fantastic (wow, I love Stanford). Both lunch and t-shirts were provided by sponsors, and, wow, I can't adequately convey how much I enjoyed the event.
My talk was about how to build a mobile app with web technologies. I'll do a screencast shortly. In the meantime, my slides and a transcription of the talk I gave:
Building a game of words, made with letters, using HTML5, CSS3, and javascript to build a mobile application
A few months I went to a web tech workshop, expecting not much other than a day of head nodding and thoughts of, "yeah, I know that." To my delight, I walked out with pages of notes, tips and new topics to look up, and I thought to myself, next tech talk I do, I'm so way going to try for that. So, my hope is that you leave this talk with a spring in your step and more than a few inspirations on your next project.
My inspiration for this particular project was a conversation I had with Jonathan Snook, who gives a number of talks about HTML5 and CSS3 and javascript and mobile stuff. He commented that a number of the apps available in the Apple App Store could be done as web applications, and went on to build a number of them and tweet about them.
One of the applications he wanted to do was Words With Friends, and I thought, eh, why not? It'll make a great walk through in creating a mobile application, because the hard part is done: figuring out WHAT you want to make, making it is the easy part.
So, let's start out, with a Scrabble or Words with Friends board. That's easy, you need a grid for the board, a tray for the tiles, a few buttons for actions and a few extra elements for the game details like score and who's playing.
A board.
Can even something as simple as a board have options?
Yeah.
div.tile { float: left; } div.tile { display: grid } table td
You can do a grid with a divs of a specified width and height and float them all to the left, clearing the floats on the last element. Need to make sure the row and board have widths, though, to prevent the dreaded float dropdown.
Another option is { display: grid; }
- grid is a proposed option for the display attribute that creates a layout grid much as tables were used for page layouts. You can have grid columns and rows and the equivalent of spans, and everything just lays out nicely. Unfortunately, unless your browser is IE 10, and pretty much no one's is quite yet, { display: grid; }
isn't available. But sometimes "soon" we can use the { display: grid; }
on our elements and have the browser generate the grid for us.
http://caniuse.com/css-grid
http://dev.w3.org/csswg/css3-grid-align/
So, with the toss up between floating divs and using a table for the grid layout, I used the table. Floating divs have the advantage of fewer elements to get the rounded corners, as you can't style rounded corners on a table cell, but you add a div and style that inside the table data element. Tables have the advantage that when you resize the screen, the table cells will resize, too, which can be handy.
<pre> <table id="board"> <tr> <td id="r00"></td> <td id="r01"></td> <td id="r02"></td> <td id="r03" class="tw"></td> ...Okay, so, the board. Technically, the board can be any number of tiles across that we want. Using both Scrabble and Words with Friends as inspiration, we'll have a 15 by 15 board. If you're inspiring younger players, fewer board spaces might be less intimidating.
Now, how do we style the background?
We could use one giant background image.
We could style it exactly as we want it, we can do fun and exciting things with the background image, we can have polka dots, we can get pixel perfect across every single browser that supports background images, which is nominally all of them.
This approach has some disadvantages: you'll need to update your image when your colors change, or have multiple images for different board sizes (tile counts), and you'll need to have different images for every screen resolution, including some of those that don't yet exist. If you decide to add a feature that the background won't support (in this case, say, random placement of the Double and Triple Words and Letters), you'll need new background images for the "random" boards.
While the single background image isn't terribly flexible, but IS the correct solution for some applications. A side scrolling game with multiple repeating backgrounds sliding at different rates to fake parallax, for example, could be served with background images.
Okay, other than the single background image, we could still use images to style the individual tiles, also using background images. If you do this, you'll want to use sprites, merging all of your tile images together into one image. The round trip HTTP request for multiple images is more of a performance hit on mobile than it is on the desktop, though it is on the desktop also: better to have one big image than 110 small images each with its own request.
This technique has some of the issues of the previous image technique: having less flexibility, needing to scale the images or having differing images altogether for different screen resolutions, but you don't have the variable board size issue.
Alternately, we can just use CSS.
So, what are the characteristics of a tile? Well, it has rounded edges. We can get rounded corners with CSS with no effort these days:
-webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px;
It also has texture. Okay, for texture, one of the easiest ways to get the texture is to use a patterned, tiling background. Want a site for such patterns? http://tileabl.es is a good one.
Update: Andrew Wooldridge (blog) let's me know of http://subtlepatterns.com/ as another site for nice, tileable background images.
We can, however, use javascript to generate a random noise for a background.
https://github.com/DanielRapp/Noisy
$('body').noisy({ 'intensity' : 1, 'size' : 200, 'opacity' : 0.08, 'fallback' : '', 'monochrome' : false }).css('background-color', '#EDEBDE');
Okay, so we have the background, we have the colors, what about the big part staring at us in the face?
The font face.
We'd like to match the Scrabble font, as well as we can. A quick search and we see that a consensus is that the font is "News Gothic". Now, this isn't one of the supported-by-default browser fonts. While we can choose another font that looks very similar, or use the background image technique we decided against earlier, we can use @font-face to render new fonts in the browser.
And designers everywhere had a field day.
Okay, so we download our new font from the 'net, finding it at http://www.fontyukle.net/en/DownLoad-News+Gothic+Std+Medium.ttf, and look at it.
It's a True Type Font. True type fonts (.ttf) won't render with @font-face
. Okay, so, we can go through the process of converting the font to the different formats, .eot: embedded open type, .woff: web open font format, or we can do what Snook suggested, just use Font Squirrel.
Font Squirrel's @font-face
generator gives a fantastic interface for converting various font formats to font-face web embeddable fonts. You can upload a .ttf file, and receive back the converted font files, along with a CSS stylesheet that you can include in your project.
Now, just because it's easy to do, doesn't mean you should do it willy-nilly. Be sure you have the right to distribute your font before creating and using a font on your site. If you don't, don't use it. I don't have the right to distribute the News Gothic font, so it's not included in the project's source.
Update: You can buy a @font-face
version at http://new.myfonts.com/.
There are font services available, use one of them if you are unsure about your fonts.
http://fontsquirrel.com/
http://fontspring.com/
http://typekit.com/
http://www.google.com/webfonts
Okay, so, we have our basic page done, we're happy with the layout, we have the board, we have the tray, we have our starting buttons for the game.
Our default tile:
<div class="tile"><span class="letter">O</span><span class="val">1</span></div></td>
Let's start making it work!
First thing we want to do, is start dragging the tiles from the tray to the board. Time to load in the javascript. Following the thought that many eyes make all bugs shallow, and idea of standing on the shoulders of giants, and the realization that there are some darn smart people who have worked on this stuff before me, instead of using the native HTML5 Drag and Drop or reinventing the wheel, I'm going to use a javascript library. For me, that means jQuery.
For mobile apps, there are a number of framework options available, depending on what development style, approach and feature requirements, the big ones being jQTouch, Sencha Touch, jquery mobile, and phonegap. I'm a big believer in using the tools you're comfortable with, the ones you know, but continue to expand your knowledge. If you can't decide when starting out, use the ones that have the best documentation (and how do you define best? Can _you_ understand it? Great! Choose that.)
In this case, we need drag and drop, and submit capabilities, and not much else: no location, no accelerometer, no GPS, no camera, which makes things considerably simpler.
So, starting out, added jQuery to the document.
<!DOCTYPE html> <html> <head> <script src="js/jquery-1.6.2.js"></script> <script src="js/jquery-ui-1.8.14.js"></script>
Also added jQuery UI, which has drag and drop functionality with it. While I was at it, I also moved the CSS into an external style sheet, and created a new javascript file for the project. All of them included at the top of the page, along with the !DOCTYPE. Love that new doctype.
Preliminaries in, let's start dragging tiles, by adding draggable to the letters:
$(document).ready(function() { $(".tile").draggable(); });
It's that easy. That's great!
Except our letters are now floating out in the middle of nowhere. With no styling of their own, the letters appear to float.
Need to add opacity so that we see the board background, and a tile background color so that we can actually see it.
.tile { opacity: .9; background-color: AntiqueWhite; }
We want the letters to drop on the board tiles, not just randomly. jQuery UI makes it Easy enough to snap to the board squares:
$(".tile").draggable( { snap: "#board td" });
Now, there are draggable objects in jQuery along with items that can be used as containers to receive the dropped elements, appropriately called "droppables": HTML elements that can have draggables dropped on them. We'll limit the tiles being dropped on the board for the moment, and give it red border around it so that we can see it dropped.
$("#board td").droppable({ drop: function(event, ui) { $(this).addClass("dropped"); ... } }
We need to keep track of of the changes to the board that we're making. We can let the board be the canonical source, extracting the data from the DOM (which can be slow) or we can keep track of the state elsewhere.
Advantage of the board is that what is displayed is what we have, we don't have to keep two different sources. Disadvantages of using the DOM to store the data include mixing our data with our view, complicated access and save methods, and a slow DOM query to extract the data.
So, let's have a 2D array to hold our board tiles. Our representation of the board becomes:
SHK.board = new Array(15); ... for (var i = 0; i < 15; i++) { SHK.board[i] = new Array(15); for (var j = 0; j < 15; j++) { SHK.board[i][j] = ''; } }
When a tile is dropped we can save its state this board array by using the drop event we defined above. We're currently just changing the border color of the landing board space, but we can also save the row and column numbers to our board array we just defined.
So, how do we identify which of the in play tray tiles we're moving and dropping?We could use an extra div to store information, and just hide it:
<div class="tilenum">14</div> .tilenum { display: none; }
But it's much easier to use the data-* attributes!
<td id="l1"><div id="t1" class="tile" data-tilenum="1"><span class="letter">U</span><span class="val">2</span></div></td>
"Custom data attributes are intended to store custom data private to the page or application, for which there are no more appropriate attributes or elements."
http://html5doctor.com/html5-custom-data-attributes/
We have the tile number from the tray on the dragging tile. We can also store the previous location in the tile itself in another data- attribute, just as we store the tile number. A tile in the tray has no previous board location. Once a tile is dropped on the board, we can save its location.
// if previously placed on a board tile, clear the last position var p = ui.helper.data('prevloc'); if (ui.helper.data('prevloc')) { var p = ui.helper.data('prevloc'); SHK.board[p[0]][p[1]] = ''; } // set the new position ui.helper.data('prevloc', [row, col]);
data- attributes can be used for all sorts of things, just don't use them for things that should be public, like microformats, or an existing attribute.
Sidenote:
The board space divs are named with an id of r followed by two hexadecimal numbers. The first is the row number, the second is the column number. Recalling we're in hex, we need to convert to decimal for storing into our array (okay, no, but it made it easier earlier):
<td id="r9b"><div class="it"></div></td> var row = id.substring(1, 2); // 9 var col = id.substring(2, 3); // bb = 11, so need to convert from hex to decimal
Do it this way:
parseInt(row, 16);
Okay, so, we've dragged tiles to the board. As soon as we've dragged a tile, we can submit it to the server, right? Sure!
A quick adjustment to the button to change it from a pass to a play link and we're good to go.
// submit the board $('#bp').attr('href', (SHK.inplaycount()) ? 'go/play':'go/pass'); $('#bp').html((SHK.inplaycount()) ? 'submit' : 'pass');Or, maybe not.
While dropping a tile on the board means we can submit the board, we should really verify all tiles are placed legally. In particular, we need to check the tiles are all in a row, and that they are all contiguous with the all letters on the board.
To check if all the placed letters are in a line, we can loop through the positions of the placed tiles and check that the row is the same for all the placed tiles or the column is the same. When only one tile is placed, we'll have both cases being true.
// t = tile to compare // rc = index of inplay array: 0 = row, 1 = col, 2 = letter var t = false; for (var i in SHK.inplay) { // inplay holds the max seven tiles to move if (i && SHK.inplay[i]) { // if we have set our second or later value to compare if (t) { // if the set value doesn't match the current value, we're done if (t != SHK.inplay[i][rc]) { return false; } } else { // set the first value to check later tiles against t = SHK.inplay[i][rc]; } } } /* end for */
To check if the tiles were placed contiguously, using whichever direction the line of letters were placed (up/down or left/right, as previously determined), find the minimum index on the board (so if the tiles were all placed on row 4, find the lowest column value of tiles played in row 4), then find the maximum index of all tiles on the board in that same line, then loop through the line and check there are no gaps by incrementing by one from the minimum to the maximum in that line. If there are gaps, the word isn't valid.
At this point, we have a board, the player has played a word, how do we know it's a valid word? Can we submit the board now?
In traditional scrabble games, one player plays a word and the other player challenges the word if she believes the word played isn't a valid word. At this point, however, not only do we want to demo more of the HTML5 features, but I also told you we were going to download a dictionary and store it in HTML5 local storage, so we're going to go the Words with Friends route: instead of challenge, we're going to check the word is valid before sending it to the server.
To do that, though, we need to first get the dictionary. The uncompressed dictionary I have is just under 2MB in size. That is not going to fit into our 4K cookie space alloted.
So, let's use localStorage!
Seriously, this is NOT the way to build a highly scalable multi-player application. Having users download 1.9MB of dictionary? Yeah, don't do that.
There's another dictionary that we're going to go with, it's only 620kB, so let's download that, but do it only once so that people don't start throwing fits at how dang slow the app loads.
// See if the property that we want is pre-cached in the localStorage if ( window.localStorage !== null && window.localStorage.shkrubbeldict ) { var words = window.localStorage.shkrubbeldict.split("\n"); for ( var i = 0; i < words.length; i++ ) { SHK.dict[words[i]] = true; } } ... // Cache the dictionary, if possible if ( window.localStorage !== null ) { window.localStorage.shkrubbeldict = txt; }
See: http://diveintohtml5.org/storage.html
Once we have it loaded into our dictionary array (did I mention don't do this?), we can check if the word is valid with a simple check:
if (SHK.dict[word.toLowerCase()]) { // we have a word! }
Did I mention don't do this? There are more efficient ways of storing large dictionaries.
http://ejohn.org/blog/dictionary-lookups-in-javascript/
http://ejohn.org/blog/javascript-trie-performance-analysis/
http://ejohn.org/blog/revised-javascript-dictionary-search/
While we're here, and I'm taking a breather, let's just implement that recall button...
To do that, we need to:
1. move the tiles back to the tray
2. clear our inplay tiles
3. clear their spots out of the board, using data prevloc as before
And the resign button, that one's easy, too, just submit a resign call to the server, no further updates needed.
Wait a second... you know what's been missing from this entire process? Yeah, we should test this on an actual mobile device, eh?
First time I brought this up on an ipad and went to drag the tile, and the whole window dragged. While that happened to be a pretty simple issue to fix, and this project doesn't require a lot of interaction from the server, that isn't always the case. When testing across multiple devices, or on mobile devices in a non-test environments, when I need remote debugging, yeah, "winery" for the win:
http://phonegap.github.com/weinre/
While testing, wow, remember that part where I said don't do this? When you have a desktop or even laptop computer with a fast connection and lots of member, even running an emulator doesn't seem too bad or slow. You can fake the connection by having a proxy that can slow things down.
But wow, okay, performance.
This isn't a big application, but as the features and code base grow, we're going to need to start paying attention to performance, as well as loading time. One way to reduce loading time is, well, not to load resources you don't need. If we had gone with the background images, for example, wow, what a pain in the download to fetch that large background image, only not to use it.
Blech.
So, downloading and handling those extra files, how do we avoid them?
Well, we *could* do browser detection, but have you seen the list of User-Agent strings (which is what you use to detect the browser)? The list grows,
People now do browser feature detection: what can the browser support?
Easiest way is with Modernizr, which includes yepnope.js. yepnope lets you choose which files are downloaded based on browser feature support (yep) and lack of browser feature support (nope).
Depending on which features you need for your application, you can follow Modernizr's links for libraries and tools that fill in the missing pieces so that you can fake the features.
In this case, though, we can use media queries to adjust the CSS3 that we load.
As long as we've been using the link element, we've pretty much been using the media attribute. Used to be, you'd see them for only print style sheets.
<link rel="stylesheet" href="theme.css" /> <link rel="stylesheet" href="print.css" media="print"/>
But now with media queries in CSS3, we can be more selective based on browser information and our specified links.
/* iphone portrait */ @media screen and {max-width: 320px) { ... } /* iphone landscape */ @media screen and {min-width: 321px) and (max-width: 480px) { ... }
Alternately, we can use Adapt to dynamically load different style sheets based on changing browser size.
Though, really, except for rotating between portrait and landscape, no one changes their browser sizes except designers.
You are going to want to compress your files before delivering them. Seriously now, white space is overrated in production. Remove it and the other stuff not needed:
http://developer.yahoo.com/yui/compressor/
http://www.phpied.com/cssmin-js/
Reduce the image sizes, using compression or different image formats depending on your need (JPG is not always the smallest image). Use Smush.it or Image Optimizer to reduce image sizes.
http://developer.yahoo.com/yslow/smushit/
http://www.imageoptimizer.net/Pages/Home.aspx
Sprite your images when you can, reducing the number of calls for resources. Combine your CSS files into one cached version and send that over. On the server, gzip the files and send the compressed version to the browser. Remove unneeded whitespace when compressing: in production, white space is overrated.
If you can, maybe use Data URIs for images. The advantage is that you won't have 404s, and no second http round trip is needed to fetch the image. You may or may not save bytes, though, depending on the image's optimization from before.
See: http://css-tricks.com/5970-data-uris/
So, all of this is great, but I don't want to be connected to the internet in order to play. I'd like to have all of my games loaded up so that I can think about the game as I'm swinging in my hammock.
First step, is back to localStorage. When saving the game to the server, we can save the game locally also, and on successful submit of a word, update the game's local copy with the new tray letters.
To be able to save to the device, however, we need to generate and deliver a MANIFEST
http://diveintohtml5.org/offline.html
http://developer.apple.com/library/safari/#documentation/iPhone/Conceptual/SafariJSDatabaseGuide/OfflineApplicationCache/OfflineApplicationCache.html
An easier way to generate a MANIFEST, though, is to let a webserver walk your application and tell you what it needs. In particular:
https://github.com/jamesgpearce/confess
Okay, now we need to tell the mobile browser (well, apple anyway) that the app is available as an offline application. We do that by adding the meta attribute to the top of the page, an icon, and a splash screen:
<meta name="apple-mobile-web-app-capable" content="yes" /> <link rel="apple-touch-icon" href="icon.png" /> <link rel="apple-touch-startup-image" href="home.png" />
We need to do a little more than just store it in localStorage. We need to check at points for the status updates of the games, and send the words a player has played when the connection is back. Having a blocking game, I can't play until you have played, makes this a little easier to manage, but state is definitely an issue to watch out for.
Okay, so I've played my game, and now it's time for "not kitt" to play her turn. With native applications, you have the ability to send notifications to other people for installed applications.
Web applications are missing a number of these core features, but using web technologies doesn't mean your left out. Recall that point before about a lot of really smart people doing really cool shit?
Yeah.
http://www.phonegap.com/
http://www.appcelerator.com/
It's possible to access the phone features with other kits, if those are needed. Alternately, you could create a native application that wraps the application in a, say, UIWebView (for iPhone) so that the user sees the web app, but you can register for push notifications.
Or just go with the smart guys.
So, what's next?
I can think of a giant list of game features that I'd like to put in. Fortunately, someone else created a list of supported browser features for me to build on:
I'm still trying to figure out how to get canvas or video into the application in a meaningful way.
My plan is to clean up the code and post it on github within the next two weeks or so. I'll update this post when I do.
Add new comment