HTML5 Canvas Drawing Library Exploration: Raphael.js

This is the third post in a series of articles exploring Javascript drawing libraries. Others covered at this point include Easel.js and oCanvas.js.

The next library I am going to cover is Raphael.js. This one is a little different. Instead drawing to the canvas element, it makes use of SGV in modern browsers, and VML in old versions of Internet Explorer. Therefore it actually works in browsers all the way back to IE6!

From a programming perspective there isn’t much difference from the other libraries. Using SVG simplifies a lot of things, because SVG display elements are part of the HTML DOM, so the Raphael library doesn’t need to “fake it” with custom objects. On the other hand, because there are more individual elements on the screen at any one time – as opposed to drawings of elements in the canvas tag – performance tends to decline more quickly as visual complexity increases.

Once again, I have created the Trigonometron, trying to keep everything as close as I can to the code structure in the earlier articles.

Click here to launch the demo.

HTML

<!DOCTYPE html>
<html>
	<head>
		<style>
			body {background:#ffffff;margin:0;padding:0;overflow:hidden;}
			#myCanvas {position:absolute;left:0;top:0;width:800px;height:480px;}
		</style>
		<script type="text/javascript" src="raphael-min.js"></script>
	</head>
	<body>
		<div id="myCanvas"></div">
		<script type="text/javascript">
			// code goes here
		</script>
	</body>
</html>

As usual, not a whole lot to see here. Note that, instead of using a canvas element, we are using a div element. This is the element which will contain all of the SVG/VML pieces created by Raphaël.

Javascript

/*	global variables	*/
var canvas,
	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;
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 = init;

function init() {
	canvas = new Raphael("myCanvas",800,480);
	initInterface();
	initObjects();
	onEnterFrame();
	isInitialized = true;
	setInterval(onEnterFrame,25);
}
function initInterface() {
	var xOff = 625;
	var yOff = 25;
	for(var i=0;i<shapes.length;i++) {
		var btn = canvas.rect(xOff,yOff,150,20)
			.attr("fill","#ededed")
			.attr("strokeColor","#808080")
			.data("shapeName",shapes[i])
			.mouseover(function(){
				document.getElementById("myCanvas").style.cursor="pointer";
				this.attr("fill","#ffffff");
			})
			.mouseout(function(){
				document.getElementById("myCanvas").style.cursor="auto";
				this.attr("fill","#ededed");
			})
			.click(function(){
				currentShape = this.data("shapeName");
				isInitialized = false;
				onEnterFrame();
				isInitialized = true;
			});
		var txt = canvas.text(xOff+75,yOff+10,shapes[i]).attr({fill:"#000000","font-size":14,"font-family":"Courier","text-anchor":"middle"});
			txt.mouseover(function(){
				document.getElementById("myCanvas").style.cursor="pointer";
				this.graphicBase.attr("fill","#ffffff");
			})
			.mouseout(function(){
				document.getElementById("myCanvas").style.cursor="auto";
				this.graphicBase.attr("fill","#ededed");
			})
			.click(function(){
				currentShape = this.graphicBase.data("shapeName");
				isInitialized = false;
				onEnterFrame();
				isInitialized = true;
			});
		txt.graphicBase = btn;
		yOff += 23;
	}
	yOff += 23;
	var pauseBtn = canvas.rect(xOff,yOff,150,20)
		.attr("fill","#ededed")
		.attr("strokeColor","#808080")
		.mouseover(function(){
			document.getElementById("myCanvas").style.cursor="pointer";
			this.attr("fill","#ffffff");
		})
		.mouseout(function(){
			document.getElementById("myCanvas").style.cursor="auto";
			this.attr("fill","#ededed");
		})
		.click(function(){
			isPaused = !isPaused;
		});
	var txt = canvas.text(xOff+75,yOff+10,"PLAY/PAUSE").attr({fill:"#000000","font-size":14,"font-family":"Courier","text-anchor":"middle"});
	txt.graphicBase = pauseBtn;
	txt.mouseover(function(){
			document.getElementById("myCanvas").style.cursor="pointer";
			this.graphicBase.attr("fill","#ffffff");
		})
		.mouseout(function(){
			document.getElementById("myCanvas").style.cursor="auto";
			this.graphicBase.attr("fill","#ededed");
		})
		.click(function(){
			isPaused = !isPaused;
		});
}

function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
	var obj = canvas.circle(centerX,centerY,5);
	obj.attr({
		fill:'#'+randomRGB()+randomRGB()+randomRGB(),
		stroke:'#000000'
	});
	objects.push(obj);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}
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 onEnterFrame() {
	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].attr('cx',newX);
		objects[i].attr('cy',newY);
	}
	theta += (orbitSpeed*direction);
}

