{{http://www.aemon.com/file_dump/logo.jpg}}
===== Announcements =====
The locus for Croqodile development/news has been moved to: http://code.google.com/p/croqodile/
===== Summary =====
* **Description **: An AS3/Erlang implementation of the Croquet project's [[http://www.opencroquet.org/index.php/System_Overview|TeaTime protocol]]. Croqodile abstracts networking and state synchronization to allow for rapid development of multi-user, richly-collaborative flash applications.
* **License **: MIT-License
* **Author **: Aemon Cannon - aemoncannon at gmail dot com
===== Download =====
[[http://www.aemon.com/file_dump/croqodile_0.1.1.zip| Croqodile 0.1.1]] - May 05, 2007
[[http://www.aemon.com/file_dump/croq_0_1_0.rar| Croqodile 0.1.0]] - April 21, 2007
===== Tutorials =====
Croqodile differs from Croquet in many details (it is a far less ambitious project), but the general architecture is similar, so it is important that you read (or at least skim) the
Croquet project's [[http://www.opencroquet.org/index.php/System_Overview|System Overview]] before continuing.
A Croqodile system will consist of three components. Two of these components will run on the client's machine and are implemented entirely in Flash Actionscript - they are the Controller and the IslandReplica. The third component is the router, a thin server implemented in erlang that will serve as a middle-man between the many clients.
In most cases, Croqodile's router and Controller can be used as-is. The IslandReplica, on the other hand, must be extended to suit the needs of the specific application. We will be looking at the Croqodile distribution's whiteboard demo as an example.
package com.croqodile.whiteboard {
import flash.display.*;
import flash.geom.Rectangle;
import flash.utils.ByteArray;
import flash.system.Security;
import com.senocular.utils.Output;
import flash.utils.Timer;
import com.croqodile.*;
import com.croqodile.whiteboard.*;
import com.croqodile.serialization.base64.Base64;
import flash.events.*;
public class WhiteboardIsland extends IslandReplica {
private var _canvas:Sprite;
public function WhiteboardIsland(config:Object){
super();
this._canvas = config.canvas;
this.clearWithWhite();
}
override public function freeze():Object{
var data:Object = new Object();
var bitmap:BitmapData = new BitmapData(this._canvas.width, this._canvas.height);
bitmap.draw(this._canvas);
var bitmapBytes:ByteArray = bitmap.getPixels(new Rectangle(0, 0, bitmap.width, bitmap.height));
bitmapBytes.compress();
data.imageData = Base64.encodeByteArray(bitmapBytes);
bitmap.dispose();
return data;
}
override public function unfreeze(data:Object):void {
var bitmapBytes:ByteArray = Base64.decodeToByteArray(data.imageData);
bitmapBytes.uncompress();
var bitmap:BitmapData = new BitmapData(this._canvas.width, this._canvas.height);
bitmapBytes.position = 0;
bitmap.setPixels(new Rectangle(0, 0, bitmap.width, bitmap.height), bitmapBytes);
this._canvas.graphics.clear();
this._canvas.graphics.beginBitmapFill(bitmap);
this._canvas.graphics.drawRect(0,
0,
this._canvas.width,
this._canvas.height);
this._canvas.graphics.endFill();
}
private function drawSegment(seg:Object):void{
this._canvas.graphics.lineStyle(seg.thickness, int(seg.color));
this._canvas.graphics.moveTo(seg.startX, seg.startY);
this._canvas.graphics.lineTo(seg.endX, seg.endY);
}
private function clearWithWhite():void{
this._canvas.graphics.beginFill(0xFFFFFF);
this._canvas.graphics.drawRect(0,
0,
this._canvas.width,
this._canvas.height);
this._canvas.graphics.endFill();
}
////////////////////////
// External Interface //
////////////////////////
public function addSegment(segment:Object):void{
this.drawSegment(segment);
}
}
}
'drawSegment' and 'clearWithWhite' are private utility methods - pretty self explanatory.
'freeze' and 'unfreeze' are an important part of the Croqodile infrastructure. When a new client connects to the router they must somehow synchronize their IslandReplica with everyone else's. In order to bring the new user into the Island-in-progress, Croqodile will call the 'freeze' method of an existing client's IslandReplica. The resulting frozen IslandReplica will be serialized and sent through the router to the new client, where it will be passed to a fresh IslandReplica's 'unfreeze' method. At that point the two IslandReplicas should (and must) be identical.
There is only one method in the 'External Interface' section of the class, 'addSegment'. This is the //only// method that the clients of our Island will be calling, and we must now add the code that will generate these calls. What follows is just some standard, event-handling flex code - first the mxml:
And now the code that is referenced by //source="whiteboard.as"//:
import flash.display.*;
import flash.ui.Keyboard;
import flash.utils.getTimer;
import com.senocular.utils.Output;
import com.croqodile.*;
import com.croqodile.whiteboard.*;
import com.croqodile.events.*;
import com.croqodile.di.*;
import flash.events.*;
private var _controller:Controller;
private var _islandRef:FarRef;
private var _lastX:Number;
private var _lastY:Number;
private static const SEGMENT_FREQ:Number = 50;
private var _lastSegTime:Number = 0;
public function onConnectButtonClicked():void {
var host:String = this.routerHostInput.text;
var config:Object = DIRunner.run([
{name: "island", klass: WhiteboardIsland,
args: {canvas: this.canvas},
injectArgs: {} },
{name: "controller", klass: SnapshottingController,
args: {policyHost: host, routerHost: host, snapshotHost: host},
injectArgs: {island: "island"}} ]);
this._controller = config.controller;
this._controller.addEventListener(RouterConnectionReadyEvent.type, routerConnectionReady);
this.stage.addChild(new Output());
}
public function routerConnectionReady(event:Event):void{
Output.trace("Router connection ready.");
this._islandRef = this._controller.island().farRef();
this._controller.addEventListener(DisconnectedFromRouterEvent.type, disconnectedFromRouter);
this.canvas.addEventListener(MouseEvent.MOUSE_DOWN, onPenDown);
this.canvas.addEventListener(MouseEvent.MOUSE_UP, onPenUp);
}
public function disconnectedFromRouter(event:Event):void{
Output.trace("Disconnected from router... :(");
}
private function onPenDown(event:MouseEvent):void{
this._lastX = event.localX;
this._lastY = event.localY;
this.canvas.addEventListener(MouseEvent.MOUSE_MOVE, onPenDraw);
}
private function onPenUp(event:MouseEvent):void{
this.canvas.removeEventListener(MouseEvent.MOUSE_MOVE, onPenDraw);
}
private function onPenDraw(event:MouseEvent):void{
var time:Number = getTimer();
if((time - this._lastSegTime) > SEGMENT_FREQ){
var segment:Object = new Object();
segment.startX = this._lastX;
segment.startY = this._lastY;
segment.endX = event.localX;
segment.endY = event.localY;
segment.color = this.colorPicker.selectedColor;
segment.thickness = this.thicknessSlider.value;
this._islandRef.send("addSegment", [ segment ]);
this._lastX = event.localX;
this._lastY = event.localY;
this._lastSegTime = time;
}
}
First, in the 'onConnectButtonClicked' handler, we create our Controller and our IslandReplica. The Controller immediately (auto-magically) connects to our router (which must already be running).
Now that everything is hooked up, we can finally send a message to the Island.
Pay attention to the line //this._islandRef.send("addSegment", [ segment ]);//.
'_islandRef' is an object of FarRef type. Whenever a message is 'sent' to a FarRef, that message is passed to the local Controller. The Controller knows to transmit the message to the router. The router will then broadcast the message to all the Controllers of all the clients, and then each of the Controllers will tell their respective IslandReplicas to execute the message.
To be continued....