HTML5 Canvas Drawing Library Exploration: Easel.js

I’ve spent the last few weeks, at home and at work, exploring some of the capabilities of HTML 5. The most exciting one for me, at the moment, is the Canvas element, which allows dynamic creation and updating of graphics within a web page. This type of functionality is useful for creating games, data visualizations, custom user interfaces, and most anything else that traditionally is considered to be in the realm of Adobe Flash.

In exploring the various tutorials I discovered that many people have put a lot of time into building Javascript libraries which add functionality to the canvas tag, making it more Flash-like. A side effect of this is that these libraries usually – though not always – simplify the coding process dramatically. What could take many hours and many hundreds of lines of custom code can now be accomplished in a short amount of time and relatively little code.

So starting with this post, and going on for the next few weeks, I will be posting examples of these libraries in use, along with source code and commentary. A note here: The examples will only work in browsers which support the canvas tag – Internet Explorer 9+, Firefox, Safari or Chrome on Windows, and Safari and Chrome on Macs, and Chrome and Konqueror on Linux. These examples should also work on most newer mobile devices using their default web browsers. However, things might be a little slow and clunky.

For this post, I am working with Easel.js (current version 0.4), a canvas drawing library created by Flash guru Grant Skinner.

When I learn a new language or technology, I consider the first sign of success to be when I manage to get a bunch of objects to spin in a circle. The canvas tag libraries are no different. For this, and the next several, I have re-created the Trigonometron (which seems to have not made the transition to the new site…), one of my proudest moments as a Flash developer.

Click to launch the demo.

Detailed breakdown of the code follows:

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Easel.js demo</title>
		<style>
			* {margin:0;padding:0;}
			body {background:#ffffff;margin:0;padding:0;overflow:hidden;}
			#myCanvas {position:absolute;left:0;top:0;width:800px;height:480px;background:#ffffff;}
		</style>
		<script type="text/javascript" src="easel.js"></script>
		<script type="text/javascript" src="easel-demo-code.js"></script>
	</head>
	<body>
		<canvas id="myCanvas" width="800" height="480"></canvas>
	</body>
</html>

Nothing particularly amazing about this HTML. Note that I used the width and height attributes in the canvas tag, rather than simply relying on the style sheet. This is a quirk of Easel.js – if the width and height attributes are not set, then it will visibly skew and stretch the graphics inside the canvas tag. Hopefully this will be addressed in an upcoming release.

Javascript

/*	global variables	*/
var canvas,
	stage,
	currentShape="circle",
	isPaused = true,
	isInitialized = false;

/*	math and positioning	*/
var shapes = ["circle","tricuspoid","tetracuspoid","epicycloid","epicycloid 2","epicycloid 3","lissajous","lemniscate","butterfly"];
var w = 800;
var h = 480;
var centerX = 240;
var centerY = 240;
var radius_x = 150;
var radius_y = 150;
var theta = 0;
var objects = [];
var numObjects = 0;
var r2d = 180/Math.PI;
var d2r = Math.PI/180;
var orbitSteps = 180;
var orbitSpeed = Math.PI*2/orbitSteps;
var objectInterval;// = orbitSteps/numObjects;
var objectPosition;
var direction = 1;
var index = 0;
var xVar1 = 0;
var xVar2;
var xVar3;
var xVar4;
var startingObjects = 100;
var newX;
var newY;

onload = initEaselDemo;

function initEaselDemo() {
	canvas = document.getElementById("myCanvas");
	stage = new Stage(canvas);
	stage.enableMouseOver(36)
	stage.mouseEnabled = true;
	
	initInterface();
	initObjects();
	
	//	set 
	Ticker.setFPS(36);
	Ticker.addListener(window);
	tick();
	isInitialized = true;
}

function initInterface() {
	var interfaceBase = new Container();
	interfaceBase.x = 630;
	interfaceBase.y = 20;
	for(var i=0;i<shapes.length;i++) {
		var b = new Container();
		b.mouseEnabled = true;
		b.shape = shapes[i];
		var g = new Graphics();
		g.setStrokeStyle(1)
			.beginStroke('#808080')
			.beginFill('#ededed')
			.rect(0,0,150,20);
		var s = new Shape(g);
		s.x = 0;
		s.y = 0;
		
		var t = new Text(shapes[i],"12px Courier","#000000");
		t.textAlign="center";
		t.x = 75;
		t.y = 14;
		
		b.addChild(s);
		b.addChild(t);
		b.x = 0;
		b.y = i*23;
		b.onMouseOver = function(e) {
			document.getElementById("myCanvas").style.cursor="pointer";
			e.target.getChildAt(0).graphics.beginFill("#ffffff").rect(0,0,150,20);
			if(isPaused) stage.update();
		}
		b.onMouseOut = function(e) {
			document.getElementById("myCanvas").style.cursor="auto";
			e.target.getChildAt(0).graphics.beginFill("#ededed").rect(0,0,150,20);
			if(isPaused) stage.update();
		}
		b.onClick = function(e) {
			currentShape = e.target.shape;
			isInitialized = false;
			tick();
			isInitialized = true;
		}
		
		interfaceBase.addChild(b);
		
		
		
	}
	var ppb = new Container();
	ppb.x = 0;
	ppb.y = (shapes.length*23+10);
	var ppg = new Graphics()
		.setStrokeStyle(1)
		.beginStroke('#808080')
		.beginFill('#ededed')
		.rect(0,0,150,20);
	var pps = new Shape(ppg);
	pps.x = 0;
	pps.y = 0;
	var ppt = new Text("PLAY/PAUSE","12px Arial,sans-serif","#000000");
	ppt.textAlign = "center";
	ppt.x = 75;
	ppt.y = 14;
	ppb.addChild(pps);
	ppb.addChild(ppt);
	ppb.onMouseOver = function(e){
		document.getElementById("myCanvas").style.cursor="pointer";
		e.target.getChildAt(0).graphics.beginFill("#ffffff").rect(0,0,150,20);
		stage.update();
	}
	ppb.onMouseOut = function(e){
		document.getElementById("myCanvas").style.cursor="auto";
		e.target.getChildAt(0).graphics.beginFill("#ededed").rect(0,0,150,20);
		stage.update();
	}
	ppb.onClick = function() {
		isPaused = !isPaused;
	}
	interfaceBase.addChild(ppb);
	
	stage.addChild(interfaceBase);
}
function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
	var g = new Graphics()
		.setStrokeStyle(1)
		.beginStroke("#000000")
		.beginFill("#"+randomRGB()+randomRGB()+randomRGB())
		.drawCircle(0,0,5)
		.endFill();
	var s = new Shape(g);
	s.x = centerX;
	s.y = centerY;
	objects.push(s);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
	stage.addChild(s);
}
function removeObject() {
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}