Here is line-by-line breakdown of the above code.

  • 2-5 – initialize global variables
  • 8-32 – initialize variables for the animation. Not Raphaël-specific
  • 34 – set the init() function to fire when the page finishes loading
  • 36-43 – init()
    • 37 – create a new Raphaël object
    • 38 – create the user interface
    • 39 – initialize the animation code
    • 40 – display the initial pattern on the screen
    • 42 – start the animation
  • 44-112 – initInterface()
    • 45-46 initialize the variables used to position the UI buttons
    • 47-83 – create one UI button for each animation pattern
      • 48-51 – create and style a gray rectangle
      • 52-55 – add the mouseover functionality
      • 56-59 – add the mouseout functionality
      • 60-65 – add the onclick functionality
      • 66 – create a text object
      • 67-70 – add mouseover functionality to the text object. This is necessary because, as a DOM element, the text object will block mouse events from reaching the gray square beneath it. Elements cannot be nested in Raphaël, so they must instead be positioned and stacked.
      • 71-74 – add mouseout functionality to the text object
      • 75-80 – add onclick functionality to the text object
      • 81- associate the button object underneath each text object with that text object
      • 82 – update the vertical positioning variable for the next button
      • 84 – add some space between the pattern buttons and the play/pause button
      • 85-111 – create the play/pause button, using the same methods as the pattern buttons above
  • 114-118 – initObjects()
    • 115-116 – create a number of circles on the screen equal to the value in the startingObjects variable
  • 119-128 – addObject()
    • 120 – create a new “circle” graphic object
    • 121-124 – style the color and outline stroke of the circle
    • 125 – add the circle to the objects array
    • 126-127 – update the variable which controls spacing between the circles on the screen
  • 129-132 – removeObject()  – not used in this demonstration
    • 130-131 – update the variable which controls spacing between the circles on the screen
  • 134-138 – function randomRGB() – create and return a random hexadecimal number between 0 and 255, inclusive.
  • 139-189 – onEnterFrame()
    • This is the function which animates the circles based on the current animation pattern, which is held in the variable “currentShape”
      • 140 – if the animation is currently paused, do nothing
      • 141 – begin iterating through each object in the animation
      • 142 – get the position of the circle graphic in the sequence of graphic objects
      • 143-184 – based on the variable “currentShape”, determine the new position of the circle graphic within the animation
      • 185-186 – update the position of the circle graphic on the screen
      • 188 – increment the rotation of the entire animation

So there you have it: A simple web app writting in Javascript, using the Raphaël graphics library. Be sure, when using this library, to remember that you are working with vector graphics and DOM elements, not custom Javascript objects drawn onto the <canvas/> element.

As in the other articles in this series, the math for the animations comes from my Simple Trigonometric Curves Tutorial over at Kongregate.

If you come up with something interesting using the Raphaël library, be sure to post a link to it in the comments!

HTML5 Canvas Drawing Library Exploration: oCanvas.js

This is part 2 of an ongoing series exploring the different Javascript/canvas element drawing libraries which are currently in use.

oCanvas.js is another small-footprint, easy to use Javascript library. In use it is similar to Easel.js (see my review of easel.js here), but the syntax is a little simpler. Where Easel feels more like Actionscript, oCanvas feels more like jQuery. This makes sense, since Easel was written to appeal to Flash developers.

Other than syntax, oCanvas and Easel seem to be interchangeable. Easel might have more fine-tuned control over drawing, but not enough to make a significant difference in any but the most complex web applications.

oCanvas is a lot of fun to play with, The syntax is easy to pick up; the documentation is well structured, and anyone familiar with jQuery should be able to extrapolate from the examples on the oCanvas site and have something up and running in a few minutes.

The only real downside to oCanvas is that no-one appears to be using it. Almost all of the articles I found which talk about oCanvas are of the “here are a dozen new Javascript drawing libraries” type, and they only link back to the oCanvas page. No examples out in the wild. No demos or source code other than that created by the oCanvas team. I think that is a shame. oCanvas deserves more attention than that.

