Archive for category Showcase

ExtensionFM: A case study on a sexy app, turn extension

Editor's note: Dan Kantor is the CEO behind the awesome ExtensionFM project. It really pushes the boundaries on what the Web can do, so I asked Dan to give us a mini case study on the project. What follows is his words on the matter. Thanks for taking the time Dan!

Dion recently posted about the march to a more client-centric web. The post hit home with me as the founder of a web-based application that is as much 'app-like' as it is 'web-like'. The promise of the web, and HTML5 in particular, is that we will finally reach the write-once, run-anywhere dream we have all been in search of that neither Java nor Flash could fully deliver. Dion poses this question (and answer) towards the end of the post - 'As a developer, do you want to port experiences between incredibly varied platforms such as Web, iPhone, Android, WinPho 7, RIM, Kindle SDK, [insert many others!]? No.'. I don't think many developers would argue with Dion's answer. The real question is - 'Are we there yet?'

My company is developing a client-centric web-based music player that cuts no corners in its attempt to look, feel and run like a native application. We threw progressive enhancement out the door, pushed the pedal to the HTML5 metal and created something that most people would be hard-pressed to believe is not a native application if they didn't already see it running inside the browser. However, there is a catch. Our application only runs inside Chrome. This was of course by design. We built it as a Chrome extension. We didn't choose to do this because we wanted to restrict it to Chrome. We did this because a large part of the functionality of our application needs the extra APIs and permissions an extension gives you. A nice side-effect of that is we know 100% of our users are using an HTML5 capable browser. Chrome extensions are 100% HTML(5), CSS(3) and Javascript. Extensions provide extra Javascript APIs that give you access to functionality that you otherwise would not have running as a website such as removing security restrictions on cross-domain xhr and requiring permission to display desktop notifications. Of the thousands of lines of code in our extension, only 5% of it is Chrome specific. In theory, this should make porting to other browsers and OS's easy. Let's see what it looks like in practice.

A few months ago, we decided to see what our application would look like on the iPad. After just four hours, we had a working prototype of our music player that looked and felt native but more importantly looked and felt exactly like our Chrome extension. This was possible because Chrome and Safari share the same rendering engine - Webkit. Mobile Safari has already implemented many of the HTML5 features we needed such as HTML5 Audio, Web Database and Local Storage and many of the CSS3 features we needed such as Background Gradients, Rounded Corners and Box-Shadows. We were able to re-use most of our Javascript code and since we used a lot of CSS in place of images, we were able to resize elements with ease. But a prototype is just a prototype. Could our web-application actually be a viable shipping product on the iPad?

At this point, the answer is no. Mobile Safari has two quirks that seriously hamper its ability to act as a web platform. The first is it's lack of fixed positioning. If there was one feature that defined native applications vs. web applications it would be that native applications almost always frame content with a top and bottom layer of buttons or displays. Think about iPhone apps. Almost every one of them (minus games) has a navigation layer on top and a series of buttons on the bottom. To pull this off as a web-application, you need fixed positioning. Unfortunately, Mobile Safari (as well as Android's browser) do not provide this feature. There are many hacks to emulate it though. Apple has even developed a Javascript/CSS framework to get around this themselves. For our iPad web-app, we used a library called TouchScroll that uses Javascript and CSS animations to emulate scrolling. Overall, the lack of fixed positioning is not a game-ender, as hacks are available, but it certainly adds a layer of complexity to building a client-centric web-app that will make many developers' lives more difficult. The next quirk may just be a game-ender. Mobile Safari on the iPhone and iPad has an annoying ability to refresh windows once you open up a few. So say you have a few windows open including our music player. The music player is not the active window however. When you click back on it to make it active, it refreshes. I'm not sure why this happens although my guess is it has something to do with memory management. Either way, most apps (especially music or video ones) cannot be taken seriously if the application is constantly refreshing. Sure, in theory applications could maintain their state between refreshes. But in practice, this never works quite as well as we'd like it to. Of course, music stopping and starting constantly will be a game-ender. I am hoping that the iPhone 4 with its 512MB RAM does not have this issue. Android does not seem to suffer from this but Android does not support HTML5 Audio or Video so it certainly has drawbacks of its own.

