Tag Archives: JavaScript

Sketching with HTML5 Canvas and “Brush Images”

In a previous post on capturing user signatures in mobile applications, I explored how you capture user input from mouse or touch events and visualize that in a HTML5 Canvas.  Inspired by activities with my daughter, I decided to take this signature capture component and make it a bit more fun & exciting.   My daughter and I often draw and sketch together… whether its a magnetic sketching toy, doodling on the iPad, or using a crayon and a placemat at a local pizza joint, there is always something to draw. (Note: I never said I was actually good at drawing.)

Olivia & the iPad

You can take that exact same signature capture example, make the canvas bigger, and then combine it with a tablet and a stylus, and you’ve got a decent sketching application.   However, after doodling a bit you will quickly notice that your sketches leave something to be desired.   When you are drawing on a canvas using moveTo(x,y) and lineTo(x,y), you are somewhat limited in what you can do. You have lines which can have consisten thickness, color, and opacity. You can adjust these, however in the end, they are only lines.

If you switch your approach away from moveTo and lineTo, then things can get interesting with a minimal amount of changes. You can use images to create “brushes” for drawing strokes in a HTML5 canvas element and add a lot of style and depth to your sketched content.  This is an approach that I’ve adapted to JavaScript from some OpenGL drawing applications that I’ve worked on in the past.  Take a look at the video below to get an idea what I mean.

Examining the sketches side by side, it is easy to see the difference that this makes.   The variances in stroke thickness, opacity & angle add depth and style, and provide the appearance of drawing with a magic marker.

Sketches Side By Side

It’s hard to see the subtleties in this image, so feel free to try out the apps on your own using an iPad or in a HTML5 Canvas-capable browser:

Just click/touch and drag in the gray rectangle area to start drawing.

Now, let’s examine how it all works.   Both approaches use basic drawing techniques within the HTML5 Canvas element.   If you aren’t familiar with the HTML5 Canvas, you can quickly get up to speed from the tutorials from Mozilla.

moveTo, lineTo

The first technique uses the canvas’s drawing context moveTo(x,y) and lineTo(x,y) to draw line segments that correspond to the mouse/touch coordinates.   Think of this as playing “connect the dots” and drawing a solid line between two points.

The code for this approach will look something like the following:

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

context.beginPath();
context.moveTo(a.x, a.y);
context.lineTo(b.x, b.y);
context.lineTo(c.x, c.y);
context.closePath();
context.stroke();

The sample output will be a line from point A, to point B, to point C:

lineTo(x,y) Stroke Sample

Brush Images

The technique for using brush images is identical in concept to the previous example – you are drawing a line from point A to point B.  However, rather than using the built-in drawing APIs, you are programmatically repeating an image (the brush) from point A to point B.

First, take a look at the brush image shown below at 400% of the actual scale.  It is a simple image that is a diagonal shape that is thicker and more opaque on the left side.   By itself, this will just be a mark on the canvas.

Brush Image (400% scale)

When you repeat this image from point A to point B, you will get a “solid” line.  However the opacity and thickness will vary depending upon the angle of the stroke.   Take a look at the sample below (approximated, and zoomed).

Brush Stroke Sample (simulated)

The question is… how do you actually do this in JavaScript code?

First, create an Image instance to be used as the brush source.

brush = new Image();
brush.src = 'assets/brush2.png';

Once the image is loaded, the image can be drawn into the canvas’ context using the drawImage() function. The trick here is that you will need to use some trigonometry to determine how to repeat the image. In this case, you can calculate the angle and distance from the start point to the end point. Then, repeat the image based on that distance and angle.

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

var halfBrushW = brush.width/2;
var halfBrushH = brush.height/2;

var start = { x:0, y:0 };
var end = { x:200, y:200 };

var distance = parseInt( Trig.distanceBetween2Points( start, end ) );
var angle = Trig.angleBetween2Points( start, end );

var x,y;

for ( var z=0; (z<=distance || z==0); z++ ) {
	x = start.x + (Math.sin(angle) * z) - halfBrushW;
	y = start.y + (Math.cos(angle) * z) - halfBrushH;
	context.drawImage(this.brush, x, y);
}

For the trigonometry functions, I have a simple utility class to calculate the distance between two points, and the angle between two points. This is all based upon the good old Pythagorean theorem.