So here is a simple demonstration of oCanvas, along with source code and commentary. Enjoy!

Click to Launch the demo.

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>oCanvas.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="ocanvas.js"></script>
		<script type="text/javascript" src="ocanvas-test-code.js"></script>
	</head>
	<body>
		<canvas id="myCanvas" width="800" height="480"></canvas>
	</body>
</html>

Nothing too exciting in the HTML. Be sure to include the width and height attributes in the canvas element so that the graphics therein are not distorted.

Javascript

/*	global variables	*/
var canvas,
	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;
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 = init;

function init() {
	canvas = oCanvas.create({
		canvas: "#myCanvas",
		background: "#ffffff"
	});
	
	initInterface();
	initObjects();
	
	
	onEnterFrame();
	isInitialized = true;
	canvas.setLoop(onEnterFrame).start();
}

function initInterface() {
	var xOff = 625;
	var yOff = 25;
	for(var i=0;i<shapes.length;i++) {
		var b = canvas.display.rectangle({
			x:xOff,
			y:yOff,
			width:150,
			height:20,
			fill:'#ededed',
			stroke:'1px outside #808080',
			shapeName:shapes[i]
		});
		var txt = canvas.display.text({
			x:75,
			y:4,
			align:'center',
			font:'12px courier,monospace',
			text:shapes[i],
			fill:'#000000'
		});
		b.addChild(txt);
		b.bind('mouseenter',function(){
			document.getElementById("myCanvas").style.cursor="pointer";
			this.fill = '#ffffff';
		});
		b.bind('mouseleave',function(){
			document.getElementById("myCanvas").style.cursor="auto";
			this.fill = '#ededed';
		});
		b.bind('click',function(){
			currentShape = this.shapeName;
			isInitialized = false;
			onEnterFrame();
			isInitialized = true;
		});
		yOff += 23;
		canvas.addChild(b);
	}
	yOff += 23;
	var pauseButton = canvas.display.rectangle({
		x:xOff,
		y:yOff,
		width:150,
		height:20,
		fill:'#ededed',
		stroke:'1px outside #808080'
	});
	var pauseButtonText = canvas.display.text({
		x:75,
		y:4,
		align:'center',
		font:'12px courier,monospace',
		text:"PLAY/PAUSE",
		fill:'#000000'
	});
	pauseButton.addChild(pauseButtonText);
	pauseButton.bind('mouseenter',function(){
		document.getElementById("myCanvas").style.cursor="pointer";
		this.fill = '#ffffff';
	});
	pauseButton.bind('mouseleave',function(){
		document.getElementById("myCanvas").style.cursor="auto";
		this.fill = '#ededed';
	});
	pauseButton.bind('click',function() {
		isPaused = !isPaused;
	});
	canvas.addChild(pauseButton);

}
function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
var obj = canvas.display.ellipse({
		x:centerX,
		y:centerY,
		radius_x:5,
		radius_y:5,
		stroke:"1px #000000",
		fill:"#"+randomRGB()+randomRGB()+randomRGB()
	});
	objects.push(obj);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
	canvas.addChild(obj);
}
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 onEnterFrame() {
	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);
}