A couple of weeks ago, Safari 5 was introduced with support for extensions. We spent a few hours reading the documentation to see what it would take to port our extension from Chrome to Safari. Unlike Firefox, where extensions are a bit more than just HTML/CSS/JS and support for HTML5 features are lagging, Safari extensions are theoretically similar to Chrome. Other than different Javascript APIs, the functionality offered is almost exactly the same. We determined that it would not be too difficult to port. Of course, business decisions are not always just about technology, so we are waiting a bit before we do just that. It will however, be very interesting to see if Apple brings extension support to Mobile Safari on iOS.

Coming back to Dion's original post, it seems like we are closer to write-once, run-anywhere using client-centric web-based applications than we have ever been. With a few modifications, we can basically re-use 80% of our code to deploy on the desktop and mobile. Unfortunately, not all browsers are there yet though. The issues I listed above seem like minor ones that can be fixed easily however. It would not surprise me if in the second half of 2010, Mobile Safari fixes the refreshing issue and Android adds support for HTML5 Audio/Video. As more frameworks are released that deal with the fixed positioning problem, I expect more developers to look seriously at the web as their platform of choice for deploying mobile applications. As for the desktop, Firefox 4 and IE9 should finally add the HTML5 and CSS3 feature support that Chrome and Safari have had for a while now. Of course, not all users will have the latest and greatest browser. Progressive enhancement will be the only way to support those users. But by pushing the envelope of what's possible, we will hopefully push those users towards upgrading to a modern browser.

For a 60 second run through of ExtensionFM, check out the video below:

Amazing Audio API JavaScript Demos

David Humphrey and the hit squad of audio gurus have some new amazing demos for us. Perfect for a Friday. This is all through the rich Mozilla Audio API work which will hopefully be pushed into other browsers at some point in the not so distant future.

Charles Cliffe has some awesome WebGL visualizations from Audio. David narrates:

What I like most about these (other than the fact that he’s written the music, js libs, and demo) is that these combine a whole bunch of JavaScript libraries: dsp.js, cubicvr.js and beatdetection.js, and processing.js. Some people will tell you that doing anything complex in a browser is going to be slow; but Charles is masterfully proving that you can do many, many things at once and the browser can keep pace.

Corban and Ricard Marxer have been busy exploring how far we can push audio write, and managed to also produce some amazing demos. The first is by Ricard, and is a graphic equalizer (video is here):

The second is by Corban, and shows a JavaScript based audio sampler. His code can loop forward or backward, change playback speed, etc. (video is here):

Chris McCormick has been working on porting Pure Data to JavaScript, and already has some basic components built. Here’s one that combines processing.js and webpd (video is here):

I think that my favourite demo by far this time around is one that I’ve been waiting to see since we first began these experiments. I’ve written in the past that our work could be used to solve many web accessibility problems. A few weeks ago I mentioned on irc that someone should take a shot at building a text to speech engine in JavaScript, now that we have typed arrays. Yury quietly went off and built one based on the flite engine. When you run this, remember that you’re watching a browser speak with no plugins of any kind. This is all done in JavaScript (demo is here, video is here):

In order to do this he had to overcome some interesting problems, for example, how to load large binary voice databases into the page. The straightforward approach of using a JS array was brittle, with JS sometimes running out of stack space trying to initialize the array. After trying various obvious ways, Yury decided to use the web to his advantage, and pushed the binary data into a PNG, then loaded it into a canvas, where getImageData allows him to access the bytes very quickly, using another typed array. The browser takes care of downloading and re-inflating the data automatically. Here’s what the database looks like:

My favourite line is:

What began as a series of experiments by a small group of strangers, has now turned into something much larger.

What an awesome community you guys have... and we are all benefitting. Thank you.

Side by side diffs that look great, all in the browser