var Trig = {
	distanceBetween2Points: function ( point1, point2 ) {

		var dx = point2.x - point1.x;
		var dy = point2.y - point1.y;
		return Math.sqrt( Math.pow( dx, 2 ) + Math.pow( dy, 2 ) );
	},

	angleBetween2Points: function ( point1, point2 ) {

		var dx = point2.x - point1.x;
		var dy = point2.y - point1.y;
		return Math.atan2( dx, dy );
	}
}

The full source for both of these examples is available on github at:

This example uses the twitter bootstrap UI framework, jQuery, and Modernizr.  Both the lineTo.html and brush.html apps use the exact same code, which just uses a separate rendering function based upon the use case.    Feel free to try out the apps on your own using an iPad or in a HTML5 Canvas-capable browser:

Just click/touch and drag in the gray rectangle area to start drawing.

Stylistic Sketchy
Stylistic Sketchy - Click to Get Started

Capturing User Signatures in Mobile Applications

One growing trend that I have seen in mobile & tablet applications is the creation of tools that enable your workforce to perform their job better. This can be in the case of mobile data retrieval, streamlined sales process with apps for door-to-door sales, mobile business process efficiency, etc…

One of the topics that comes up is how do you capture a signature and store it within your application? This might be for validation that the signer is who they say they are, or for legal/contractual reasons. Imagine a few scenarios:

  • Your cable TV can’t be installed until you sign the digital form on the installation tech’s tablet device
  • You agree to purchase a service from a sales person (door to door, or in-store kiosk) – your signature is required to make this legally binding.
  • Your signature is required to accept an agreement before confidential data is presented to you.

These are just a few random scenarios, I’m sure there are many more.   In this post, I will focus on 2 (yes, I said two) cross-platform solutions to handle this task – one built with Adobe Flex & AIR, and one built with HTML5 Canvas & PhoneGap.  

Source for both solutions is available at: https://github.com/triceam/Mobile-Signature-Capture

Watch the video below to see this in action, then we’ll dig into the code that makes it work.

The basic flow of the application is that you enter an email address, sign the interface, then click the green “check” button to submit to the signature to a ColdFusion server.  The server then sends a multi-part email to the email address that you provided, containing text elements as well as the signature that was just captured.

If you’d like to jump straight to specific code portions, use the links below:



The Server Solution

Let’s first examine the server component of the sample application.   The server side is powered by ColdFusion. There’s just a single CFC that is utilized by both the Flex/AIR and HTML/PhoneGap front-end applications.   The CFC exposes a single service that accepts two parameters: the email address, and a base-64 encoded string of the captured image data.

<cffunction name="submitSignature" access="remote" returntype="boolean">
    <cfargument name="email" type="string" required="yes">
    <cfargument name="signature" type="string" required="yes">
    
    <cfmail SUBJECT ="Signature"
        FROM="#noReplyAddress#"
        TO="#email#"
        username="#emailLoginUsername#"
        password="#emailLoginPassword#"
        server="#mailServer#" 
        type="HTML" >
        
        <p>This completes the form transaction for <strong>#email#</strong>.</p>
        
        <p>You may view your signature below:</p>
        <p><img src="cid:signature" /></p>
        
        <p>Thank you for your participation.</p>
        
        <cfmailparam
            file="signature"
            content="#toBinary( signature )#"
            contentid="signature"
            disposition="inline" />
    
    </cfmail>

    <cfreturn true />
</cffunction>

Note: I used base-64 encoded image data so that it can be a single server component for both user interfaces. In Flex/AIR you can also serialize the data as a binary byte array, however binary serialization isn’t quite as easy with HTML/JS… read on to learn more.


The Flex/AIR Solution

The main user interface for the Flex/AIR solution is a simple UI with some form elements. In that UI there is an instance of my SignatureCapture user interface component. This is a basic component that is built on top of UIComponent (the base class for all Flex visual components), which encapsulates all logic for capturing the user signature. The component captures input based on mouse events (single touch events are handled as mouse events in air). The mouse input is then used to manipulate the graphics content of the component using the drawing API. I like to think of the drawing API as a language around the childhood game “connect the dots”. In this case, you are just drawing lines from one point to another.

When the form is submitted, the graphical content is converted to a base-64 encoded string using the Flex ImageSnapshot class/API, before passing it to the server.