Here is a line-by-line breakdown of the preceding Javascript:

  • 2-5
    • initializing global variables
  • 8-32
    • initializing variables for the demo. This is mostly all for the animation. Nothing here that is oCanvas-specific.
  • 34
    • This tells the browser to fire the init() function when the page finishes loading.
  • 36-49 – function init()
    • Fired when the page loads
      • 37-40 initialize the global canvas variable, and associate it with the HTML canvas element which has the id of “myCanvcas”
      • 42-43 – create the user interface, and initialize the objects for animation
      • 46 – call the animation function once in order to display the objects
      • 47 – set the global initialization state
      • 48 – begin the animation. By default the animation is paused
  • 51-121 – initInterface()
    • Create the user interface for the demo
      • 52-53 – set the base position for the UI buttons
      • 54-89 – create the UI buttons
        • 55-63 – create and style a rectangle graphic, and assign the “shapeName” property, which references one of the animation sequences
        • 64-71 – create and style a text block, which displays the name of one of the animation sequences
        • 72 – add the text block as a child of the rectangle graphic
        • 73-76 – add the mouseover behavior for the button
        • 77-80 – and the mouseout behavior for the button
        • 81-86 – add the onclick behavior to the button. Updates the “currentShape”  variable to equal the value of the button’s “shapeName” property
        • 87 – update the base Y position for the buttons
        • 88 – add the button object to the canvas object. This is how objects are made visible in oCanvas
      • 90 – update the Y variable to add some space between the preceding buttons and the play/pause button
      • 91-98 – create and style a rectangle graphic for the play/pause button
      • 99-106 – create a text block for the play/pause button
      • 107 – add the text block as a child of the play/pause button graphic
      • 108-111 – add mouseover functionality to the play/pause button
      • 112-115 – add mouseout functionality to the play/pause button
      • 116-118 – add onclick functionality to the play/pause button. A click will toggle the paused state of the demo
      • 119 – add the play/pause button to the canvas
  • 122-126 – initObjects()
    • This method counts up to the value of startingObjects and calls addObject once for each iteration
  • 127-140 – addObject()
    • this function creates a circle graphic, styles it, updates some of the animation variables, and adds the new object to the canvas
      • 128-135 – create and style a circle graphic
      • 136 – add the circle to the array of circles
      • 137 – update the numObjects variable to equal the number of elements in the objects array
      • 138 – update the spacing between circles, based on the number of circles in the animation
      • 139 – add the new circle graphic to the canvas
  • 141-144 – removeObject()
    • remove a circle from the animation. Not used in this demo
  • 146-150 – randomRGB()
    • create a random hexadecimal number between 0 and 255, convert it to a string, and return it for use in coloring the circle graphics.
  • 151-201 – onEnterFrame()
    • This is the function which animates the circles based on the current animation pattern, which is held in the variable “currentShape”
      • 152 – if the animation is currently paused, do nothing
      • 153 – begin iterating through each object in the animation
      • 154 – get the position of the circle in the sequence of circles
      • 155-196 – based on the variable “currentShape”, determine the new position of the circle within the animation
      • 197-198 – update the position of the circle within the canvas
      • 200 – increment the rotation of the entire animation

And there you have it. If you compare the code for this animation with the code in the Easel.js post, you will see that they are quite similar. The syntax for oCanvas is a little simpler, but it still allows the same control over style and position.

As in the other articles in this series, the math for the animations comes from my Simple Trigonometric Curves Tutorial over at Kongregate.

I hope that oCanvas gets some more attention from web developers. It is a worthy project.

 

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.

Away3d and Flash Player 11

Butterfly Curve animated with Away3d

Click the image above to launch the experiment. Requires Flash Player 11. May beat up on older computers. A lot.

What you see here is a simple example of what the Flash 11 player is capable of. There are 500 cubes, dynamically lit, moving through a “Transcendent Butterfly” curve on the x and y axes, and a variation of an epicycloid in the Z. The whole formation is oscillating through the x, y, and z axes as well. The little box in the upper left corner shows frame rate and the amount of RAM which the animation is using. I have had as many as 1000 cubes running through this animation but the frame rate dropped down below 30 FPS. 500 cubes is plenty for the moment.

This animation was created using the Away3d code library.

Procedural Generation 1 – Creating a Cave using Cellular Automata

Cellular Automata rule sets are quite useful for building cave-like systems.

  1. Fill a bitmap with random black and white dots. Black represents filled space, white represents open space.
  2. Iterate through the bitmap, applying the CA rules to each pixel
  3. create the updated bitmap
  4. cycle back to step 2, repeat until desired result

The following is the series, from random noise all the way to a completed cave system. Each image is 64×64, though it can easily be scaled up to any arbitrary size. I have found that images at 256×256 and above tend to bog down somewhat, so be careful.

Once you have tweaked the algorithms to get the desired cave design, you may have more than one disconnected piece. There are three ways you can re-connect those pieces to the cave system

  1. go through and eliminate all but the largest open area
    – This might result in a quite small cave, so some experimentation here might be necessary, such as setting an arbitrary number of pixels or percentage of overall area as the minimum allowed cave size
  2. draw tunnels to connect the pieces
    – either find the center point of each piece, and draw a tunnel from point A to point B, or iterate through each point in each open area, find there they are closest together, and connect those points
  3. in multilevel games, connect to the next level(s) up and down. A cave needn’t be only one level.
    – this assumes that the same issue does not exist in the next level up and/or down. An unfortunate selection of initial starting conditions could result in two “silo” caves next to each other, with few or no connection points.