We knew that we needed VCS support for Bespin. We also knew that we wanted to have support that is richer than just having command line access. Since we are in a rich UI environment we can do more, and one of the features I was excited to play with was diff visualization.

Well, the Atlassian FishEye crew just popped up with a preview of side by side diffs in the browser and the looks great, similar to FileMerge and the like. Pete Moore takes us on a walk through:

3D CSS Parallax Effect

Roman Cortes has a fun parallax effect using just CSS and HTML. Scroll over the bad boy above and see it at work.

Then view the source and have your eyes bleed :)

HTML:
  1.  
  2. <style type="text/css">
  3.                 a#a96:hover img {left: 224px}a#a96:hover b.d1 {background-position: 224px 0; width: 224px}a#a96:hover b.d2 {background-position: -449.6px 18px;}a#a96:hover b.d3 {background-position: -32px -216px; width: 368px}a#a96:hover b.d4 {background-position: -64px -74px;}a#a97:hover img {left: 223px}a#a97:hover b.d1 {background-position: 223px 0; width: 223px}a#a97:hover b.d2 {background-position: -449.7px 18px;}a#a97:hover b.d3 {background-position: -32.3333333333px -216px; width: 367.666666667px}a#a97:hover b.d4 {background-position: -64.6666666667px -74px;}a#a98:hover img {left: 222px}a#a98:hover b.d1 {background-position: 222px 0; width: 222px}a#a98:hover b.d2 {background-position: -449.8px 18px;}a#a98:hover b.d3 {background-position: -32.6666666667px -216px; width: 367.333333333px}a#a98:hover b.d4 {background-position: -65.3333333333px -74px;}a#a99:hover img {left: 221px}a#a99:hover b.d1 {background-position: 221px 0; width: 221px}a#a99:hover b.d2 {background-position: -449.9px 18px;}a#a99:hover b.d3 {background-position: -33px -216px; width: 367px}a#a99:hover b.d4 {background-position: -66px -74px;}        </style>
  4.  
  5.         <a href="#" id="a0"><img src="meninas_bg2.jpg" width="80" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a1"><img src="meninas_bg2.jpg" width="81" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a>
  6.         <a href="#" id="a2"><img src="meninas_bg2.jpg" width="82" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a3"><img src="meninas_bg2.jpg" width="83" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a4"><img src="meninas_bg2.jpg" width="84" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a5"><img src="meninas_bg2.jpg" width="85" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a6"><img src="meninas_bg2.jpg" width="86" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a7"><img src="meninas_bg2.jpg" width="87" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a8"><img src="meninas_bg2.jpg" width="88" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a9"><img src="meninas_bg2.jpg" width="89" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a10"><img src="meninas_bg2.jpg" width="90" height="455" alt="" /><b class="d1"></b><b class="d4"></b><b class="d3"></b><b class="d2"></b><span>&nbsp;</span></a><a href="#" id="a11"><img src="meninas_bg2.jpg" width="91" height="455" alt="" /><b class="d1"></b>
  7.  

I hope he used a tool ;)

CBC Radio 3 Case Study

Phil Rabin of CBC Radio 3 has kindly written a guest post on his experience creating a fantastic Web interface for the station that uses Flash for audio, but a full HTML experience that maintains state from page to page.

cbcradio3

CBC Radio 3 is a community, radio station and user-generated independent music library which is a small department of the Canadian Broadcasting Corporation. When the CBC Radio 3 web team was called upon to rebuild the site we were confronted with the technical problem of having an uninterrupted music experience for our users. The old design of the site (see image) achieved this by embedding a flash player in the body with the content being served through a statically positioned iframe in the center of the page. Radio 3's content offerings were outgrowing the design so we went with a full page 1000px-wide layout with the player resting in the page. This created an obvious hurdle being that with a fresh page load comes a bad listening experience like myspace where a single wrong click breaks the audio. Also, not having popup player was a design decision that was made to give the website a more integrated feel.