You can check out a browser-based Flex version of this in action at http://tricedesigns.com/portfolio/sigCaptureFlex/ – Just enter a valid email address and use your mouse to sign within the signature area. When this is submitted, it will send an email to you containing the signature.

You can check out the SignatureCapture component code below, or check out the full project at https://github.com/triceam/Mobile-Signature-Capture/tree/master/flex%20client. This class will also work in desktop AIR or browser/based Flex applications. The main application workflow and UI is contained with Main.mxml.

package
{
	import flash.display.DisplayObject;
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	
	import mx.core.UIComponent;
	import mx.graphics.ImageSnapshot;
	import mx.managers.IFocusManagerComponent;
	
	import spark.primitives.Graphic;
	
	public class SignatureCapture extends UIComponent
	{
		private var captureMask : Sprite;
		private var drawSurface : UIComponent;
		private var lastMousePosition : Point;
		
		private var backgroundColor : int = 0xEEEEEE;
		private var borderColor : int = 0x888888;
		private var borderSize : int = 2;
		private var cornerRadius :int = 25;
		private var strokeColor : int = 0;
		private var strokeSize : int = 2;
		
		public function SignatureCapture()
		{
			lastMousePosition = new Point();
			super();
		}
		
		override protected function createChildren():void 
		{
			super.createChildren();
			
			captureMask = new Sprite();
			drawSurface = new UIComponent();
			this.mask = captureMask;
			addChild( drawSurface );
			addChild( captureMask );
			
			this.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
		}
		
		protected function onMouseDown( event : MouseEvent ) : void
		{
			lastMousePosition = globalToLocal( new Point( stage.mouseX, stage.mouseY ) );
			stage.addEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
			stage.addEventListener( MouseEvent.MOUSE_UP, onMouseUp );
		}
		
		protected function onMouseMove( event : MouseEvent ) : void
		{
			updateSegment();
		}
		
		protected function onMouseUp( event : MouseEvent ) : void
		{
			updateSegment();
			stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
			stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp );
		}
		
		protected function updateSegment() : void
		{
			var nextMousePosition : Point = globalToLocal( new Point( stage.mouseX, stage.mouseY ) );
			renderSegment( lastMousePosition, nextMousePosition );
			lastMousePosition = nextMousePosition;
		}
		
		
		public function clear() : void
		{
			drawSurface.graphics.clear();
		}
		
		override public function toString() : String
		{
			var snapshot : ImageSnapshot = ImageSnapshot.captureImage( drawSurface );
			return ImageSnapshot.encodeImageAsBase64( snapshot );
		}
		
		override protected function updateDisplayList(w:Number, h:Number):void
		{
			super.updateDisplayList(w,h);
			
			drawSurface.width = w;
			drawSurface.height = h;
			
			var g : Graphics = this.graphics;
			
			//draw rectangle for mouse hit area
			g.clear();
			g.lineStyle( borderSize, borderColor, 1, true );
			g.beginFill( backgroundColor, 1 );
			g.drawRoundRect( 0,0,w,h, cornerRadius, cornerRadius );
		
			
			//fill mask
			g.clear();
			g = captureMask.graphics;
			g.beginFill( 0, 1 );
			g.drawRoundRect( 0,0,w,h, cornerRadius, cornerRadius );
		}
		
		protected function renderSegment( from : Point, to : Point ) : void
		{
			var g : Graphics = drawSurface.graphics;
			g.lineStyle( strokeSize, strokeColor, 1 );
			g.moveTo( from.x, from.y );
			g.lineTo( to.x, to.y );
		}
	}
}



The HTML5/PhoneGap Solution

The main user interface for the HTML5/PhoneGap solution is also a simple UI with some form elements. In that UI there is a Canvas element that is used to render the signature. I created a SignatureCapture JavaScript class that encapsulates all logic for capturing the user signature. In browsers that support touch events (mobile browsers), this is based on the touchstart, touchmove and touchend events. In browsers that don’t support touch (aka desktop browsers), the signature input is based on mousedown, mousemove and mouseup events. The component captures input based on touch or mouse events, and that input is used to manipulate the graphics content of the Canvas tag instance. The canvas tag also supports a drawing API that is similar to the ActionScript drawing API. To read up on Canvas programmatic drawing basics, check out the tutorials at http://www.adobe.com/devnet/html5/html5-canvas.html