There will be some back and forth between tweaking the algorithm and deciding what makes for the best caves. You can add logic to sometimes eliminate extra rooms, sometimes connect them, and sometimes connect them to the next level.

Of course, you don’t necessarily need to create all of the levels at load time. You can create them on the fly – though this does make vertical connections more difficult. Do some experimenting; see which method works best for you. I suggest always having at least two lower levels created, to reduce potential conflicts when fleshing out the current level.

Procedural Generation, Intro

This is the first of what I hope to be many posts exploring the topic of procedural generation, particularly as it applies to game development and art.

At it simplest level, procedural generation (pg) is the use of a small amount of code, or an algorithm, to create a result, rather than creating that result by hand. Randomness and pseudo-randomness generally figure into the process, as well as set theory, emergence, and a wide variety of mathematical concepts such as fractals, the Fibonacci sequence, cellular automata, Perlin noise algorithms, and occasionally cryptography.

PG starts with the creation of a series of bits or numbers, then branches out into the myriad uses to which that series can be applied. How the numbers are chosen is just as important. So PG starts a level lower, at the algorithm which creates the data.

A list of numbers can mean almost anything depending on its context. But for a given context, not all sets of numbers will work. Therefore it is important to have a number generator which will produce useful data for a given task. This is where experimentation comes in to play.

But enough of the high-level stuff.

I have several years of notes, graphics, experiments, and source code through which I am currently sorting. Over the upcoming months I will post breakdowns of some of them, particularly those which can be applied to game development. And in those, I will be providing ideas about how to make PG useful, and how to tweak things so that using this method actually saves time and effort. Here are some of the ideas which I will cover:

  • terrain generation
  • town placement
  • resource placement
  • maze generation
  • cave/dungeon generation and population
  • place name generation
  • graphics creation
  • plant/tree generation

…and various combinations of the above.

In the meantime, click here to see the nearly 30 old entries I have made in this blog regarding procedural generation.

Mersenne Twister in Actionscript

A few years ago I attempted to create a game for the GameDev.net Four Elements Contest. I had an idea that I wanted the game to be a cross between Nethack and Elite – and maybe a little Spore – which is to say, loads and loads of procedurally generated content. I never got past a very rough prototype of the world-building engine, but I learned a lot about procedural generation, and game development in general. Specifically, that it takes a lot more time than I generally have available.

One of the artifacts of this experiment was an extremely useful Mersenne Twister class, which I ported over from a C class I found on Wikipedia. A Mersenne Twister is a seeded pseudo-random number generator. In other words, for a given input n and a range r, it will return a random number between 0 (or whichever number you designate as the lower bound) and r, using n as the seed.

How is that useful? If you want to be able to, for instance, save a game which is based on random number-seeded procedural content, you want to be able to return the same seed every time. And if someone wants to start a new game, you want that seed to be different, but also repeatable. If you can’t reload a saved game and have it be based off the same random number as before, then loading a game would be no different from starting a new one.

Anyway. Here is the Actionscript 3 class:

/*
   A C-program for MT19937, with initialization improved 2002/1/26.
   Coded by Takuji Nishimura and Makoto Matsumoto.

   Before using, initialize the state by using init_genrand(seed)
   or init_by_array(init_key, key_length).

   Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
   All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions
   are met:

     1. Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.

     2. Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

     3. The names of its contributors may not be used to endorse or promote
        products derived from this software without specific prior written
        permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


   Any feedback is very welcome.
   http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html
   email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)

     -------------------

     Converted to Actionscript 2005 by John Winkelman
     Feedback welcome at john.winkelman@gmail.com
*/