We decided to completely removed flash from the UI equation and went full html/ajax because we found that it offered more flexibility and play with the page. The hardest part was figuring out a way to maintain state on each page load while keeping the audio continuous.

We went with an old-school frameset to create a type of inter-frame communication with the top level frameset acting as the orchestrator/bootstrapper. The visible "UI Controller" frame is completely blown out with the stateful player frame hidden from view.

The stateful player frame contains hidden swfs to handle playing audio and connecting to RTMP for our live streaming. All the communication in and out of flash is handled by a couple gateway javascript classes to abstract out the flash from the rest of the application.

Here's an example of a communication gateway for wrapping the events coming to and from flash. The event objects are native flash event objects that get sent by Flash's ExternalInterface and come in as JSON that can:

JAVASCRIPT:
  1.  
  2. CBCR3.namespace("CBCR3.Player.External");
  3.  
  4. CBCR3.Player.External.RTMPGateway = Class.create(CBCR3.Commons.EventDispatcher, {
  5.      
  6.     initialize:function($super)
  7.     {
  8.         $super();         
  9.     },
  10.      
  11.     //Functions to receive events from flash   
  12.     sendStreamEvent:function(event)
  13.     {
  14.         this.dispatchEvent(event.type);
  15.     },
  16.    
  17.     sendMetaDataEvent:function(event)
  18.     {         
  19.         var metaData = new CBCR3.Player.Mappers.StreamMetaDataDtoMapper().mapCollection(event.metaData);       
  20.         this.dispatchEvent(CBCR3.Player.Events.RTMPStreamEvent.metaDataReceived, {metaData:metaData});
  21.     },
  22.        
  23.     //Functions to send commands from flash
  24.     sendStreamCommand:function(commandName, commandArgs)
  25.     {         
  26.         $(CBCR3.Player.Globals.rtmpPlayerId).streamCommand(commandName, commandArgs);
  27.     }
  28. });
  29.  

A single instance of this gateway is always maintained in the application which is called by a sort of simple container like this:

JAVASCRIPT:
  1.  
  2. ExternalInterface.call("CBCR3.Player.Application.IoC.getInstanceOf('rtmpGateway').sendStreamEvent", event);   
  3.  

An instance of the gateway has to be maintained by the player application because events coming from flash have no context. This way the application classes can subscribe to events coming from flash like this:

JAVASCRIPT:
  1.  
  2. CBCR3.Player.Players.RTMPPlayer = Class.create(CBCR3.Commons.EventDispatcher, {
  3.     initialize:function($super, rtmpGateway, thumbLookupService)
  4.     {
  5.         $super();     
  6.          
  7.         this.rtmpGateway = rtmpGateway;       
  8.  
  9.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.metaDataReceived, this.rtmpMetaDataReceivedHandler.bind(this));         
  10.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.ready, this.streamReadyHandler.bind(this));
  11.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.connecting, this.streamConnectingHandler.bind(this));
  12.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.streaming, this.streamStreamingHandler.bind(this));
  13.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.connected, this.streamConnectedHandler.bind(this));
  14.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.metaDataConnected, this.rtmpMetaDataConnectedHandler.bind(this));
  15.         this.rtmpGateway.addEventListener(CBCR3.Player.Events.RTMPStreamEvent.failed, this.rtmpMetaDataFailedHandler.bind(this));
  16.  
  17.  
  18.     },
  19.  
  20.     streamReadyHandler:function(event)
  21.     {
  22.         //handle stream event
  23.     },
  24.  
  25.     streamConnectingHandler:function(event)
  26.     {         
  27.         //handle connecting event
  28.     },
  29.  
  30.     streamConnectedHandler:function(event)
  31.     {
  32.         //handle connected event
  33.     },
  34.  
  35.     rtmpMetaDataConnectedHandler:function(event)
  36.     {
  37.        //handle meta deta connected event
  38.     },
  39.  
  40.     rtmpMetaDataReceivedHandler:function(event)
  41.     {         
  42.         //handle meta data event etc etc
  43.     }
  44. );
  45.  