When the form is submitted, the graphical content is converted to a base-64 encoded string using the Canvas’s toDataURL() method. The toDataURL() method returns a base-64 encoded string value of the image content, prefixed with “data:image/png,”. Since I’ll be passing this back to the server, I don’t need this prefix, so it is stripped, then sent to the server for content within the email.

You can check out a browser-based version of this using the HTML5 Canvas in action at http://tricedesigns.com/portfolio/sigCapture/ – Again, just enter a valid email address and use your mouse to sign within the signature area. When this is submitted, it will send an email to you containing the signature. However, this example requires that your browser supports the HTML5 Canvas tag.

You can check out the SignatureCapture code below, or check out the full project at https://github.com/triceam/Mobile-Signature-Capture/tree/master/html%20client. This class will also work in desktop browser applications that support the HTML5 canvas. I used Modernizr to determine whether touch events are supported within the client container (PhoneGap or desktop browser). The main application workflow is within application.js.

Also a note for Android users, the Canvas toDataURL() method does not work in Android versions earlier than 3.0. However, you can implement your own toDataURL() method for use in older OS versions using the technique in this link: http://jimdoescode.blogspot.com/2011/11/trials-and-tribulations-with-html5.html (I did not update this example to support older Android OS versions.)

function SignatureCapture( canvasID ) {
	this.touchSupported = Modernizr.touch;
	this.canvasID = canvasID;
	this.canvas = $("#"+canvasID);
	this.context = this.canvas.get(0).getContext("2d");	
	this.context.strokeStyle = "#000000";
	this.context.lineWidth = 1;
	this.lastMousePoint = {x:0, y:0};
	
	this.canvas[0].width = this.canvas.parent().innerWidth();
    
	if (this.touchSupported) {
		this.mouseDownEvent = "touchstart";
		this.mouseMoveEvent = "touchmove";
		this.mouseUpEvent = "touchend";
	}
	else {
		this.mouseDownEvent = "mousedown";
		this.mouseMoveEvent = "mousemove";
		this.mouseUpEvent = "mouseup";
	}
	
	this.canvas.bind( this.mouseDownEvent, this.onCanvasMouseDown() );
}

SignatureCapture.prototype.onCanvasMouseDown = function () {
	var self = this;
	return function(event) {
		self.mouseMoveHandler = self.onCanvasMouseMove()
		self.mouseUpHandler = self.onCanvasMouseUp()

		$(document).bind( self.mouseMoveEvent, self.mouseMoveHandler );
		$(document).bind( self.mouseUpEvent, self.mouseUpHandler );
		
		self.updateMousePosition( event );
		self.updateCanvas( event );
	}
}

SignatureCapture.prototype.onCanvasMouseMove = function () {
	var self = this;
	return function(event) {

		self.updateCanvas( event );
     	event.preventDefault();
    	return false;
	}
}

SignatureCapture.prototype.onCanvasMouseUp = function (event) {
	var self = this;
	return function(event) {

		$(document).unbind( self.mouseMoveEvent, self.mouseMoveHandler );
		$(document).unbind( self.mouseUpEvent, self.mouseUpHandler );
		
		self.mouseMoveHandler = null;
		self.mouseUpHandler = null;
	}
}

SignatureCapture.prototype.updateMousePosition = function (event) {
 	var target;
	if (this.touchSupported) {
		target = event.originalEvent.touches[0]
	}
	else {
		target = event;
	}

	var offset = this.canvas.offset();
	this.lastMousePoint.x = target.pageX - offset.left;
	this.lastMousePoint.y = target.pageY - offset.top;

}

SignatureCapture.prototype.updateCanvas = function (event) {

	this.context.beginPath();
	this.context.moveTo( this.lastMousePoint.x, this.lastMousePoint.y );
	this.updateMousePosition( event );
	this.context.lineTo( this.lastMousePoint.x, this.lastMousePoint.y );
	this.context.stroke();
}

SignatureCapture.prototype.toString = function () {

	var dataString = this.canvas.get(0).toDataURL("image/png");
	var index = dataString.indexOf( "," )+1;
	dataString = dataString.substring( index );
	
	return dataString;
}

SignatureCapture.prototype.clear = function () {

	var c = this.canvas[0];
	this.context.clearRect( 0, 0, c.width, c.height );
}