/* Period parameters */
package org.eccesignum.utilities {
    public class MersenneTwister {
        private var N:Number = 624;
        private var M:Number = 397;
        private var MATRIX_A:Number = 0x9908b0df;   /* constant vector a */
        private var UPPER_MASK:Number = 0x80000000; /* most significant w-r bits */
        private var LOWER_MASK:Number = 0x7fffffff; /* least significant r bits */

        private var mt:Array; /* the array for the state vector  */
        private var mti:Number;

        private var seed:Number;
        private var returnLength:Number;
        private var maxSize:Number;

        private var returnArray:Array;


        public function MersenneTwister():void {

        }

        public function twist($seed:Number,$returnLength:int,$maxSize:int):Array {    //    seed number, number of values to return ,max size of returned number
            seed = $seed;
            returnLength = $returnLength;
            maxSize = $maxSize;
            mt = [];

            returnArray = [];

            mti = N+1; /* mti==N+1 means mt[N] is not initialized */
            var i:int;
            //var initArray=(0x123, 0x234, 0x345, 0x456);    //2010.04.20    modiied to the below
            var initArray:Array = [0x123, 0x234, 0x345, 0x456];
            init_by_array(initArray,initArray.length);
            for (i=0; i<returnLength; i++) {
                returnArray[i] = genrand_int32()%maxSize;
            }
            //returnArray.sort(16);
            //trace(returnArray);
            /*
            trace("\n1000 outputs of genrand_real2()\n");
            for (i=0; i<returnLength; i++) {
              trace(" " + genrand_real2());
              if (i%5==4) trace("\n");
            }
            */
            return returnArray;

        }


        /* initializes mt[N] with a seed */
        private function init_genrand($seed:Number):void {
            mt[0]= $seed & 0xffffffff;
            for (mti=1; mti<N; mti++) {
                mt[mti] = (1812433253 * (mt[mti-1] ^ (mt[mti-1] >> 30)) + mti);
                mt[mti] &= 0xffffffff;
                /* for >32 bit machines */
            }
        }

        /* initialize by an array with array-length */
        /* init_key is the array for initializing keys */
        /* key_length is its length */
        /* slight change for C++, 2004/2/26 */
        //    void init_by_array(unsigned long init_key[], int key_length)

        private function init_by_array($seedArray:Array,$seedArrayLength:Number):void {
            var i:Number = 1;
            var j:Number = 0;
            init_genrand(seed);
            //init_genrand(19650218);
            var k:Number = (N>$seedArrayLength) ? N : $seedArrayLength;
            for (k; k>0; k--) {
                mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1664525)) + $seedArray[j] + j; /* non linear */
                mt[i] &= 0xffffffff; /* for WORDSIZE > 32 machines */
                i++;
                j++;
                if (i >= N) {
                    mt[0] = mt[N-1];
                    i=1;
                }
                if (j >= $seedArrayLength) j=0;
            }
            for (k = N-1; k; k--) {
                mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1566083941)) - i; /* non linear */
                mt[i] &= 0xffffffff; /* for WORDSIZE > 32 machines */
                i++;
                if (i>=N) {
                    mt[0] = mt[N-1];
                    i=1;
                }
            }

            mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */
        }

        /* generates a random number on [0,0xffffffff]-interval */
        private function genrand_int32():Number    {
            var y:Number;
            var mag01:Array=[0x0, MATRIX_A];
            /* mag01[x] = x * MATRIX_A  for x=0,1 */

            if (mti >= N) { /* generate N words at one time */
                var kk:Number;

                if (mti == N+1)   /* if init_genrand() has not been called, */
                    init_genrand(5489); /* a default initial seed is used */

                for (kk=0;kk<N-M;kk++) {
                    y = (mt[kk]&UPPER_MASK)|(mt[kk+1]&LOWER_MASK);
                    mt[kk] = mt[kk+M] ^ (y >> 1) ^ mag01[y & 0x1];
                }
                for (;kk<N-1;kk++) {
                    y = (mt[kk]&UPPER_MASK)|(mt[kk+1]&LOWER_MASK);
                    mt[kk] = mt[kk+(M-N)] ^ (y >> 1) ^ mag01[y & 0x1];
                }
                y = (mt[N-1]&UPPER_MASK)|(mt[0]&LOWER_MASK);
                mt[N-1] = mt[M-1] ^ (y >> 1) ^ mag01[y & 0x1];

                mti = 0;
            }

            y = mt[mti++];

            /* Tempering */
            y ^= (y >> 11);
            y ^= (y << 7) & 0x9d2c5680;
            y ^= (y << 15) & 0xefc60000;
            y ^= (y >> 18);

            return y;
        }

        /* generates a random number on [0,0x7fffffff]-interval */
        private function genrand_int31():Number    {
            return (genrand_int32()>>1);
        }

        /* generates a random number on [0,1]-real-interval */
        private function genrand_real1():Number    {
            return genrand_int32()*(1.0/4294967295.0);
            /* divided by 2^32-1 */
        }

        /* generates a random number on [0,1)-real-interval */
        private function genrand_real2():Number {
            return genrand_int32()*(1.0/4294967296.0);
            /* divided by 2^32 */
        }

        /* generates a random number on (0,1)-real-interval */
        private function genrand_real3():Number    {
            return ((genrand_int32()) + 0.5)*(1.0/4294967296.0);
            /* divided by 2^32 */
        }

        /* generates a random number on [0,1) with 53-bit resolution*/
        private function genrand_res53():Number    {
            var a:Number = genrand_int32()>>5;
            var b:Number = genrand_int32()>>6;
            return(a*67108864.0+b)*(1.0/9007199254740992.0);
        }
        /* These real versions are due to Isaku Wada, 2002/01/09 added */
    }
}