function randomRGB(){
	var s = Math.floor(Math.random()*256).toString(16);
	if(s.length==1) s = "0"+s;
	return s;
}
function tick() {
	if(isPaused==true && isInitialized==true) return;
	for(var i = 0; i < numObjects; i++) {
		objectPosition = orbitSpeed*objectInterval*i;    //    each object is individually updated
		switch(currentShape) {
			case "circle":
				newX = centerX + radius_x * Math.cos(theta + objectPosition);
				newY = centerY + radius_y * Math.sin(theta + objectPosition);
				break;
			case "tricuspoid":
				newX = centerX + (radius_x*.5) * ((2 * Math.cos(theta + objectPosition)) + Math.cos(2 * (theta + objectPosition)));
				newY = centerY + (radius_y*.5) * ((2 * Math.sin(theta + objectPosition)) - Math.sin(2 * (theta + objectPosition)));
				break;
			case "tetracuspoid":
				newX = centerX + radius_x * Math.pow((Math.cos(theta + objectPosition)),3);
				newY = centerY + radius_y * Math.pow((Math.sin(theta + objectPosition)),3);
				break;
			case "epicycloid":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 1) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 1) * (theta + objectPosition)));
				break;
			case "epicycloid 2":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 2) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 2) * (theta + objectPosition)));
				break;
			case "epicycloid 3":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 3) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 3) * (theta + objectPosition)));
				break;
			case "lissajous":
				newX = centerX + radius_x * (Math.sin(3 * (theta + objectPosition) + xVar1));
				newY = centerY + radius_y * Math.sin(theta + objectPosition);
				xVar1 += .002;
				break;
			case "lemniscate":
				newX = centerX + (radius_x*1.2) * ((Math.cos(theta + objectPosition)/(1 + Math.pow(Math.sin(theta + objectPosition),2))));
				newY = centerY + (radius_y*1.2) * (Math.sin(theta + objectPosition) * (Math.cos(theta + objectPosition)/(1 + Math.pow(Math.sin(theta + objectPosition),2))));
				break;
			case "butterfly":
				newX = centerX + (radius_x*.4) * (Math.cos(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				newY = centerY + (radius_y*.4) * (Math.sin(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				break;
			default:
				break;
	
		}
		objects[i].x = newX;
		objects[i].y = newY;
	}
	theta += (orbitSpeed*direction);
	stage.update();
}

Here is a line-by-line breakdown of what is going on in the above code:

  • 2 – 6
    • Declare the global variables for Easel.js, and set the initial environment for the trigonometron code.
  • 9 – 33
    • These are all variables for the trigonometry functions. They are not specific to Easel.js
  • 35:  onload = initEaselDemo
    • This line tell the browser to, once the page is finished loading, run the method called “initEaselDemo()”
  • 37-51 initEaselDemo();
    • This method initializes the global variables.
      • 38 – assign the variable canvas to the HTML element with the ID “myCanvas”
      • 39 – initialize the stage object
      • 40 – tell the stage to listen for mouseover events 36 times a second
      • 41 – allow the stage to register mouse events
      • 43 – initialize the user interface for the trigonometron
      • 44 – initialize the animated circles
      • 47-49 – initialize the Ticker object, which allows frame-based animation
      • 50 – everything is ready to begin
  • 53-134 initInterface();
    • This method creates all of the user control elements, styles them and adds text, and assigns mouse event listeners
      • 54-56 – create and position a container for all of the user control elements
      • 57-100 – iterate through all of the elements in the shapes[] array, and create a button for each
        • 58 – create a container for the button elements
        • 59 – enable mouse events on the button
        • 60 – b.shape is a custom attribute I am adding so that arbitrary values can be assigned to each button
        • 61-65 – create a graphic element, and use it to draw a rectangle
        • 66-68 – create a “shape” display object, and use it to display the graphic element
        • 70-73 – create, position, populate, and style a text object.
        • 75-76 – add the button graphic and the text object to the button container
        • 77-78 – position the button element
        • 79-83 – add onMouseOver functionality to the button – change the cursor to a pointer, change the button color, and update the stage to display the changed button
        • 84-88 – add onMouseOut functionality to the button; essentially identical to the onMouseOver functionality
        • 89-94 – add onClick functionality – switch to different shape for the animation, and show the new animation
        • 96 – add the button to the user control container
        • 101-103 – create and position a container for the elements of the play/pause button
        • 104-108 – create a graphic elements, and use it to draw a rectangle
        • 109-111 – create a “shape” display object, and use it to display the graphic element
        • 112-115 – create, position, populate, and style a text object
        • 116-117 – add the rectangle and the text to the play/pause button element container
        • 118-130 – add mouseover, mouseout, and click functionality to the play/pause button
        • 131 – add the play/pause button to the user control container
        • 132 – add the user control container to the stage
  • 135-139 initObjects();
    • This method counts up to the value of startingObjects and calls addObject once for each iteration
  • 140-154 addObject();
    • This method creates a circle and adds it to the display list
      • 141-146 – create a new circle graphic
      • 147-149 – create and position a display object for the circle
      • 150 – add the circle to the array of circles
      • 151 – update the total number of created circles
      • 152 – adjust the spacing of all the circles in the animation, based on the new number of circles
      • 153 – add the circle to the stage
  • 155-158 removeObjects();
    • this method removes objects from the display list. It is not currently being used
  • 160-164 randomRGB();
    • This method creates a random hexadecimal number between 0 and 255, converts it to a string, and returns it for use in coloring the circles
  • 165-216 tick();
    • This built-in Easel.js method is called based on the Ticker.setFPS() method call on line 47. It iterates through all of the circles in the objects[] array, and updates their positions on the stage, based on the current curve shape
      • 166 – if the trigonometron is paused, do nothing
      • 167- 210 – iterate through the objects, and based on the current curve shape, create new X and Y coordinates for each
      • 211-212 – update the position of each object to match its new coordinates
      • 214 – iterate theta, which is how much the circle should turn on each frame
      • 215 – update the stage to display the changed positions of the objects

So there it is: the entirety of the code for this Easel.js demo, line by line. I left out explanations of the math which calculates the positions of the objects; you can read more on that subject at my Simple Trigonometric Curves tutorial over on Kongregate.com.

Enjoy!

On Walking to Work

For most of my career as a developer – say, nine of the past twelve years – I have lived within two miles of my workplace. Cybernet, BBK/PeopleDesign and now, Cynergy. On heavy traffic days it is actually faster for me to walk to work than to drive. Even on good days, driving saves me, at best, ten minutes in each direction. When weather permits, I ride my bike.

But I like best to walk. Especially in the morning, when the city is still waking up. The best days are in the cold parts of the year when the sun is just hitting the tops of the highest trees and buildings. Those are also the days when I walk home after dark.

Biking is more efficient, certainly, but – weather aside – I trust the drivers on the road less than during brighter parts of the year. There are fewer bikers from November through March, so drivers are even less aware of them (us) than usual. I can then choose to slalom quickly on the streets, slowly on the sidewalk, or just walk.

Currently I am exactly a mile and a half from work. The walk takes a little less than half an hour each way. Call it a total of fifty minutes a day, for three miles. Fifteen miles a week, and slightly over four hours. Sometimes I will stop for coffee at MadCap or the Grand Central Market. On the way home, I will often swing by the library. Sometimes I will stop back at the Grand Central Market for a sandwich.

The smell of the city changes from block to block and from month to month. In the summer, the city smells green and steamy. In the winter, earth and smoke. Currently, in the morning the scent trail goes something like this: leaves, earth, bread baking, pavement, car exhaust, bus fumes, cigarette smoke, concrete, pancakes, coffee, river, and occasionally hops from one of the local breweries. Each day is unique as a fingerprint.

This is the last work day of the year. Since i started this job on August 22, I have driven to work exactly twice. Call it 18 weeks. Or 17, when holidays are removed. So 17 weeks, five work-days a week, three miles a day. 255 miles. Or in my Subaru, a full tank of gas. Extrapolate it out and it is around 750 miles a year of using alternate transportation.

And the best part is that I feel more connected with Grand Rapids than I have in years. Working in front of a computer for 8+ hours a day, even in an office full of good people, is kind of alienating. Walking brings me back to earth.

Accessing Views From Flash in Drupal Gardens

Wow, life can be whacky.

So I am pretty sold on Drupal Gardens as a base for most of my future development projects. However, there are still a couple of useful pieces missing, that are out-of-the-box available on a full Drupal install. Specifically, the Services module. Services allow alternate means of accessing data stored in the Drupal database. So if I want to, say, use Drupal as a content management system for a Flash game, I can just pull information in as and when I need it, by referencing a URL and passing the appropriate parameters.

Drupal Gardens doesn’t allow that yet. Well, technically they do, but only in very specific ways, none of which are ideal for using Drupal as a straightforward CMS. Having said that, apparently adding this functionality is something they are looking into.

But enough carping! It is possible to use DG as a CMS for a Flash (or AJAX, or Silverlight, etc) application, provided you only need to pull content out of the database, not put it back in. It just takes a bit of a work-around to get everything up and running.

Note: The rest of this post assumes a familiarity with Actionscript, Drupal Views and RSS feeds. If not, take a few minutes to read up on them.

The Views module allows the contents of a view to be published as a Page, as a Block, or as an RSS feed. Say I am making a role-playing game, and I need to populate the world therein with wildlife. To keep things simple, each critter has the following information points:

  • name
  • description
  • terrain (where the creature might be encountered)
  • associated element ( from the classic 5, for combat purposes)

So after entering data for several animals, you would end up with a table which looks something like this:

Name Description Terrain element
squirrel cute, fluffy, voracious forest wood
camel cute, fluffy, spits a lot desert water
walrus truly, nature’s most majestic animal plains earth

…at least, that’s how it looks in the database. To get it into Flash (in Drupal Gardens, at present) requires a little more work. But not a lot more work.

Pulling the RSS feed of a view is quite simple. Just create a “feed” version of an existing view, and set up a URL path for it, and voila! You have a  feed of the contents of a view.

However, note that the actual contents of a view, when delivered in an RSS feed, are all packed into the <description> tag of the view, and include all of the HTML which would normally be rendered in the page. It looks something like this:

 

  http://ecceludum.drupalgardens.com/feeds/monsters/terrain/desert
    
    en
          http://ecceludum.drupalgardens.com/content/camel
    
&lt;div class=&quot;field field-name-body field-type-text-with-summary field-label-hidden&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item even&quot; property=&quot;content:encoded&quot;&gt; 
&lt;p&gt;camel text&lt;/p&gt; 
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;field field-name-field-monster-element field-type-taxonomy-term-reference field-label-above&quot;&gt;
&lt;div class=&quot;field-label&quot;&gt;element:&nbsp;
&lt;/div&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item even&quot;&gt;
&lt;a href=&quot;/elements/water&quot; typeof=&quot;skos:Concept&quot; property=&quot;rdfs:label skos:prefLabel&quot;&gt;Water
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;field field-name-field-monster-terrain field-type-taxonomy-term-reference field-label-above&quot;&gt;
&lt;div class=&quot;field-label&quot;&gt;terrain:&amp;nbsp;
&lt;/div&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item even&quot;&gt;
&lt;a href=&quot;/terrain/desert&quot; typeof=&quot;skos:Concept&quot; property=&quot;rdfs:label skos:prefLabel&quot;&gt;desert
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

     Thu, 15 Dec 2011 18:22:47 +0000
 John Winkelman
 71 at http://ecceludum.drupalgardens.com
 http://ecceludum.drupalgardens.com/content/camel#comments
  
  

All well and good, except boy, are the contents of the DESCRIPTION tag ugly. That is because the  angle brackets, ampersands, quote marks and non-breaking spaces have  been re-written as plain text character entities. In a sense, they have been rendered as a picture of source code, rather than source code. There is a simple fix for this: When Flash pulls in an RSS feed (or any other XML-based document) it pulls this content in as plain text. It is not structured as XML until after it is loaded into the Flash movie. This means that the string can be parsed internally, and all of the character entities turned into their respective text elements. E.g. each instance of “&gt;” can be replaced with a “>”. The simplest way might be the following line of code:

var newString = oldString.split("&lt;").join("<").split("&gt;").join(">").split("&quot;").join("\"").split("&nbsp;").join(" ");
var myXML = new XML(newString);

If that is run on the preceding big ugly bit of RSS feed, then the contents of the DESCRIPTION tags would suddenly look like this:

<div class="field field-name-body field-type-text-with-summary field-label-hidden">
<div class="field-items">
<div class="field-item even" property="content:encoded"> 
<p>camel text</p> 
</div>
</div>
</div>
<div class="field field-name-field-monster-element field-type-taxonomy-term-reference field-label-above">
<div class="field-label">element: 
</div>
<div class="field-items">
<div class="field-item even">
<a href="/elements/water" typeof="skos:Concept" property="rdfs:label skos:prefLabel">Water
</a>
</div>
</div>
</div>
<div class="field field-name-field-monster-terrain field-type-taxonomy-term-reference field-label-above">
<div class="field-label">terrain:&nbsp;
</div>
<div class="field-items">
<div class="field-item even">
<a href="/terrain/desert" typeof="skos:Concept" property="rdfs:label skos:prefLabel">desert
</a>
</div>
</div>
</div>

Now the contents of the DESCRIPTION element of the RSS feed XML are structured and can be parsed using the Flash XML tools. In this instance, you would look for the contents of the elements div.field-name-field-monster-element a, div.field-name-field-monster-terrain a, and div.field-name-body p.

You will note that there are a lot of extra DIV tags, and many of them have huge long class names and/ir IDs. This is not a problem for two reasons: first, the data is well-structured, and second, it is consistent. Unless there are changes to the structure of the View in Drupal Gardens – e.g. changing the “terrain” data field to be called “territory” instead – every time the data is pulled from this RSS feed for this View, it will have exactly the same structure. Having five records or ten thousand won’t change things.

So there it is: An overview of how to access Views information from a Flash movie, via RSS, in Drupal Gardens.

Wikipedia as a Seed for Flash Fiction

Lacking for a creative outlet, I recently tried a new writing exercise:

  1. go to the front page of Wikipedia
  2. click “random article”
  3. research/plan for a maximum of one minute
  4. write for fifteen minutes, or 500 words, or one page, or some other arbitrarily small limit

You must use the first random page that comes up, and the subject of that random page must be integral to the story. No picking and choosing.

It actually turned out to be a lot of fun! In one afternoon I wrote short pieces about a coastal town in Kenya, an early 20th century ceramics artist, data compression, and a Dungeons and Dragons version of Hell. All told, a little over an hour of writing. My mind felt limbered up and cleared out. Dusted off, even; like hitting the gym after an extended absence. My favorite part was that it broke me out of my comfort zone. I know next to nothing about Kenya, or ceramics. But that is still more than I knew before I tried this exercise.

In a way, this could work as a brainstorming exercise for an external topic. Trying to fit disparate ideas into a common narrative creates new viewpoints for that narrative. Imagine writing a story about wineries of the Great Lakes region, and when stuck for inspiration, randomly hitting the following five pages: Inger GiskeødegÃ¥rd, Tahuna Breaks, Vincent HallinanList of Pittsburgh Pirates first-round draft picks, and Telephone numbers in the British Indian Ocean Territory. For each page, try to fit the the content or concept into the larger narrative. Completely random brainstorm. Let your mind go where it will. At the end of the exercise, go back through and see if there are any useful insights; any new and unusual ways of thinking about Great Lakes wine.

 

Procedural Generation X – User Modified Content

Having all the content in a game created dynamically simplifies many aspects of game design. One code base can potentially create an infinite number of unique gaming experiences. However, what if you want to include a save game feature? If the game content is created anew every time the game is loaded, how is it possible to close the game, then pick it up tomorrow without losing all my progress?

Fortunately, the very act of creating locations and objects can be used to allow data to persist across multiple sessions. Here is an example:

Imagine you have created a dungeon crawl in a procedurally generated cave. The player has the option to dig through walls to reach e.g. deposits of minerals. You want to have a save-game feature, but you don’t want to have the caves reset to new every time the game is reloaded.

Every location in the cave has a unique x/y coordinate, starting at the upper left corner with (0,0) and ending at the lower right with (63,63). Each of these points is either a wall or a floor tile. Now here is the brain-twisty part: you don’t need to store the individual tiles of the original state. Every time the tile map is regenerated, it will be exactly the same. It doesn’t need to be stored.

Your character blows up a chunk of wall, say, at (20,20). Suddenly, the tile map has changed. It has history. It exists in a state different from the one which was produced by the algorithm which created it. Does that mean the entire map needs to be saved now? No! The only piece which needs to be saved is the piece which has changed. And this can be done by creating a data file which saves only the changed pieces of the map.

{
x:20,
y:20,
z:0
terrain:0
}

Reading the above data, we can see that the map coordinate 20,20 on level 0 should be to the terrain-type 0. So a workflow would look something like this:

  1. generate the initial map data
  2. look for a save game file
  3. iterate through the save game file and, where necessary, change the tiles.
  4. render the map to the screen

 

Away3d and Flash Player 11 – Source Code

Here is the source code for the Flash experiment I posted on Thursday.

package
{
	import away3d.containers.ObjectContainer3D;
	import away3d.containers.View3D;
	import away3d.debug.AwayStats;
	import away3d.entities.Sprite3D;
	import away3d.filters.BloomFilter3D;
	import away3d.filters.BlurFilter3D;
	import away3d.filters.DepthOfFieldFilter3D;
	import away3d.lights.DirectionalLight;
	import away3d.lights.LightBase;
	import away3d.lights.PointLight;
	import away3d.materials.BitmapMaterial;
	import away3d.materials.ColorMaterial;
	import away3d.materials.methods.FogMethod;
	import away3d.primitives.Cube;
	import away3d.primitives.Plane;
	import away3d.primitives.Sphere;
	
	import flash.display.BitmapData;
	import flash.display.BlendMode;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.geom.ColorTransform;
	import flash.geom.Vector3D;
	import flash.utils.getTimer;
	
	/**
	 * Cubes01 Stage3D Demo by Felix Turner - www.airtight.cc
	 * Modified by John Winkelman - www.eccesignum.org
	 */
	
	[SWF( backgroundColor='0xffffff', frameRate='60', width='800', height='600')]
	public class Cubes02 extends Sprite
	{
		private var CUBE_COUNT:int = 500;
		private var MATERIAL_COUNT:int = 30;
		private var CUBE_SIZE:int = 10;
		
		private var view : View3D;
		private var cubes:Array = [];
		private var cubeHolder : ObjectContainer3D;
		
		private var theta:Number = 0;  
		private var radius:Number = 150;
		private var orbitSteps:Number = 1000; // number of steps necessary to complete one orbit
		private var orbitSpeed:Number = Math.PI*2/orbitSteps; //amount by which theta will be incremented at each interval
		private var objectInterval:Number = orbitSteps/CUBE_COUNT;	//	distance between objects on the curve
		private var objectPosition:Number;   
		private var direction:Number = 1;	//	or -1 - controls direction of orbit
		public function Cubes02(){
			super();
			
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			
			//init 3D world
			view = new View3D();
			view.camera.z = -1000;
			addChild(view);
			//init object to hold cubes and rotate
			cubeHolder = new ObjectContainer3D();
			view.scene.addChild(cubeHolder);
			
			//add lights
			var light:PointLight = new PointLight();
			light.position = new Vector3D(-1000,1000,-1000);
			light.color = 0xffeeaa;
			view.scene.addChild(light);
			
			var light2:PointLight = new PointLight();
			light2.position = new Vector3D(1000,1000,1000);
			light2.color = 0xFFFFFF;
			view.scene.addChild(light2);
			
			//init materials
			var materials:Array = [];
			
			for (var i:int = 0 ; i < MATERIAL_COUNT; i ++ ){
				var material : ColorMaterial = new ColorMaterial(Math.random()*0xFFFFFF,1);
				//material.blendMode = BlendMode.ADD;
				material.lights = [light,light2];
				materials.push(material);
			}
			
			
			for (var j:int = 0 ; j < CUBE_COUNT; j ++ ){
				var s:Number = CUBE_SIZE;
				var cube:Cube = new Cube(materials[j % MATERIAL_COUNT], s,s,s);
				cubeHolder.addChild(cube);
				cube.x = 0;
				cube.y = 0;
				cube.z = 0;
				cubes.push(cube);
				
			}
			
			//add stats
			addChild(new AwayStats(view));
			
			this.addEventListener(Event.ENTER_FRAME, onEnterFrame);
			stage.addEventListener(Event.RESIZE, onStageResize);
			onStageResize(null);
		}
		
		private function onStageResize(event : Event) : void{
			view.width = stage.stageWidth;
			view.height = stage.stageHeight;
		}
		
		
		private function onEnterFrame(ev : Event) : void{
			cubeHolder.rotationX+=.2;
			cubeHolder.rotationY+=.4;
			cubeHolder.rotationZ+=.8;
			for(var i:int=0; i < CUBE_COUNT; i++) {
				objectPosition = orbitSpeed*objectInterval*i;    //    each object is individually updated
				/*	OBJECT MOVEMENT CODE GOES HERE	*/
				
				
				cubes[i].x = radius * (Math.cos(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				cubes[i].y = radius * (Math.sin(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				cubes[i].z = radius * Math.sin(theta + objectPosition) - radius*1*(Math.sin((radius/radius + 3) * (theta + objectPosition)));
				
				/*	OBJECT MOVEMENT CODE GOES HERE	*/
			}
			theta += (orbitSpeed*direction);
			view.render();
		}
	}
}

Most everything is a duplication of a block of code I snagged from Airtight Interactive’s Stage3D vs. WebGL demo. In addition to finally understanding something of how programming for 3d interfaces works, this gave me an opportunity to dive back into some of the trigonometry experiments I built back in the day.

In the code above, look at the method onEnterFrame. In it, inside the for loop, are three lines which update the x, y, and z coordinates of each block. Those blocks have some scary looking math attached to them; lots of sin and cos and radii, and thetas. I cheated – I actually wrote that code almost three years ago for my Simple Trigonometric Curves Tutorial over on Kongregate. X and Y positions are set using a transcendental Butterfly curve. The Z position is set using a variation on an Epicycloid curve. For fun, grab some of the other curves out of the tutorial, and plug them in in place of the current formulae. If you come up with something interesting, post it.