Source for the ColdFusion server, as well as Flex/AIR and HTML5/PhoneGap clients is available at: https://github.com/triceam/Mobile-Signature-Capture

Toying with Realtime Data & Web Sockets

Recently I was acting as a “second set of eyes” to help out fellow Adobe Evangelist Kevin Hoyt track down a quirk with a websockets example that he was putting together. Kevin has a great writeup to familiarize yourself with web sockets & streaming communication that I highly recommend checking out.

While working with Kevin’s code, I started tinkering… “what if I change this, what if I tweak that?” Next thing you know, I put together a sample scenario showing subscription-based realtime data streaming to multiple web clients using web sockets. Check out the video below to see it in action.

You are seeing 9 separate browser instances getting realtime push-based updates from a local server using web sockets. When the browser loads, the html-based client makes a web socket connection, then requests all symbols from the server. The server then sends the stock symbol definitions back to the client and displays them within the HTML user interface. From there, the user can click on a stock symbol to subscribe to updates for that particular symbol. DISCLAIMER: All that data is randomly generated!

I put together this example for experimentation, but also to highlight a few technical scenarios for HTML-based applications. Specifically:

  • Realtime/push data in HTML-based apps
  • Per-client subscriptions for realtime data
  • Multi-series realtime data visualization in HTML-based apps

The server is an AIR app started by Kevin, based on the web sockets draft protocol. It is written in JavaScript, and the client is a HTML page to be viewed in the browser.

If you don’t feel like reading the full web sockets protocol reference, you can get a great overview from websocket.org or Wikipedia.

One thing to keep in mind is that web sockets are not widely supported in all browsers yet. There is a great reference matrix for web socket support from caniuse.com:

If you still aren’t sure if your browser supports web sockets, you can also check simply by visiting websocketstest.com/. If you want to test for web socket support within your own applications, you can easily check for support using Modernizr. Note: I didn’t add the Modernizr test in this example… I only tested in Chrome on OSX.

OK, now back to the sample application. All of the source code for this example is available on github at: https://github.com/triceam/Websocket-Streaming-Example.  To run it yourself, you first have to launch the server. You can do this on the command line by invoking ADL (part of the AIR SDK):

cd "/Applications/Adobe Flash Builder 4.6/sdks/4.6.0/bin"
./adl ~/Documents/dev/Websocket-Streaming-Example/server/application.xml

You’ll know the server is started b/c an air window will popup (you can ignore this, just don’t close it), and you will start seeing feed updates in the console output.

Once the server is running, open “client/client.html” in your browser. It will connect to the local server, and then request the list of symbols. If you click on a symbol, it will subscribe to that feed. Just click on the symbol name again to unsubscribe. You’ll know the feed is subscribed b/c the symbol will show up in a color (matching the corresponding feed on the chart). Again, let me reiterate that I only tested this in Chrome.

You can open up numerous client instances, and all will receive the same updates in real time for each subscribed stock symbol.

The “meat” of code for the server starts in server/scripts/server/server.js. Basically, the server loads a configuration file for the socket server, then creates a ConnectionManager and DataFeed (both of these are custom JS classes). The ConnectionManager class encapsulates all logic around socket connections. This includes managing the ServerSocket as well as all client socket instances and events. The DataFeed class handles data within the app. First, it generates random data, then sets up an interval to generate random data updates. For every data update, the ConnectionManager instance’s dispatch() method is invoked to send updates to all subscribed clients. Rather than trying to put lots of code snippets inline in this post (which would just be more confusing), check out the full source at: https://github.com/triceam/Websocket-Streaming-Example/tree/master/server

The client code all starts in client.html, with the application logic inside of client/scripts/client.js. Once the client interface loads, it connects to the web socket and adds the appropriate event handlers. Once subscribed to a data feed, realtime data will be returned via the web socket instance, transformed slightly to fit the data visualization structure, then rendered in an HTML canvas using the RGraph data visualization library. RGraph is free to get started with, however if you want to deploy a production app with it, you’ll need a license. You’ll notice that each feed updates independently, based upon the client subscriptions. Note: The data visualization is not temporally aligned… if you want the updates in time-sequence, there is a litte bit more work involved in the client-side data transformation.