And it is called like this:

var twister:MersenneTwister = new MersenneTwister();
twister.twist(17436,100,50000); // seed number, number of values to return, maximum size of a given value

Since I wrote this, many other people have made versions in Actionscript. There is a comprehensive list on the Mersenne Twister page at Wikipedia.

Lindenmayer System Basics: More on Branches

This post is one of a series exploring the creation of Lindenmayer System patterns using my Lindenmayer System Explorer.

The introduction of branching into our patterns, which we explored in my previous post, allows for a near infinite variety of designs. Often these pattens come remarkably close to the patterns seen in plant growth. In order to provide some realism to the patterns, there are a few more options in the explorer which are only available when creating branches: Line Scale, Line Taper, Angle Increment, and the option of using multiple colors.

Line Scale modifies the length of individual line segments. Line taper modifies the width of line segments. Angle Increment adjusts the angle that a branch is drawn from its parent.

The following images illustrate how each modification works. Clicking on an image will take you to the explorer pre-configured to recreate that image.

Start with this basic tree shape:

Now change the Line Scale to .75. You should end up with this:

Each branch is 75% of the length of its parent. This can be any number greater than 0. For branches twice the length of their parents, set the value to 2. For half as long, set it to .5.

For line thickness, update the line width so that it is something like 10:

Now change the Line Taper to .75:

Each branch is 75% of the thickness of its parent. You can use any number greater than 0. For instance, to have each branch twice as thick as its parent, you would set this field to 2. For half as thick, you would set it to .5.

Now change the Angle Increment to 10:

The angle of each branch from its parent is 10 degrees greater than that of the preceding branching. This can be any positive or negative number, though they will always evaluate to a value between -360 and 360.

Finally, you can use multiple colors, which are applied at each branching, by creating a comma-separated list of hexadecimal color numbers. This will have the following effect:

As the pattern is rendered, at each branch the next color in the list is used.

You can use any hexadecimal color you would like, and use as many as you would like.

So that, along with the previous post, is Lindenmayer System branches, in a nutshell. Enjoy!

Lindenmayer System Basics: Branches

This is the fifth in a series of blog posts explaining the usage of the Lindenmayer System Explorer. Clicking on an image will take you to the explorer page, pre-configured to draw that image.

So far, we have seen many different patterns created with the L-system explorer – fractals, dragon curves, snowflakes, and so on. They all have one thing in common: they are made up of a single line.

No branching yet

To create branches, enclose the rules for a branch in square brackets, like so:

[F]+[F]+[F]

Instead of yielding a bent line, it creates a pattern like this:

Branches, 1 iteration

Not terribly interesting yet, but it does allow for the creation of more interesting shapes. Remember: Astrid Lindenmayer was a botanist, and he originally created this system to model the structure of living plants. If we nest a few brackets, and play around with the angles, we can get patterns like this:

Something like a shrub

Branch rules can be nested within each other, to the extent that extremely complex patterns can emerge very quickly:

more branching, something like a wreath

And with a little practice, the patterns can become increasingly plant-like:

closer to a plant

An oddly symmetrical tree

So that’s it for branches. In the next post I will show how you can use branching to change the drawing angles, colors, line length and line thickness to create increasingly life-like plants.

More posts on this subject:

Lines
Angles
Rule sets
The Start Condition