Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

503 lines (358 sloc) 30.863 kb
<h1>HTML5: Beyond the Buzzwords</h1>
<p>I am aiming to expand your imagination about the kinds of features that can be added to modern web applications without adding any dependencies
or friction to your codebase. I want to provide you with just enough knowledge that you could start to take advantage of these
capabilities in your own apps.</p>
<li><p>These instructions were built using the latest version of Chrome, but will also work with the latest version of Safari or Firefox. Some of the exercises do not work perfectly in Firefox due to limitations in the browser.
I have not tested them with IE or Opera but much of the code should still work.</p></li>
<li><p>Your browser should have a built-in JavaScript console. Chrome and Safari have a built-in JavaScript console. If you are using Firefox, be sure to install <a href="">Firebug</a>.</p></li>
<li><p>You need some kind of basic text editor (Vi, Emacs, Textmate, Notepad, etc.) to make changes to the project files.</p></li>
<li><p>Copy the directory named <code>start</code> from this project to a new location on your machine. This will form the basis for the game we are building
and includes all necessary dependencies.</p></li>
<li><p>In your project, examine the index.html file you copied from <code>start</code>. This skeleton is heavily stripped down from the <a href="">HTML5 Boilerplate project</a>. The <a href="">full template</a>
is a good way to learn some state of the art HTML5 practices.</p>
<p>Note the doctype that starts this file. That's all you need to do to indicate to the browser that you want to use the latest, greatest version of HTML.
The boilerplate also shows the use of new HTML5 semantic tags <code>&lt;header&gt;</code> and <code>&lt;footer&gt;</code>. You can use these instead of less semantic code such as <code>&lt;div id=&quot;header&quot;&gt;&lt;/div&gt;</code>.</p></li>
<li><p>Open up the index.html file in your browser. Then activate your JavaScript console (in Chrome, go to the View menu, choose Developer, then choose JavaScript console; in Safari, hit option-Apple-C). You should see the message &quot;You have a working console&quot;. If you don't see this message you should install Firebug or switch to a different browser.
You will have a hard time completing this tutorial without a working console.</p>
<p>That message was created by the file <code>js/tutorial.js</code>. I'll be using that file for all of our application code.</p></li>
<h2>Feature Detection</h2>
<li><p>Examine <code>js/libs/modernizr</code>. This is the <a href="">Modernizr</a> library that performs browser feature detection and helps style new HTML5 semantic elements. Because you can't yet
rely on users having fully-capable browsers, feature detection is essential for providing useful error messages and fallbacks.</p></li>
<li><p>To activate Modernizr, use your favorite text editor to add this line to the <code>&lt;HEAD&gt;</code> tag of <code>index.html</code>.</p>
<p><code>&lt;script src=&quot;js/libs/modernizr-1.7.js&quot;&gt;&lt;/script&gt;</code></p>
<p>Then add the class <code>no-js</code> to the <code>&lt;HTML&gt;</code> tag on the second line of the index.html file.</p></li>
<li><p>Open up <code>index.html</code> in your browser and inspect the <code>&lt;HTML&gt;</code> tag. Notice that <code>no-js</code> has been replaced by several CSS declarations that can be used to style fallback content, reveal error messages, etc.</p></li>
<li><p>Open up a JavaScript console and inspect the Modernizr object. To be able to complete all of the exercises in this tutorial, you'll need to see true values for all of these properties:</p>
<p>If you don't see &quot;true&quot; for all of these you should go ahead and install the <a href="">Chrome</a> browser.</p></li>
<h2>Extra Credit</h2>
<p>Using only CSS and HTML, use the <code>&lt;HTML&gt;</code> classes <code>touch</code> and <code>no-touch</code> to reveal a message to the user indicating whether or not they have a touch interface. If you have a mobile OS simulator installed (such as iOS Simulator) you can
test both messages.</p>
<h2>Basic Canvas Drawing</h2>
<li><p>Add <code>&lt;canvas id=&quot;main&quot; width=&quot;400&quot; height=&quot;400&quot;&gt;&lt;/canvas&gt;</code> just after the opening of the <code>&lt;body&gt;</code> tag in your <code>index.html</code> file.</p></li>
<li><p>To draw a rectangle in the canvas, add this code to js/tutorial.js:
<pre>var canvas = document.getElementById(&quot;main&quot;);
var context = canvas.getContext(&quot;2d&quot;);
<p>All drawing operations happen on the canvas context, not the canvas itself. Right now &quot;2d&quot; is the only available context, but a future HTML spec may define a 3d context.</p></li>
<li><p>Open <code>index.html</code> in your browser. You should see a black rectangle!</p></li>
<li><p>Change the <code>fillRect</code> call to <code>strokeRect</code> and reload your browser. Now the rectangle is not filled-in.</p></li>
<li><p>Set the context's <code>fillStyle</code> property to a CSS color like &quot;red&quot;, then reload the page to see a red rectangle.</p>
<pre>context.fillStyle = "red";</pre></li>
<li><p>To draw text, add lines like this to <code>tutorial.js</code> and reload.</p>
<pre>context.font = "bold 24px sans-serif";
context.fillStyle = "blue";
<h2>Extra Credit</h2>
<p>Try filling your rectangle with a gradient. You'll need to create a gradient object as explained in <a href="">Dive Into HTML5</a>, then set your fillStyle to that gradient.</p>
<h2>Canvas Image Manipulation</h2>
<li><p>Copy the file <code>media/water.jpg</code> from the tutorial project to your project's <code>media</code> directory. (This image is <a href="">Creative Commons-licensed</a>)</p></li>
<li><p>Draw this image using the canvas by adding these lines to your <code>tutorial.js</code> file:</p>
<pre>var img = new Image();
img.src = "media/water.jpg";
img.onload = function() {
<li><p>Reload your browser to see the image.</p></li>
<li><p>Now add two additional parameters, a destination width and height, after the original 3 parameters:</p>
<li><p>Reload the browser and observe the image scaled to a different size.</p></li>
<li><p>Copy the sprite file we'll be using for our simple game, <code>media/characters.jpg</code> to the <code>media</code> folder of your project. (I got this CC-licensed file by David E. Gervais from the <a href="">TomeTik</a> project)</p></li>
<li><p>Use the 9 argument version of drawImage to slice out one character from the characters file and project it onto the canvas:</p>
<p>The 9 arguments are explained in this image:
<img src="" alt="drawImage arguments">
<p>Each sprite is 32 pixels wide and 32 pixels high. So if you want to slice out the 2nd character in the top row, with the red hood, you would start with these parameters:</p>
<li>sx = 33</li>
<li>sy = 0</li>
<li>sw = 32</li>
<li>sh = 32</li>
<p>Choose dx, dy, dw, and dh to your taste, then reload the browser. Consult the solution in <code>ex3</code> if you get confused.</p></li>
<h2>Basic Animation</h2>
<li><p>Copy <code>js/libs/jquery-1.5.2.js</code> to the <code>js/libs</code> directory in your project. We'll be using jQuery to bind keyboard events and to perform other tasks.</p></li>
<li><p>Add a this tag to the bottom of your index.html file, before the <code>tutorial.js</code> file is loaded, to load the jQuery library:</p>
<p><code>&lt;script src=&quot;js/libs/jquery-1.5.2.js&quot;&gt;&lt;/script&gt;</code></p></li>
<li><p>Comment out most of the drawing code we had been working with so the page starts clean. The only working code in your <code>tutorial.js</code> should look like this:</p>
<pre>var canvas = document.getElementById("main");
var context = canvas.getContext("2d");
var characters = new Image();
characters.src = "media/characters.gif";</pre></li>
<li><p>Initialize two variables, <code>x</code> and <code>y</code>, to zero. We'll use these to keep track of the user's current position.</p></li>
<li><p>Initialize variables to store the height and width of the canvas. Later we'll be changing the size of the canvas dynamically so it makes sense to not hard code these values.</p>
<p><pre>var height = $(canvas).height();</pre>
<pre>var width = $(canvas).width();</pre></p></li>
<li><p>Setup another onload function for the characters object. When the characters image has loaded, we want to bind keyboard events to a function we'll write next:</p>
<li><p>Write a move function that updates the character position on screen based on which arrow key was pressed. Using the binding from the above step, your move function should
accept one argument passed by jQuery: an event object. That object has a &quot;which&quot; property containing the code for the key that was pressed.</p>
<p>The key codes are:</p>
<li>Up = 38</li>
<li>Down = 40</li>
<li>Left = 37</li>
<li>Right = 39</li>
<p>Increment or decrement x or y by 10, depending on which key was pressed. Be sure to guard against x or y going out of bounds (less than zero or greater than the canvas size).</p>
<p>If you get confused check out <code>ex4/js/tutorial.js</code> for an example move function.</p></li>
<li><p>At the end of your move function, call <code>context.drawImage</code> to slice out one of the sprites from our file and project it on the screen at the coordinates stored
in x and y.</p>
<p>If you get confused check out <code>ex4/js/tutorial.js</code> to see what this looks like.</p></li>
<li><p>Reload your browser and try it out. You should see your character tracking across the screen slowly.</p></li>
<li><p>Let's make this look nicer by clearing the screen before each draw command. Add this before your drawImage call:</p>
<li><p>You may also want to increase the number of pixels that the character travels per key press.</p></li>
<h2>Extra Credit</h2>
<p>Try setting up an animation loop that redraws the screen a few times per second. This will let you decouple the keyboard events from the drawing. You'll need something like this
to start your loop:</p>
<p>Where runLoopFunction is the name of your function and interval is the number of milliseconds the browser should wait in between calls to that function.</p>
<h2>Fun With Forms</h2>
<li><p>When we link this game up with other players we want to know who's who. So let's add a username field that takes advantage of new HTML5 form features. Add this line to your <code>index.html</code> file:</p>
<p><code>&lt;input id=&quot;username&quot; placeholder=&quot;Your name&quot;&gt;</code></p></li>
<li><p>Reload the page. If you don't see any placerholder text, check the value of <code>Modernizr.input.placeholder</code> in your JavaScript console.</p></li>
<li><p>Now add the <code>autofocus</code> attribute to that input field and reload. If your browser supports it, the field will automatically receive the focus. </p></li>
<li><p>Let's try out a new form element, a slider we can use to control the size of our character. Add this to your <code>index.html</code> file (1):</p>
<p><code>&lt;input id=&quot;size&quot; type=&quot;range&quot; min=&quot;4&quot; max=&quot;320&quot; step=&quot;8&quot; value=&quot;32&quot;&gt;</code></p></li>
<li><p>Bind changes to that slider to a function in <code>tutorial.js</code> that will control the destination width and height of your drawImage call. The binding should look like this:</p>
<p><code>$('#size').change(function() { ... });</code></p>
<p>To get the value of the slider, use <code>$('#size').val()</code></p></li>
<p>(1) This element does not render in Firefox. Try it out in Safari or Chrome.</p>
<h2>Extra Credit</h2>
<p>Try out some of the other input elements listed in the <a href="">Dive Into HTML5 book</a>, like the color picker!</p>
<h2>Local Storage</h2>
<p><strong>Note</strong>: this exercise will not work well in Firefox because of a bug in the way Firefox handles file:// URLs. If you want this to work in Firefox you'll need to serve up your code from a web server
on your development machine. <a href="">Details</a></p>
<li><p>At your JavaScript console, set a localStorage value:</p>
<li><p>Close the page completely, then reopen it in a new window and type this at your JavaScript console:</p>
<p>If your browser supports localStorage, you should get the value of <code>shaz</code> back.</p></li>
<li><p>Try the above steps with the sessionStorage object. How does sessionStorage differ from localStorage? What about when you merely reload a page? Do they perform in the same way?</p></li>
<li><p>Call <code>localStorage.clear()</code> and try getting the 'shaz' item again.</p></li>
<li><p>Bind a new function to the change event of the username input field we added in exercise 5, like this:</p>
<pre>$('#username').change(function() { ... });</pre></li>
<li><p>In that bound function, use localStorage to store the user's name. You may need to change the focus by clicking elsewhere on the page to get this event to fire. To fetch the value of the username field, use <code>$(&quot;#username&quot;).val()</code>.</p></li>
<li><p>Add a line of code to <code>tutorial.js</code> to fetch the user's name from localStorage (which you stored in the above step) when the page loads. If there is a previously-stored username, set the value of the username field
to the pre-stored field like this: <code>$(&quot;#username&quot;).val(nameStr)</code>.</p></li>
<li><p>Reload the page. Type in a username, tab away from the username field (to make sure the change event fires), then reload the page. Your username should already be filled-in, instead of the placerholder text.</p></li>
<li><p>Try setting and getting numbers and hashes in localStorage. Does localStorage preserve type?</p></li>
<p><em>Your JavaScript console may also have an inspector for local and session storage. In WebKit's inspector, it's Storage tab.</em></p>
<h2>Extra Credit</h2>
<li><p>Bind the window object's <code>storage</code> event to keep track of when new items are added to localStorage. See <a href="">Dive Into HTML5</a> if you get stuck.</p></li>
<li><p>What happens when you try to get an item that has not been previously set from localStorage?</p></li>
<h1>EXERCISE 7</h1>
<h2>Canvas Cleanup</h2>
<li><p>Let's make things a little nicer before we add multi-player features to this &quot;game&quot;. First, let's set a dark background for our canvas. Add this declaration to the <code>&lt;head&gt;</code> section of <code>index.html</code>:</p>
canvas {
background-color: black;
input { display: block; }
<li><p>You may also want to increase the width and height of your canvas element.</p></li>
<li><p>Increase the step size for your character so it moves 5 or 10 pixels at a time.</p></li>
<h1>EXERCISE 8</h1>
<h2>Web Sockets</h2>
<p><strong>Note</strong>: WebSockets are disabled in Firefox. You may be able to get something working by using <a href=""></a>.</p>
<li><p>Let's connect to a websocket server in order to exchange information with other players. Add the following code to your <code>tutorial.js</code> file. I noticed that I would sometimes get errors
if this code fired before the <code>characters.gif</code> image file loaded, so you may want to stick this in the onload handler for that object. See the exercise 8 solution if this is confusing.</p>
<pre>var ws = new WebSocket("ws://");
ws.onmessage = handleMessage;
function handleMessage(event) {;
<li><p>Reload your browser. Every 10 seconds you should see a &quot;ping&quot; message from the server. Note that this is a JSON string that we'll need to unmarshal before we can work with it.</p></li>
<li><p>Check out the Ruby code in <code>server/server.rb</code> to see what you're connecting to with that string.</p></li>
<li><p>Modify the <code>handleMessage</code> function to parse the JSON string. If your browser does not have a native JSON implementation, you'll need to add the script <code>js/libs/json2.js</code> to your project for this code to work.</p>
<pre>var msg = JSON.parse(;;</pre></li>
<li><p>Reload your browser, wait 10 seconds, then check your console. You should see the de-marshalled JSON object displayed.</p></li>
<li><p>Modify your <code>move</code> function to send a JavaScript object out on the websocket every time you move your character. Use the websocket.send method like this:</p>
<pre>ws.send(JSON.stringify({ name: name, x: x, y: y, type: "move" }));</pre></li>
<li><p>Check out the server log being tailed on the screen. You should see your movement messages showing up every time you push a key.</p></li>
<p>If you are thinking of building an app with websockets, definitely check out <a href="">Pusher</a> which may save you the trouble of writing your own server.</p>
<h2>Extra Credit</h2>
<p>The server is broadcasting all movement events to the whole class. To display other student positions on your screen, you'll need to keep track of their usernames and x and y positions (probably
with a hash, where the keys are user names and the values are coordinates). Modify your handleMessage message to display multiple players, displaying a different image for your own sprite vs.
other users. </p>
<p>Also, we're not doing anything to ensure uniqueness of usernames, so make sure you pick a name that won't collide with anyone else's name.</p>
<h1>EXERCISE 9</h1>
<h2>Embedded Media (and Data Attributes)</h2>
<p>Let's add some sound effects to our game and take advantage of HTML5's data attributes to simplify our controls.</p>
<li><p>Add this list element to your <code>index.html</code> body:</p>
&lt;li&gt;&lt;a href=&quot;#&quot; data-soundname='bubble'&gt;Play Bubble&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#&quot; data-soundname='ray_gun'&gt;Play Ray Gun&lt;/a&gt;&lt;/li&gt;
<li><p>Use jQuery to bind the <code>&lt;a&gt;</code> tag's <code>click</code> event. You can figure out which soundname the user wants by inspecting the click event's data attribute, as below. I've
included the cross-browser version as well as the version provided for in the <a href="">HTML5 spec</a>, which only Chrome seems to support.</p>
<pre><code>$('a').click(function(evt) {
// this spec version is not as pretty but works across browsers
// the HTML5 spec provides a nicer API, but this version only seems to work in Chrome
// $('#'[0].play();
<p>Note that data attributes are different from micro-data, because they are not intended for external consumption. See <a href="">Dive Into HTML5</a> for more details about microdata.</p></li>
<li><p>Reload the page. Click each link to verify that you can read the <code>dataset</code> property and are getting the correct soundname.</p></li>
<li><p>Playing audio and video in HTML5 involves a lot of codec hassles. You usually have to provide your content in multiple formats. To make things simple, I've included these two
sound files in four different formats. Copy the sound files from the <code>media</code> directory to your project. If you are serving these files through a web server (and not viewing them via a file:// URL), you may have
to fiddle with your MIME settings because HTML5 will choke if your audio files aren't served with the proper MIME type. See <a href="">MIME Types</a> for details.</p>
<p>The following audio embed should work for most people, though. The spec says that the browser should pick the first listed source that it can play.</p>
<pre><code>&lt;div style=&quot;display:hidden&quot;&gt;
&lt;audio id=&quot;bubble&quot; preload&gt;
&lt;source src=&quot;media/bubble.ogg&quot;&gt;
&lt;source src=&quot;media/bubble.mp3&quot;&gt;
&lt;source src=&quot;media/bubble.wav&quot;&gt;
&lt;audio id=&quot;ray_gun&quot; preload&gt;
&lt;source src=&quot;media/ray_gun.ogg&quot;&gt;
&lt;source src=&quot;media/ray_gun.mp3&quot;&gt;
&lt;source src=&quot;media/ray_gun.wav&quot;&gt;
<p>I chose to embed these directly on the page so we could take advantage of the browser's content fallback selection. You can also create audio objects just like we did with Image objects earlier:</p>
<pre><code>var audio = new Audio;
audio.src = &quot;http://...&quot;;
<li><p>Reload your page, then try playing both sounds at the console:</p>
<li><p>Modify your anchor click event handler to automatically play the requested sound using the above technique.</p></li>
<li><p>To see what basic HTML5 audio controls look like, remove <code>display:hidden</code> from the <code>&lt;div&gt;</code> and add the <code>controls</code> attribute next to <code>preload</code>, then reload the page.</p></li>
<li><p>Video embedding works the same way. We need to provide multiple versions of video files to ensure compatibility across modern browsers. Copy the files <code></code>, <code>short.mp4</code>, <code>short.ogv</code>, and <code>short.webm</code> from the <code>media</code> directory to your project's <code>media</code> directory.</p>
<p>These files were created from a QuickTime movie using <code>ffmpeg2theora</code>, <code>ffmpeg</code> and <code>HandBrakeCLI</code>, using settings from <a href="">Dive Into HTML5</a>.</p></li>
<li><p>Add this to the bottom of your <code>index.html</code> page:</p>
<pre><code> &lt;video width=&quot;320&quot; height=&quot;240&quot; preload controls&gt;
&lt;source src=&quot;media/short.ogg&quot; type='video/ogg; codecs=&quot;theora, vorbis&quot;' /&gt;
&lt;source src=&quot;media/short.mp4&quot; /&gt;
&lt;source src=&quot;media/; /&gt;
<li><p>Reload the page. One of those four formats should display in your browser.</p>
<p>For a cool example of how to use the canvas to manipulate images from a video, check out <a href="">this demo</a>. There's also a good demonstration of using embedded media events to show a timer
in <a href="">this demo</a>.</p></li>
<h2>Extra Credit</h2>
<p>Use the <a href="">FlowPlayer</a> Flash-based video player as the ultimate fallback for this content (you'll need to embed a <code>&lt;object&gt;</code> tag after the <code>&lt;source&gt;</code> tags. The technique is explained
at <a href="">Video for Everybody</a>.</p>
<li><p>At the JavaScript console, type the following command:</p>
<pre>navigator.geolocation.getCurrentPosition(function(loc) { }, function(err) { console.error(err) })</pre></li>
<li><p>Inspect the location object in the console. If you lookup those coordinates in Google Maps you should get a result fairly close to the convention center! It's very easy to integrate this info
with Google Maps to show a map at the user's location, but unfortunately this can't be done from localhost due to Google Maps API authentication issues.
<a href="">This link</a> has a simple demo - be sure to view source on the page.</p>
<p>The first callback gets fired if the browser can guess its location. The second callback fires if it can't. For me, the second callback fired in
Safari when I ran it on a machine with an Ethernet connection.</p></li>
<h2>Extra Credit</h2>
<p>Check out <a href="">SimpleGeo</a> for some examples of other cool things you can do when you know a user's approximate location.</p>
<h2>Web Workers</h2>
<li><p>Examine the file <code>js/worker.js</code> and then copy it to your project. This is a simple brute-force algorithm to find all the factors of a given integer.</p></li>
<li><p>Reload the page, then type these lines at your JavaScript console:</p>
<pre>var worker = new Worker('js/worker.js');
worker.addEventListener('message', function(e) {;
<p><strong>If you are using Chrome</strong>, you will get a security exception if you are loading index.html as a file:// URI. You can reopen Chrome with a command-line flag to circumvent the exception, though.
This is what worked for me on OS X:</p>
<pre>open -n -a 'Google' --args --allow-file-access-from-files</pre>
<p>Or just use a different browser.</p></li>
<li><p>You should see the worker immediately post a response to the console. Try increasing the size of the number you pass to worker.postMessage until you get something that takes awhile to run (like 1,000,000). Notice
that your web page continues to be responsive even as this task runs in the background.</p></li>
<p>Here's a <a href="">more complicated webworker example</a>.</p>
<h2>Offline Apps</h2>
<li><p>Examine the <code>index.html</code> file in the <code>manifest</code> directory. This is a stripped-down version of the exercise 11 solution. Check out the <code>&lt;html&gt;</code> tag which now includes a reference to <code>demo.manifest.</code></p></li>
<li><p>Examine <code>demo.manifest</code>.</p></li>
<li><p>If you have a packet sniffer, start it sniffing on port 80. Otherwise, make sure your JavaScript console is recording network traffic.</p></li>
<li><p>Now visit <a href=""></a></p></li>
<li><p>In your packet sniffer or JavaScript console, note that all files are being downloaded, and note the MIME type of the <code>demo.manifest</code> file (<code>text/cache-manifest</code>).</p></li>
<li><p>Now reload the page. If all goes well, the only traffic you'll see moving along the wire is a request to check the demo.manifest file, which doesn't even get downloaded since it is unchanged
(because of the <code>304</code> HTTP response status code).</p>
<p>This is the same technique you can use to make an HTML5 app &quot;installable&quot; on a smart phone.</p></li>
<h2>Extra Credit</h2>
<p>Get <code>manifest/index.html</code> running on your dev machine. All you need to do is serve up the directory from the webserver (vs. from <code>file://</code>), and make sure the manifest file has the right MIME type. In Apache,
I added this directive to <code>httpd.conf</code>:</p>
<pre><code>AddType text/cache-manifest .manifest
<p>Here are some other &quot;HTML5-ish&quot; features that you should be aware of that didn't fit into this tutorial or are too bleeding-edge to be used reliably:</p>
<li><p><a href="">Microdata</a></p></li>
<li><p><a href="">WebGL</a></p></li>
<li><p><a href="">HTML5 History API</a></p></li>
<li><p><a href="">Alternatives to websockets</a></p></li>
<li><p><a href="">Polyfills to get HTML5 features working across browsers</a> and also <a href="">Uber</a></p></li>
<li><p><a href="">Various CSS3 capabilities like rounded corners, 2D transforms, etc.</a></p></li>
<h1>USEFUL URLs</h1>
<li><p><a href="">Dive Into HTML5</a></p></li>
<li><p><a href="">HTML5 Doctor</a></p></li>
<li><p><a href="">HTML5 Demos</a></p></li>
<li><p><a href="">HTML5 Rocks</a></p></li>
<li><p><a href="">Mozilla Canvas Tutorials</a></p></li>
<li><p><a href="">WHATWG Living Standard</a></p></li>
<p>Thanks to <a href="">Jeff Casimir</a> for helping me organize this material, and to <a href="">Mark Pilgrim</a> for writing Dive Into HTML5 which was a big help. Any mistakes are my own of course!</p>
<h1>KEEP IN TOUCH</h1>
<p>Thanks for coming to my tutorial. I'm <a href=""></a> or <a href="">@subelsky</a>
on Twitter. I love talking about HTML5, so email me if you have
questions or want to discuss interesting challenges.</p>
<p>Most of the techniques I discuss in this tutorial I learned building
an HTML5-powered game for programmers named EXP. We're accepting beta
testers now, visit <a href=""></a> to signup!</p>
Jump to Line
Something went wrong with that request. Please try again.