Lately I've been playing around with some code-generated drawing and animation. The following is a tutorial describing how I created a tree that sways in the wind. The user generates a small but powerful source of wind by clicking and dragging around the stage. Here is what the final movie looks like:
Drawing the Tree
The tree movie consists of two classes — the main movie or Tree class which generates the tree and handles events, and the Branch class which represents each branch of the tree. Tree.as looks like this:
// Tree.as
package {
import flash.display.*;
import flash.geom.Point;
public class Tree extends MovieClip{
private var trunk:Branch;
public function Tree(){
trunk = new Branch(-90, new Point(stage.stageWidth/2, stage.stageHeight));
addChild(trunk);
}
}
}
At this stage the Tree class is pretty basic. It creates a new branch called trunk at an angle of -90 degrees (straight up), and centers it horizontally at the bottom of the movie.
Branch.as looks like this:
// Branch.as
package {
import flash.display.*;
import flash.geom.Point;
public class Branch extends Shape{
// class vars
private static const baseLength:Number = 100;
// instance vars
private var angle:Number;
private var endPoint:Point;
public function Branch(initialAngle:Number, origin:Point){
angle = initialAngle;
x = origin.x;
y = origin.y;
endPoint = getEndPoint();
drawBranch();
}
private function drawBranch():void{
graphics.clear();
graphics.moveTo(0, 0);
graphics.lineStyle(10, 0x000000);
graphics.lineTo(endPoint.x, endPoint.y);
}
// Using angle and baseLength as polar coordinates get the endPoint of the line as cartesian coordinates.
private function getEndPoint():Point{
return Point.polar(baseLength, degToRad(angle));
}
private static function degToRad(deg:Number):Number{
return deg/57.3;
}
private static function radToDeg(rad:Number):Number{
return rad*57.3;
}
}
}
Branch() takes an initial angle and a point. The point is the branch's origin — where it connects to the rest of the tree or the ground in the case of the trunk. The function drawBranch() uses the Shape class's graphics property to draw out the graphical representation of the branch. This is simply a line drawn from the branches origin to an end point. The endPoint is found using the Point class's polar() method to convert the polar coordinates defined by angle and baseLength, to cartesian coordinates. This is done in getEndPoint(). degToRad() is a utility function that converts a given number of degrees into radians. The sister funciton to degToRad() is radToDeg() which unsurprisingly converts radians to degrees. These are particularly useful functions since some actionScript classes use radians and others use degrees.
Compiling and running Tree will draw a line on the screen.
Not too interesting yet, but this is about to change. The clever thing about what we are about to do is that we won't have to do much to Tree.as to draw the rest of the tree. The Branch class will take care of that with a little bit of recursion. Once each branch is done initializing it will create two more slightly smaller branches at different angles. Updates are in bold.
// Branch.as
package {
import flash.display.*;
import flash.geom.Point;
public class Branch extends Shape{
// class vars
private static const baseLength:Number = 100;
private static const minScale:Number = 0.2;
private static const slope:Number = 0.8;
// instance vars
private var angle:Number;
private var endPoint:Point;
private var globalEndPoint:Point;
private var thisParent:MovieClip
private var children:Array = [];
public function Branch(initialAngle:Number, scale:Number, origin:Point, parentClip:MovieClip){
angle = initialAngle;
x = origin.x;
y = origin.y;
thisParent = parentClip;
scaleX = scaleY = scale;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch();
thisParent.addChild(this);
if(scale*slope > minScale){
sproutBranches(scale*slope);
}
}
private function sproutBranches(scale:Number):void{
// new branch angles are augmented by a random +- 10 degrees
var branchLeft:Branch = new Branch(angle - 30, scale, globalEndPoint, thisParent);
var branchRight:Branch = new Branch(angle + 30, scale, globalEndPoint, thisParent);
children.push(branchLeft);
children.push(branchRight);
}
private function drawBranch():void{
graphics.clear();
graphics.moveTo(0, 0);
graphics.lineStyle(10, 0x000000);
graphics.lineTo(endPoint.x, endPoint.y);
}
// Using angle and baseLength as polar coordinates get the endPoint of the line as cartesian coordinates.
private function getEndPoint():Point{
return Point.polar(baseLength, degToRad(angle));
}
// convert degerees to radians
private static function degToRad(deg:Number):Number{
return deg/57.3;
}
// convert radians to degrees
private static function radToDeg(rad:Number):Number{
return rad*57.3;
}
}
}
One very important thing to take note of is the last few lines of the Branch constructor. Here we call sproutBranches(), a function that attaches two new branches to the current branch. Here we check the current branch's scale against the minScale constant — the minimum allowed size for a branch. If a branch's scale has reached the lower limit for branch size, it will not call sproutBranches(). It will not create any new child branches. If we did not do this check flash runtime would get caught in an infinite loop and would soon crash.
The sproutBranches() function creates two new branches at + and - 30 degrees from the angle of the current branch. They are then added to the children array for later reference.
We've also added globalEndPoint which will be used to keep track of each branch's endPoint relative to the root coordinate plane. This is passed on to child branches to tell them where to set their origins.
We will need to change the parameters passed to trunk in Tree.as. Here we are telling trunk to render at full scale and setting its parent to the root MovieClip. We can also remove the addChild(tree) line since the Branch constructor takes care of adding branches to the stage.
// Tree.as trunk = new Branch(-90, 1, new Point(stage.stageWidth/2, stage.stageHeight), this);
Compile and run this iteration. You should see a more densely populated tree.
That's not bad but it doesn't look very natural. We could improve this by adding a little randomness to the branches. Change lines 37 and 38 of Branch.as to look like this:
// Branch.as var branchLeft:Branch = new Branch(angle - 30 + Math.random()*20 - 10, scale, globalEndPoint, thisParent); var branchRight:Branch = new Branch(angle + 30 + Math.random()*20 - 10, scale, globalEndPoint, thisParent);
Now all of the the branches should be rotated + or - 30 degrees from their parent branches plus and additional random amount somewhere between 10 and -10. This looks more natural:
Generating Wind
I could easily spend all day tweaking the size and shape of this tree but then we'd never get to animate it. Time to move on.
The first thing we're going to do is write some event handlers. One handler will trigger the "generation of wind" when the user clicks somewhere on the stage. Another handler will do the wind generation by moving the branches on ENTER_FRAME events. And finally, another handler will remove the wind generation event handler when the user releases the mouse button.
// Tree.as
package {
import flash.display.*;
import flash.geom.Point;
import flash.events.*;
public class Tree extends MovieClip{
private var trunk:Branch;
public function Tree(){
trunk = new Branch(-90, 1, new Point(stage.stageWidth/2, stage.stageHeight), this);
stage.addEventListener(MouseEvent.MOUSE_DOWN, startWind);
stage.addEventListener(MouseEvent.MOUSE_UP, endWind);
}
private function startWind(e:MouseEvent):void{
addEventListener(Event.ENTER_FRAME, generateWind);
}
private function endWind(e:MouseEvent):void{
removeEventListener(Event.ENTER_FRAME, generateWind);
}
private function generateWind(e:Event):void{
trunk.bend();
}
}
}
The other piece we need to add in this step is the bend() method of the Branch class:
// Branch.as
public function bend():void{
angle = radToDeg(Math.atan2(mouseY + endPoint.y, mouseX + endPoint.x));
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch();
}
This method finds the angle of an imaginary line between the mouse and the endPoint of the branch, moves the endPoint so that it lies on that imaginary line, then redraws the branch. It will take a few frames but the branch will end up pointing at the mouse.
Next, we want this method to propagate through the whole tree. This part is a little bit magical. After the bend() function we will reposition each of the branch's children to the the branch's endPoint and then call bend() on each. Remember that each branch has at most only 2 child branches, but since the bend() method is the same for each child branch as it is for the trunk, it will in turn call bend() on the child branches of each of those branches and so on until there are no child branches left. I've broken this task out into a function called positionChildren():
// Branch.as
public function bend():void{
angle = radToDeg(Math.atan2(mouseY + endPoint.y, mouseX + endPoint.x)) ;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch();
positionChildren();
}
private function positionChildren():void{
globalEndPoint = localToGlobal(endPoint);
for each(var child:Branch in children){
child.x = globalEndPoint.x;
child.y = globalEndPoint.y;
child.bend();
}
}
Now all the branches point toward the mouse when the user clicks on the stage.
This movement happens a little too quickly. Also, if the mouse is supposed to be the point of wind generation then they should be moving away from it rather than towards it. Let's slow it down and reorient the branches by modifying the bend() function of the Branch class:
// Branch.as
public function bend():void{
var windAngle:Number = radToDeg(Math.atan2(mouseY + endPoint.y, mouseX + endPoint.x)) - 180;
if(windAngle - angle < -180){
windAngle += 360;
} else if(windAngle - angle> 360){
windAngle -= 360;
}
if(Math.abs(windAngle - angle) > 1){
angle += (windAngle - angle)/8;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch()
}
positionChildren();
}
First we flip the angle we're working with (the angle between the mouse and the endpoint of the branch) and store it in a variable called windAngle. This will make the branches point away from the mouse rather than toward it. It is the target angle that we want the branch's angle to eventually reach. In the following if() statement we fudge the numbers a bit so that the difference between the target angle and the current angel is always more than -180 and less than 180. This is to prevent erratic behavior at certain angles. For example, if the target angle were 10 and the branch's current angle were -10 we would want the difference to be considered to be 20 degrees (the short way around the circle) rather than -340 degrees (the long way around).
The next step determines how quickly the branch rotates to the target angle. We reset the branch's angle to a number that is somewhere between the current angle and windAngle. We adjust the angle by 1/8 of the difference between angle and windAngle. This is similar to adding a motion tween with an ease out property to a frame in the timeline.
Next lets add resistance. We'll add an another ENTER_FRAME event listener to the Tree class. On every frame the event handler resist() will call a method of the Branch class called returnToRest(). returnToRest() will work in opposition to bend() and attempt to move the branch back to it's original orientation.
// Tree.as
package {
import flash.display.*;
import flash.geom.Point;
import flash.events.*;
public class Tree extends MovieClip{
private var trunk:Branch;
public function Tree(){
trunk = new Branch(-90, 1, new Point(stage.stageWidth/2, stage.stageHeight), this);
stage.addEventListener(MouseEvent.MOUSE_DOWN, startWind);
stage.addEventListener(MouseEvent.MOUSE_UP, endWind);
addEventListener(Event.ENTER_FRAME, resist);
}
private function resist(e:Event):void{
trunk.returnToRest();
}
private function startWind(e:MouseEvent):void{
addEventListener(Event.ENTER_FRAME, generateWind);
}
private function endWind(e:MouseEvent):void{
removeEventListener(Event.ENTER_FRAME, generateWind);
}
private function generateWind(e:Event):void{
trunk.bend();
}
}
}
The first change to the Branch class is to store the angel that the branch starts out at. This is the variable restAngle. Along with angle set it to initialAngle in the constructor.
//Branch.as
public var restAngle:Number;
public function Branch(initialAngle:Number, scale:Number, origin:Point, parentClip:MovieClip){
angle = restAngle = initialAngle;
...
The returnToRest() function works similarly to bend() in that once it is called on trunk it propagates through the entire tree. It uses the same technique that we used to bend the branch away from the mouse, to bend it back to restAngle. returnToRest() makes use of the function unbendChildren() which does essentially the same thing as positionChildren(). It moves child branches to the current branch's endPoint and calls returnToRest() on each of them.
// Branch.as
public function returnToRest():void{
if(Math.abs(restAngle - angle) > 1){
angle += (restAngle - angle)/8;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
// redraw
drawBranch();
}
unbendChildren();
}
private function unbendChildren():void{
globalEndPoint = localToGlobal(endPoint);
for each(var child:Branch in children){
child.x = globalEndPoint.x;
child.y = globalEndPoint.y;
child.returnToRest();
}
}
Compile and run the movie and you should have a tree that bends away from the mouse when you click and drag around the stage.
It seems to me that smaller branches closer to the source of wind should be affected more than those that are further away from it. I created a strength variable in the bend() function that is used to influence how much a branch will bend on each frame. The value of strength is the distance between the mouse and the branch's endPoint. This is found by using the pythagorean theorem (a2 + b2 = c2) on distX and distY.
// Branch.as
public function bend():void{
var windAngle:Number = radToDeg(Math.atan2(mouseY + endPoint.y, mouseX + endPoint.x)) - 180;
var distX:Number = mouseX + endPoint.x;
var distY:Number = mouseY + endPoint.y;
var strength:Number = Math.sqrt(distX*distX + distY*distY);
if(windAngle - angle < -180){
windAngle += 360;
} else if(windAngle - angle> 360){
windAngle -= 360;
}
if(Math.abs(windAngle - angle) > 1){
angle += (windAngle - angle)/strength;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch()
}
positionChildren();
}
Additionally, larger branches should not bend as much as smaller ones since they are thicker and less flexible. The variable strength can be truncated based on the branch's scale:
var strength:Number = (Math.sqrt(distX*distX + distY*distY)*scaleX*scaleX)/5;
Multiplying the distance by scaleX twice exaggerates the difference in how much a smaller branch bends compared to how much a larger branch bends. Dividing it all by 5 decreases the overall strength of the wind.
I have one final adjustment before calling it a day. The wind is a little too steady for my taste. Adding some turbulence will make it a little more realistic. Update the generateWind() function of Tree to look like this:
// Tree.as
private function generateWind(e:Event):void{
var turbulence:Number = Math.random()*40 - 20;
trunk.bend(turbulence);
}
turbulence is a random number between -20 and 20. Pass this to bend(). In bend() add turbulence to windAngle:
// Branch.as
public function bend(turbulence:Number):void{
var windAngle:Number = radToDeg(Math.atan2(mouseY + endPoint.y, mouseX + endPoint.x)) - 180;
windAngle += turbulence;
...
Don't forget to pass it on to positionChildren() at the end of bend()...
positionChildren(turbulence);
...and to bend() again in positionChildren():
child.bend(turbulence);
The final classes should look like this:
// Tree.as
package {
import flash.display.*;
import flash.geom.Point;
import flash.events.*;
public class Tree extends MovieClip{
private var trunk:Branch;
public function Tree(){
trunk = new Branch(-90, 1, new Point(stage.stageWidth/2, stage.stageHeight), this);
stage.addEventListener(MouseEvent.MOUSE_DOWN, startWind);
stage.addEventListener(MouseEvent.MOUSE_UP, endWind);
addEventListener(Event.ENTER_FRAME, resist);
}
private function resist(e:Event):void{
trunk.returnToRest();
}
private function startWind(e:MouseEvent):void{
addEventListener(Event.ENTER_FRAME, generateWind);
}
private function endWind(e:MouseEvent):void{
removeEventListener(Event.ENTER_FRAME, generateWind);
}
private function generateWind(e:Event):void{
var turbulence:Number = Math.random()*40 - 20;
trunk.bend(turbulence);
}
}
}
// Branch.as
package {
import flash.display.*;
import flash.geom.Point;
public class Branch extends Shape{
// class vars
private static const baseLength:Number = 100;
private static const minScale:Number = 0.2;
private static const slope:Number = 0.8;
// instance vars
private var angle:Number;
private var endPoint:Point;
private var globalEndPoint:Point;
private var thisParent:MovieClip
private var children:Array = [];
public var restAngle:Number;
public function Branch(initialAngle:Number, scale:Number, origin:Point, parentClip:MovieClip){
angle = restAngle = initialAngle;
x = origin.x;
y = origin.y;
thisParent = parentClip;
scaleX = scaleY = scale;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch();
thisParent.addChild(this);
if(scale*slope > minScale){
sproutBranches(scale*slope);
}
}
private function sproutBranches(scale:Number):void{
// new branch angles are augmented by a random +- 10 degrees
var branchLeft:Branch = new Branch(angle - 30 + Math.random()*20 - 10, scale, globalEndPoint, thisParent);
var branchRight:Branch = new Branch(angle + 30 + Math.random()*20 - 10, scale, globalEndPoint, thisParent);
children.push(branchLeft);
children.push(branchRight);
}
public function bend(turbulence:Number):void{
var windAngle:Number = radToDeg(Math.atan2(mouseY + endPoint.y, mouseX + endPoint.x)) - 180;
windAngle += turbulence;
var distX:Number = mouseX + endPoint.x;
var distY:Number = mouseY + endPoint.y;
var strength:Number = (Math.sqrt(distX*distX + distY*distY)*scaleX*scaleX)/5;
if(windAngle - angle < -180){
windAngle += 360;
} else if(windAngle - angle> 360){
windAngle -= 360;
}
if(Math.abs(windAngle - angle) > 1){
angle += (windAngle - angle)/strength;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
drawBranch()
}
positionChildren(turbulence);
}
private function positionChildren(turbulence:Number):void{
globalEndPoint = localToGlobal(endPoint);
for each(var child:Branch in children){
child.x = globalEndPoint.x;
child.y = globalEndPoint.y;
child.bend(turbulence);
}
}
private function drawBranch():void{
graphics.clear();
graphics.moveTo(0, 0);
graphics.lineStyle(10, 0x000000);
graphics.lineTo(endPoint.x, endPoint.y);
}
public function returnToRest():void{
if(Math.abs(restAngle - angle) > 1){
angle += (restAngle - angle)/8;
endPoint = getEndPoint();
globalEndPoint = localToGlobal(endPoint);
// redraw
drawBranch();
}
unbendChildren();
}
private function unbendChildren():void{
globalEndPoint = localToGlobal(endPoint);
for each(var child:Branch in children){
child.x = globalEndPoint.x;
child.y = globalEndPoint.y;
child.returnToRest();
}
}
// Using angle and baseLength as polar coordinates get the endPoint of the line as cartesian coordinates.
private function getEndPoint():Point{
return Point.polar(baseLength, degToRad(angle));
}
// convert degerees to radians
private static function degToRad(deg:Number):Number{
return deg/57.3;
}
// convert radians to degrees
private static function radToDeg(rad:Number):Number{
return rad*57.3;
}
}
}
Now the tree quivers in the wind as well.
Bonus
Here are a few screenshots I took during the development of this movie. These are all unexpected results from testing and debugging. This demonstrates how tweaking one or two variables can result in vastly different patterns. There is much room for exploration here.
This article was originally published on sethmabbott.com.
The Google Maps API is without a doubt one of the most popular APIs available on the web today. It offers webmasters the ability to add mapping functionality, similar to maps.google.com, to their websites. All of Google Maps functionalities, including markers, directions, zoom options and satellite view are only a few lines of code away. But before we get started with the code portion, you will need a Google Maps API key.
Getting a Google Maps API key takes a minute and costs nothing. Just sign in to your Google Account (I am sure you have one) and head to http://code.google.com/apis/maps. On the right hand side you will see ‚How Do I Start, where you will just simply click on Step 1 "Sign up for a Google Maps API key". Then enter your site's live domain, accept the terms and click the "Generate API Key" button.
On the next page your unique API key will appear. Make sure that you copy and paste it to a safe location. Below the API key will be JavaScript examples to quickly get you started. In this tutorial, we will generate similar code with the use of some rails plug-ins. The most popular plug-in is the Yellow Maps for Ruby, also known as YM4R. Since there are other mapping APIs, this plug-in comes in a few flavors. Since we're doing Google Maps, we will use YM4R/GM.
1. Lets start by generating a new Rails application:
rails map_example -d mysql2. Create a 'map_example_development' database and remove index.html from the public folder
3. The lets add a controller and the index action
ruby script/generate controller location index4. Lets install the YM4R/GM plugin
ruby script/plugin install git://github.com/queso/ym4r-gm.git5. Add frontend.html.erb in app/views/layouts with the following code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Google Maps Rails Example</title>
<%= GMap.header %>
<%= @map.to_html unless @map.blank? %>
</head>
<body>
<%= yield %>
</body>
</html>
Note that we have included two YM4R/GM lines in the head section. The GMap.header will include needed JavaScript files while the @map.to_html line generates JavaScript based on the parameters passed into the map object. We will be creating the @map object in our location controller.
6. Don't forget to specify the layout in app/views/location_controller:
layout "frontend"
7. Inside of the index action, add the following code:
coordinates = [41.8921254,-87.6096669]
@map = GMap.new("map")
@map.control_init(:large_map => true, :map_type => true)
@map.center_zoom_init(coordinates,14)
@map.overlay_init(GMarker.new(coordinates,:title => "Navy Pier", :info_window => "Navy Pier"))
First we set our map coordinates into an array. The first number is the latitude and second is the longitude. These coordinates point to Navy Pier in Chicago, Illinois. If you would like to try a different area for this example, this easy tool (http://stevemorse.org/jcal/latlon.php) provides you the latitude and longitude for any address you want to use..
Next, we set the @map object to pass into our view. The @map object contains a new instance of the GMap class; the "map" string is the id of the div that will contain the map. The next line activates controls for our map. The large map activates the zoom option while the map type activates various views such as a regular map, satellite or terrain. Then we set the default map center and zoom. We first pass in the coordinates array so our location will be centered, then we pass in a integer to determine zoom level. This integer can be between 0 and 22, the higher the number the more the zoomed in the map will be. Finally, we add a marker overlay. The first argument is the coordinates and it is the only required argument need to add a marker. The title and info_window are both optional, title is the text that will display when your mouse is idle over the marker, while info_window is the text that will appear in the pop up box that appears after clicking the marker.
8. Next open our view file located at app/views/location/index.html.erb and add the following:
<h1>Google Maps Rails Example</h1>
<%= @map.div(:width => 800, :height => 500) %>
9. Map the homepage to the index action of location by adding the following to config/routes.rb.
map.root :controller => "location", :action => "index"
10. Fire up your rails application and a map should appear.
Currently our map is static and the location can't change. It would be nice to have a option to change the location through a text field. Since it is a text field, the user will be able to type in any possible location and expect our application to map it. We're going to have to convert user-inputted text into map coordinates. Sounds complicated, luckily there is another rails plug-in that can be of help here and that plug-in is Google-Geocoder. Google Geocoder will find
11. Install the Google-Geocoder plugin:
ruby script/plugin install git://github.com/tobstarr/google-geocoder.git12. Next add the form to the bottom of the view:
<br />
<% form_for "new_location", :url => location_index_path, :html => { :method => :post } do %>
<label>Enter a new Location to map:</label><br />
<%= text_field_tag :new_location %>
<%= submit_tag "Map It" %>
<% end %>
13. We will then add the create action to the location controller:
def create
new_location = params[:new_location]
gg = GoogleGeocode.new("Your API Key Here")
gg_locate = gg.locate(new_location)
coordinates = [gg_locate.latitude, gg_locate.longitude]
@map = GMap.new("map")
@map.control_init(:large_map => true, :map_type => true)
@map.center_zoom_init(coordinates,14)
@map.overlay_init(GMarker.new(coordinates, :title => new_location, :info_window => new_location))
render :action => "index"
end
A few lines of the create action is similar to the index action. First we set the new_location variable to the value of the new location parameter. Next we pass in the Google Maps API key to a new instance of the GoogleGeocode class, this will allow us to run methods such as "locate" which will return coordinate information. Then we set the coordinate array and the rest is familiar territory. The only difference is we are setting the title and info window text to the user input and finally we reuse the index template.
That's it for my Google Maps on rails tutorial, we were able to get a functioning map by using the YM4R plug-in. Then we were able to map user-inputted locations by using the Google Geocoder plug-in. You could easily expand this example by giving the users the ability to map multiple locations at once or dry up similar code used in our index and create methods.
What's Your FatNum?
Numbers are a big deal. Whether you're talking about business, family, fun, personal goals, a lot can be simply boiled down to one big, fat number...
- How many people registered for my app?
- How many batches are left in my database import?
- How many days left until my birthday?
- How many pounds have I lost on my diet?
- And so on...
So what's the big, fat number that you need to keep track of? Whatever it is, FatNum.com can help.
What is FatNum.com?
FatNum.com lets you signup and start tracking your FatNums right now! Right away you are given a simple Web interface for creating your FatNums and keeping them up to date. Each FatNum is assigned a five-character code, so you can share your FatNum with others discreetly without having to log in.
For all you programmers who want their Ruby apps to share data with FatNum.com, I've built this RubyGem for easily accessing and updating your FatNums. From the documentation:
require 'rubygems' require 'fat_num' f = FatNum.new('your@email.com', 'password') # get your statistic response = f.get('e37gh') response.digit #=> 120 response.description #=> 'batches left' # update your statistic f.update('e37gh', 119) # sure enough, our update was successful f.get('e37gh').digit #=> 119
There's also some love for all you Mac users out there -- download the FatNum Dashboard Widget and easily track your FatNums from the comfort of your OSX Dashboard!
Any More Examples?
Here are a couple FatNums that FatNum.com is using for itself:
FatNum.com is super simple, almost to a ridiculous degree, but I hope you can find some imaginitive uses for it!