At the core, audio is always played by Flash. The swfs broadcast events, such as audio head position and download progress of mp3s, and connection, streaming, meta data events from RTMP. Those events get passed on the instance of the hidden stateful player.

Since the server frame is only loaded once when the site first loads, an instance of the stateful server player is instantiated for the entire session on the site. On each client frame page load, the server player instance is "injected" into the visible client UI controller by the "bootstrapper" top frame. State is maintained in that instance which allows for the controller to query the state of that object and reestablish everything like which track is playing, progress, time, thumbs up or down status, shuffle, play mode (stream or individual mp3 and playlists), etc. Everything had to be covered like if an mp3 was in mid-load when someone browsed to a new page, the loading progress had to pickup on the next page. Here's a example of the bootstrapper code contained in the frameset:

JAVASCRIPT:
  1.  
  2. CBCR3.namespace("CBCR3.Player.Application");
  3.  
  4. CBCR3.Player.Application.R3PlayerBootStrap = Class.create({
  5.  
  6.     serverFrame:null,
  7.     clientFrame:null,
  8.  
  9.     autoStart:true,
  10.     permalink:null,
  11.  
  12.     initialize:function(preferences)
  13.     {         
  14.         this.autoStart = preferences.autoStart;
  15.         this.permalink = preferences.permalink;         
  16.     },
  17.      
  18.     setServerFrame:function(serverFrame) {
  19.         this.serverFrame = serverFrame;
  20.     },     
  21.          
  22.     setClientFrame:function(clientFrame) {
  23.         this.clientFrame = clientFrame;         
  24.     },
  25.      
  26.      
  27.     //TRY LOAD PLAYER
  28.     loadPlayer:function()
  29.     {         
  30.         if(!this.clientFrame || !this.serverFrame)
  31.             return;
  32.  
  33.         //both frames are loaded at this point
  34.         if(this.serverFrame.getPlayerInstance() == null)
  35.             this.initializePlayer();
  36.         else
  37.             this.resumePlayer();
  38.     },
  39.      
  40.     initializePlayer:function()
  41.     {
  42.         this.serverFrame.initPlayer();
  43.  
  44.         this.clientFrame.checkEnvironment();
  45.         var masterPlayerInstance = this.serverFrame.getPlayerInstance();
  46.         this.clientFrame.loadPlayer(masterPlayerInstance);
  47.         masterPlayerInstance.addEventListener("stateInitEvent:streamPlayerLoaded", this.streamPlayerLoadedHandler.bind(this));
  48.         masterPlayerInstance.addEventListener("stateInitEvent:playlistPlayerLoaded", this.playlistPlayerLoadedHandler.bind(this));
  49.     },
  50.      
  51.     resumePlayer:function()
  52.     {
  53.         this.clientFrame.loadPlayer(this.serverFrame.getPlayerInstance());
  54.         this.clientFrame.resumePlayer();         
  55.     },
  56.      
  57.     streamPlayerLoadedHandler:function(event)
  58.     {
  59.         if(this.autoStart && this.permalink.include("/stream/"))
  60.             this.clientFrame.getPlayerInstance().stream(this.permalink);
  61.     },
  62.      
  63.     playlistPlayerLoadedHandler: function(event)
  64.     {
  65.         if(this.autoStart && this.permalink.include("/play/"))
  66.             this.clientFrame.getPlayerInstance().playlist(this.permalink);
  67.     }
  68. });
  69.  

We used Prototype/Scriptaculous as the base for the entire site. All the AJAX communication is handled with asp.net web services with scripting enabled. ASP.NET takes care of all the serialization of DTO's (Data Transfer Object) into JSON which are specific to the player application.

All of the classes in the application are written using Prototype's Class/inheritance model. Most of the classes subclass from a base EventDispatcher much like AS3, which is adapted from Matthew Foster's example for Prototype and our own custom Event model. This allows for a nice separation of concerns and decoupled classes throughout the application and allows the UI Controller to add event listeners to custom events coming from the server player instance.