Again, rather than trying to put lots of confusing code snippets inline in this post, check out the full client side source at: https://github.com/triceam/Websocket-Streaming-Example/tree/master/client

This example is intended to get your minds rolling with the concepts; it is not *yet* an all-encompassing enterprise solution. You can expect to see a few more data push scenarios here in the near future, based on different enterprise server technologies.

Enjoy!

“Almost Native” – iOS Apps Powered By iAd.js and PhoneGap

One of the benefits of using a cross-platform development technology that I mentioned in my last post is that cross-platform development technologies can be solution accelerator platforms.  I mean “solution accelerator” because it can be easier and faster to develop an application using cross-platform technologies (web development skills) rather than native development, even if you are only targeting a single platform.  This isn’t the case for every solution, but certainly can be for many scenarios.

Here’s a sample application that I put together while exploring the PhoneGap product offering.   For those that weren’t aware, Adobe has entered an agreement to acquire Nitobi, the makers of PhoneGap, and PhoneGap is an HTML5 app platform that allows you to author native applications with web technologies and get access to APIs and app stores.

This application consumes data from the rottentomatoes.com API, and is built entirely in JavaScript.  If you haven’t seen it before, RottenTomatoes.com is a site for searching movie ratings & information.  Check out the video below, and then we’ll examine how the application was built (be sure to check out the CoverFlow runtime performance at 34 seconds).

The entire codebase that I had to write for this application is 289 lines of HTML & JavaScript (including whitespace), all in a single file, and was built using iAd.js, xui.js, and of course, PhoneGap.   The first thing you might be wondering is “what is iAd.js?” or, if you know about the iAd platform, you might be thinking “um… isn’t that just for advertisements?”. Everyone who uses an iOS device has probably encountered an iAd at least once, whether they know it or not.  The iAd program is an advertising platform for iOS devices that enables advertisers to create rich, engaging, & interactive advertisements.    Interestingly, all iAds are built on top of a JavaScript framework built by Apple.

It just so happens that the iAd.js JavaScript framework can also be used outside of the advertising context (only on iOS devices – I tried Android as well, but not all of the elements worked correctly).   Not all features of the iAd framework will work outside of the advertising context, such as purchasing or interacting with the iTunes store.  However the iAd.js framework will provide you with user interface elements that look nearly identical to native iOS components.  This includes view navigators, table views (with groups, disclosure indicators, etc…), carousel views, cover flow views, wheel pickers, progress bars, sliders, switches, buttons, and much more.   In addition, the interactions and animations for these components are highly optimized for mobile safari, and interaction with these elements feels very, very close to native.  There are a few minor things here and there, but overall it is not necessarily easy to distinguish the difference.

Note: I have not submitted any apps using this technique to Apple’s app store.  However, I have heard from others that Apple has accepted their applications which are built using this approach.   

While these components are instantiated in the browser and created via HTML & JavaScript, the programming model of iAd components is very similar to native iOS development.  You still have the usage of protocols (interfaces), and controller & delegate patterns.  For example, using the iAd.TableView ui component, still requires use of the TableViewDataSource and TableViewDelegate protocols (just implemented in JavaScript). Familiarity with native iOS development will definitely be a big plus if you are using the iAd.js framework.

I used xui.js to simplify the syntax for XMLHttpRequest for asynch data loading, and of course, PhoneGap is used for the application container, as well as any native OS interaction if you wanted any.  The source code could certainly be broken up into multiple controllers or separate files for maintainability, however I was just going for a “quick & dirty” example.

Basically, you just need to include the iAd JavaScript and CSS files, then build your application as you would any other HTML/JS PhoneGap experience.   You can download the iAd framework from here if you are a registered Apple iOS developer.   In theory, this isn’t that much different from using jQuery mobile components, however these have better performance on iOS, and have a more-native feel.

Thanks to pixelranger and merhl from Universal Mind for showing me a while back that you could use this approach!

Full source code below:

<html>

    <head>
        <title>RottenTomatoes</title>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <meta name="apple-mobile-web-app-capable" content="yes">

        <!-- Import iAd Assets -->
        <link rel="stylesheet" href="iAd/iAd.css">
        <script type="text/javascript" src="iAd/iAd.js" charset="utf-8"></script>

        <!-- Import other libraries -->
        <script type="text/javascript" charset="utf-8" src="libs/xui-2.2.0.js"></script>
        <script type="text/javascript" charset="utf-8" src="libs/phonegap-1.1.0.js"></script>

        <style type="text/css" media="screen">

            body {
                margin: 0;
                overflow: hidden;
            }

            .ad-flow-view {
                position: relative;
                left: 0px;
                width: 300px;
                height: 380px;
                -webkit-perspective: 400;
            }

            .ad-flow-view .ad-flow-view-camera {
                position: absolute;
                left: 50%;
                width: 10px;
                height: 10px;
            }

            .ad-flow-view .ad-flow-view-cell {
                position: absolute;
                top: 30px;
                left: -125px;
                width: 250px;
                height: 350px;
            }

            .ad-flow-view .ad-flow-view-cell img {
                pointer-events: none;
                width: 180px;
                height: 267px;
                -webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0.8, transparent), to(rgba(255,255,255,0.35)));
            }

        </style>

        <script type="text/javascript" charset="utf-8">

            var API_KEY = "put your api key here";

            var sampleData;

            /* ==================== Controller ==================== */

            var controller = {
                data : []
            };

            controller.init = function () {
                var url = "http://api.rottentomatoes.com/api/public/v1.0/lists/movies/box_office.json?limit=15&country=us&apikey=" + API_KEY;
                console.log( url );

                x$().xhr( url, {
                         async: true,
                         callback: function() {

                            var trimmedResponse = this.responseText.replace(/^\s\s*/, '').replace(/\s\s*$/, '');

                            if ( trimmedResponse.length > 0 )
                            {
                                sampleData = eval( "(" + trimmedResponse + ")" );

                                if ( sampleData )
                                {
                                    controller.data =  sampleData.movies ;
                                    controller.table.reloadData();
                                }
                            }

                         }
                    });

                this.navigation = new iAd.NavigationController();

                this.navigation.delegate = this;
                iAd.RootView.sharedRoot.addSubview(this.navigation.view);

                this.navigation.navigationBar.barStyle = iAd.NavigationBar.STYLE_BLACK;
                this.navigation.pushViewControllerAnimated(this.createTableViewController(), false);
            };

            controller.handleEvent = function (event) {
                if ( event.type == iAd.View.TOUCH_UP_INSIDE_EVENT ) {
                    var viewController = this.createFlowViewController();
                    this.navigation.pushViewControllerAnimated(viewController, true);
                    viewController.view.hidden = false;
                }
            };

            controller.displayDetails = function (index) {
                var item = this.data[index];
                var viewController = this.createDetailViewController(item);
                this.navigation.pushViewControllerAnimated(viewController, true);
                viewController.view.hidden = false;
            }

            controller.createTableViewController = function (index) {

                if ( this.viewController == null ) {
                    this.viewController = new iAd.ViewController();
                    this.viewController.title = "Rotten Tomatoes";

                    this.viewController.navigationItem.rightBarButtonItem = new iAd.BarButtonItem();
                    this.viewController.navigationItem.rightBarButtonItem.style = iAd.BarButtonItem.STYLE_DONE;
                    this.viewController.navigationItem.rightBarButtonItem.title = 'Flow';
                    this.viewController.navigationItem.rightBarButtonItem.addEventListener( iAd.View.TOUCH_UP_INSIDE_EVENT, this, false );

                    // create a TableView
                    this.table = new iAd.TableView();
                    this.table.tableStyle = iAd.TableView.STYLE_GROUPED;
                    this.table.delegate = this;
                    this.table.dataSource = this;
                    this.table.size = new iAd.Size(window.innerWidth, window.innerHeight-46);

                    this.viewController.view.addSubview(this.table);
                }

                return this.viewController;
            };

            controller.createFlowViewController = function (index) {
                var viewController = new iAd.ViewController();
                viewController.title = "Flow";

                var flowView = new iAd.FlowView();
                flowView.dataSource = this;
                flowView.delegate = this;
                flowView.layer.style.backgroundColor = "#FFFFFF";

                // customize flow view
                flowView.sidePadding = 150;
                flowView.cellRotation = 65;
                flowView.cellGap = 50;
                flowView.dragMultiplier = 1.0;
                flowView.sideZOffset = -200;

                // load the data
                flowView.reloadData();
                flowView.centerCamera();
                this.flowView = flowView;

                viewController.view.addSubview(flowView);

                viewController.view.hidden = true;
                this.flowSelectedIndex = 0;
                return viewController;
            };

            controller.flowViewNumberOfCells = function(flowView) {
                return this.data.length;
            };

            controller.flowViewCellAtIndex = function(flowView, index) {
                var cell = document.createElement('div');
                cell.appendChild(document.createElement('img')).src = this.data[index].posters.detailed;
                return cell;
            };

            /* ==================== iAd.FlowViewDelegate Protocol ==================== */

            controller.flowViewDidTapFrontCell = function (flowView, index) {
                console.log('flowViewDidTapFrontCell ' + index + ", " + this.flowSelectedIndex);
                if ( this.flowSelectedIndex == index )
                    this.displayDetails( index );
            };

            controller.flowViewDidSelectCell = function (flowView, index) {
                console.log('flowViewDidSelectCell ' + index);
                this.flowSelectedIndex = index;
            };

            controller.flowViewDidBeginSwipe = function (flowView) {
                console.log('flowViewDidBeginSwipe');
            };

            controller.flowViewDidEndSwipe = function (flowView) {
                console.log('flowViewDidEndSwipe');
            };

            controller.createDetailViewController = function (item) {
                var viewController = new iAd.ViewController();
                viewController.title = item.title;

                var scrollView = new iAd.ScrollView();
                scrollView.userInteractionEnabled = true;
                scrollView.size = new iAd.Size(window.innerWidth, window.innerHeight-46);
                scrollView.horizontalScrollEnabled = false;
                scrollView.layer.style.backgroundColor = "#FFFFFF";

                var imageView = scrollView.addSubview(new iAd.ImageView());
                imageView.image = iAd.Image.imageForURL( item.posters.profile );
                imageView.position = new iAd.Point(10, 10);
                imageView.size = new iAd.Size(120, 178);

                var synopsisLabel = scrollView.addSubview(new iAd.Label());

                synopsisLabel.text = item.synopsis;

                synopsisLabel.numberOfLines = 0;

                synopsisLabel.size = new iAd.Size(this.navigation.view.size.width-150, 800	);
                synopsisLabel.position = new iAd.Point(140, 10);
                synopsisLabel.autoresizingMask = iAd.View.AUTORESIZING_FLEXIBLE_WIDTH | iAd.View.AUTORESIZING_FLEXIBLE_HEIGHT;
                synopsisLabel.verticalAlignment = iAd.Label.VERTICAL_ALIGNMENT_TOP;

                viewController.view.addSubview( scrollView );
                viewController.view.hidden = true;
                return viewController;
            };

            /* ==================== TableViewDataSource Protocol ==================== */

            controller.numberOfSectionsInTableView = function (tableView) {
                return 1;
            };

            controller.tableViewNumberOfRowsInSection = function (tableView, section) {
                return this.data.length;
            };

            controller.tableViewCellForRowAtPath = function (tableView, path) {
                var cell = new iAd.TableViewCell();
                cell.text = this.data[path.row].title;
                cell.detailedText = 'title';
                cell.accessoryType = iAd.TableViewCell.ACCESSORY_DISCLOSURE_INDICATOR;
                cell.selectionStyle = iAd.TableViewCell.SELECTION_STYLE_BLUE;
                return cell;
            };

            controller.tableViewTitleForHeaderInSection = function (tableView, section) {
                return "Box Office Movies";
            };

            controller.tableViewTitleForFooterInSection = function (tableView, section) {
                return "Powered by RottenTomatoes.com";
            };

            /* ==================== TableViewDelegate Protocol ==================== */

            controller.tableViewDidSelectRowAtPath = function (theTableView, path) {
                this.displayDetails(path.row);
            };

            controller.tableViewDidSelectAccessoryForRowAtPath = function (theTableView, path) {
            };

            /* ==================== iAd.NavigationViewDelegate Protocol ==================== */

            controller.navigationControllerWillShowViewControllerAnimated = function (theNavigationController, viewController, animated) {
            };

            controller.navigationControllerDidShowViewControllerAnimated = function (theNavigationController, viewController, animated) {
            };

            /* ==================== Init ==================== */

            function init () {
                console.log( "init" );
                controller.init();
            }

            window.addEventListener('load', init, false);

        </script>

    </head>

    <body></body>

</html>