JAVASCRIPT:
  1.  
  2. CBCR3.namespace("CBCR3.Commons");
  3.  
  4. CBCR3.Commons.EventDispatcher = Class.create({
  5.      
  6.     buildListenerChain:function()
  7.     {
  8.          
  9.         if(!this.listenerChain)
  10.             this.listenerChain = {};                                   
  11.      
  12.     },
  13.      
  14.     addEventListener:function(type, listener){
  15.                                    
  16.         if(!listener instanceof Function)
  17.             alert("Listener isn't a function");
  18.              
  19.         this.buildListenerChain();
  20.                  
  21.         if(!this.listenerChain[type])                         
  22.             this.listenerChain[type] = [listener];
  23.         else
  24.             this.listenerChain[type].push(listener);
  25.          
  26.     },
  27.          
  28.     hasEventListener:function(type)
  29.     {
  30.         return (typeof this.listenerChain[type] != "undefined");
  31.     },
  32.      
  33.     removeEventListener:function(type, listener)
  34.     {
  35.         if(!this.hasEventListener(type))
  36.         return false;
  37.      
  38.         for(var i = 0; i &lt;this.listenerChain[type].length; i++)
  39.             if(this.listenerChain[type][i] == listener)
  40.                 this.listenerChain.splice(i, 1);
  41.      
  42.     },
  43.      
  44.     clearEventListeners:function()
  45.     {
  46.         this.listenerChain = {};
  47.     },
  48.      
  49.     dispatchEvent:function(type, data, target)
  50.     {         
  51.         var    event = new CBCR3.Commons.Event(type, data, target || this);
  52.         this.buildListenerChain();
  53.      
  54.         if(!this.hasEventListener(type))
  55.             return false;
  56.      
  57.         this.listenerChain[type].any(function(funct){
  58.             return (funct(event) == false ? true : false);
  59.         });
  60.     }
  61. });
  62.  

This also allows the UI Controller to unsubscribe from all events when the page unloads. This was key in memory management and so that we don't get orphaned references to instances of the UI Controller.

The most difficult part of the whole player project was re-establishing state of the controller on every page load. We hoped that we could implement some sort of state-pattern with no luck. In the end, the UI controller contains a couple monster resume methods that we haven't been able to abstract out of that class. We'd like to bring in some sort of MVC architecture that wires up the UI player view to a state object. Any suggestions would be welcome! Go check out the site and give us some feedback!

Dion: I then asked Phil about the CBCR3 library and he replied

CBCR3 is the base namespace for all th javascript controls and apps written for the site. Everything for the player is in CBCR3.Player, the concert calendar is CBCR3.Gigs, etc. We have a shared base lib which is in CBCR3.Commons.

An issue with Prototype that we had was some bug with including 1.6.1 in a frameset in Opera. So, right now the frameset is holding an older version of prototype while the frames have the latest. One thing that Prototype was seriously lacking was Date extensions. (like addDay, addMonth, addWeek) etc.
We ended up going with YUI's DateMath widget for that which really smoothed out working with dates.

Most of the issues we had cross-browser stuff was with IE6 (no surprise), which were almost all related to CSS rendering bugs, and IE DOM manipulation problems. A big one was upon the dynamic removal of items from lists. IE has a real hard time refreshing the positions of items. We had to write methods like

JAVASCRIPT:
  1.  
  2. myList.select("li").each( function(item){
  3.    li.setStyle({display:"none"});
  4.    li.setStyle({display:"block"});
  5. });
  6.  

this would in effect "nudge" the browser and force it to update the position of the remaining DOM elements. In the end, we chose to drop IE6 support and to tell you the truth, we haven't heard a single complaint about it!

Mockingbird: Cappuccino-based visual mockup tool

Mockingbird is a nice Cappuccino based tool that lets you quickly mockup a wire-frame on the Web.

mockingbird

Fire it up, build out your "pages", drag and drop your UI, and then share it with your clients!